1use crate::{Config, Flow, TypeVisitor};
4use std::any::TypeId;
5use std::collections::HashSet;
6use std::path::Path;
7
8#[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
19pub 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 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
56pub 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 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
103pub 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 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
147fn 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
157fn 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}