1use crate::{Config, Flow, TypeVisitor};
4use std::any::TypeId;
5use std::collections::{HashMap, HashSet};
6use std::path::{Path, PathBuf};
7use std::sync::{Mutex, OnceLock};
8
9fn file_lock(path: &Path) -> &'static Mutex<()> {
20 static LOCKS: OnceLock<Mutex<HashMap<PathBuf, &'static Mutex<()>>>> = OnceLock::new();
21 let map_mutex = LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
22 let mut map = map_mutex.lock().unwrap_or_else(|e| e.into_inner());
23 let key = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
27 *map.entry(key).or_insert_with(|| Box::leak(Box::new(Mutex::new(()))))
28}
29
30#[derive(Debug, thiserror::Error)]
32pub enum ExportError {
33 #[error("type `{0}` cannot be exported")]
34 CannotBeExported(&'static str),
35 #[error(transparent)]
36 Io(#[from] std::io::Error),
37 #[error(transparent)]
38 Fmt(#[from] std::fmt::Error),
39}
40
41pub fn export_to<T: Flow + 'static + ?Sized>(cfg: &Config, path: &Path) -> Result<(), ExportError> {
43 if let Some(parent) = path.parent() {
44 std::fs::create_dir_all(parent)?;
45 }
46
47 let _guard = file_lock(path).lock().unwrap_or_else(|e| e.into_inner());
48
49 let type_name = T::ident(cfg);
50
51 if path.exists() {
52 let existing = std::fs::read_to_string(path)?;
53 let type_marker = format!("type {type_name}");
54 let enum_marker = format!("enum {type_name}");
55
56 if existing.contains(&type_marker) || existing.contains(&enum_marker) {
57 return Ok(());
58 }
59
60 let decl_line = format_decl::<T>(cfg);
62 let mut merged = existing;
63 if !merged.ends_with('\n') {
64 merged.push('\n');
65 }
66 merged.push('\n');
67 if let Some(docs) = T::docs() {
68 merged.push_str(&format_docs(&docs));
69 }
70 merged.push_str(&decl_line);
71 merged.push('\n');
72 std::fs::write(path, merged)?;
73 } else {
74 let content = export_to_string::<T>(cfg)?;
75 std::fs::write(path, content)?;
76 }
77
78 Ok(())
79}
80
81pub fn export_all_into<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<(), ExportError> {
83 let mut seen = HashSet::new();
84 export_recursive::<T>(cfg, &mut seen)
85}
86
87struct Visit<'a> {
88 cfg: &'a Config,
89 seen: &'a mut HashSet<TypeId>,
90 error: Option<ExportError>,
91}
92
93impl TypeVisitor for Visit<'_> {
94 fn visit<T: Flow + 'static + ?Sized>(&mut self) {
95 if self.error.is_some() || <T as crate::Flow>::output_path().is_none() {
96 return;
97 }
98 self.error = export_recursive::<T>(self.cfg, self.seen).err();
99 }
100}
101
102fn export_recursive<T: Flow + ?Sized + 'static>(
103 cfg: &Config,
104 seen: &mut HashSet<TypeId>,
105) -> Result<(), ExportError> {
106 if !seen.insert(TypeId::of::<T>()) {
107 return Ok(());
108 }
109
110 let mut visitor = Visit {
112 cfg,
113 seen,
114 error: None,
115 };
116 <T as crate::Flow>::visit_dependencies(&mut visitor);
117
118 if let Some(e) = visitor.error {
119 return Err(e);
120 }
121
122 let base = <T as crate::Flow>::output_path()
123 .ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
124 let relative = cfg.resolve_output_path(&base);
125 let path = cfg.out_dir().join(relative);
126 export_to::<T>(cfg, &path)
127}
128
129pub fn export_to_string<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<String, ExportError> {
134 let mut content = String::from("// @flow\n// Generated by flowjs-rs. Do not edit.\n\n");
135
136 let self_id = TypeId::of::<T>();
137 let self_base = <T as crate::Flow>::output_path();
138 let self_path = self_base.as_ref().map(|b| cfg.resolve_output_path(b));
139 let deps = T::dependencies(cfg);
140 let mut seen_imports = HashSet::new();
141
142 for dep in &deps {
143 if dep.type_id == self_id || !seen_imports.insert(dep.type_id) {
144 continue;
145 }
146
147 let dep_path = cfg.resolve_output_path(&dep.output_path);
149
150 if let Some(ref self_p) = self_path {
152 if &dep_path == self_p {
153 continue;
154 }
155 }
156
157 let import_path = dep_path.to_str().unwrap_or(&dep.flow_name);
158 content.push_str(&format!(
159 "import type {{ {} }} from './{import_path}';\n",
160 dep.flow_name
161 ));
162 }
163 if !seen_imports.is_empty() {
164 content.push('\n');
165 }
166
167 if let Some(docs) = T::docs() {
168 content.push_str(&format_docs(&docs));
169 }
170
171 content.push_str(&format_decl::<T>(cfg));
172 content.push('\n');
173
174 Ok(content)
175}
176
177fn format_decl<T: Flow + ?Sized>(cfg: &Config) -> String {
179 let decl = T::decl(cfg);
180 if decl.starts_with("declare export") {
181 decl
182 } else {
183 format!("export {decl}")
184 }
185}
186
187fn format_docs(docs: &str) -> String {
189 let mut out = String::from("/**\n");
190 for line in docs.lines() {
191 out.push_str(&format!(" * {line}\n"));
192 }
193 out.push_str(" */\n");
194 out
195}