Skip to main content

flowjs_rs/
export.rs

1//! File export logic for Flow type declarations.
2
3use crate::{Config, Flow, TypeVisitor};
4use std::any::TypeId;
5use std::collections::HashSet;
6use std::path::Path;
7
8/// Export error.
9#[derive(Debug, thiserror::Error)]
10pub enum ExportError {
11    #[error("type `{0}` cannot be exported")]
12    CannotBeExported(&'static str),
13    #[error(transparent)]
14    Io(#[from] std::io::Error),
15    #[error(transparent)]
16    Fmt(#[from] std::fmt::Error),
17}
18
19/// Write a Flow type declaration to a file.
20pub fn export_to<T: Flow + 'static + ?Sized>(cfg: &Config, path: &Path) -> Result<(), ExportError> {
21    if let Some(parent) = path.parent() {
22        std::fs::create_dir_all(parent)?;
23    }
24
25    let type_name = T::ident(cfg);
26
27    if path.exists() {
28        let existing = std::fs::read_to_string(path)?;
29        let marker = format!("type {type_name}");
30
31        if existing.contains(&marker) {
32            return Ok(());
33        }
34
35        // Same file — append only the declaration, skip header and imports
36        let decl_line = format_decl::<T>(cfg);
37        let mut merged = existing;
38        if !merged.ends_with('\n') {
39            merged.push('\n');
40        }
41        merged.push('\n');
42        if let Some(docs) = T::docs() {
43            merged.push_str(&format_docs(&docs));
44        }
45        merged.push_str(&decl_line);
46        merged.push('\n');
47        std::fs::write(path, merged)?;
48    } else {
49        let content = export_to_string::<T>(cfg)?;
50        std::fs::write(path, content)?;
51    }
52
53    Ok(())
54}
55
56/// Export `T` together with all of its dependencies.
57pub fn export_all_into<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<(), ExportError> {
58    let mut seen = HashSet::new();
59    export_recursive::<T>(cfg, &mut seen)
60}
61
62struct Visit<'a> {
63    cfg: &'a Config,
64    seen: &'a mut HashSet<TypeId>,
65    error: Option<ExportError>,
66}
67
68impl TypeVisitor for Visit<'_> {
69    fn visit<T: Flow + 'static + ?Sized>(&mut self) {
70        if self.error.is_some() || <T as crate::Flow>::output_path().is_none() {
71            return;
72        }
73        self.error = export_recursive::<T>(self.cfg, self.seen).err();
74    }
75}
76
77fn export_recursive<T: Flow + ?Sized + 'static>(
78    cfg: &Config,
79    seen: &mut HashSet<TypeId>,
80) -> Result<(), ExportError> {
81    if !seen.insert(TypeId::of::<T>()) {
82        return Ok(());
83    }
84
85    // Export dependencies first so they exist before the parent
86    let mut visitor = Visit {
87        cfg,
88        seen,
89        error: None,
90    };
91    <T as crate::Flow>::visit_dependencies(&mut visitor);
92
93    if let Some(e) = visitor.error {
94        return Err(e);
95    }
96
97    let relative = <T as crate::Flow>::output_path()
98        .ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
99    let path = cfg.out_dir().join(relative);
100    export_to::<T>(cfg, &path)
101}
102
103/// Render the full file content for type `T` as a string.
104///
105/// Includes the `// @flow` header, import statements for dependencies that
106/// export to different files, and the type declaration.
107pub fn export_to_string<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<String, ExportError> {
108    let mut content = String::from("// @flow\n// Generated by flowjs-rs. Do not edit.\n\n");
109
110    let self_id = TypeId::of::<T>();
111    let self_path = <T as crate::Flow>::output_path();
112    let deps = T::dependencies(cfg);
113    let mut seen_imports = HashSet::new();
114
115    for dep in &deps {
116        if dep.type_id == self_id || !seen_imports.insert(dep.type_id) {
117            continue;
118        }
119
120        // Skip import if dependency exports to the same file
121        if let Some(ref self_p) = self_path {
122            if &dep.output_path == self_p {
123                continue;
124            }
125        }
126
127        let import_path = dep.output_path.to_str().unwrap_or(&dep.flow_name);
128        content.push_str(&format!(
129            "import type {{ {} }} from './{import_path}';\n",
130            dep.flow_name
131        ));
132    }
133    if !seen_imports.is_empty() {
134        content.push('\n');
135    }
136
137    if let Some(docs) = T::docs() {
138        content.push_str(&format_docs(&docs));
139    }
140
141    content.push_str(&format_decl::<T>(cfg));
142    content.push('\n');
143
144    Ok(content)
145}
146
147/// Format the declaration line with `export` prefix.
148fn format_decl<T: Flow + ?Sized>(cfg: &Config) -> String {
149    let decl = T::decl(cfg);
150    if decl.starts_with("declare export") {
151        decl
152    } else {
153        format!("export {decl}")
154    }
155}
156
157/// Format doc comments as JSDoc.
158fn format_docs(docs: &str) -> String {
159    let mut out = String::from("/**\n");
160    for line in docs.lines() {
161        out.push_str(&format!(" * {line}\n"));
162    }
163    out.push_str(" */\n");
164    out
165}