1use std::collections::BTreeSet;
13use std::path::PathBuf;
14
15use bynk_ts::TsDecl;
16
17use super::runtime_modules::{RUNTIME_EXPORTS, RUNTIME_MODULES};
18
19pub const RUNTIME_DIR: &str = "__bynk_runtime";
21
22fn module_of(name: &str) -> Option<&'static str> {
24 RUNTIME_EXPORTS
25 .binary_search_by_key(&name, |(n, _)| n)
26 .ok()
27 .map(|i| RUNTIME_EXPORTS[i].1)
28}
29
30fn split_specifier(from: &str) -> Option<(&str, &str)> {
33 let (prefix, file) = from.rsplit_once('/')?;
34 let ext = file.strip_prefix("runtime.")?;
35 matches!(ext, "js" | "ts").then_some((&from[..=prefix.len()], ext))
36}
37
38pub(crate) fn split_runtime_import(type_only: bool, names: &[String], from: &str) -> Vec<TsDecl> {
47 try_split_runtime_import(type_only, names, from)
48 .unwrap_or_else(|| panic!("an import from {from} names a non-runtime export: {names:?}"))
49}
50
51fn try_split_runtime_import(type_only: bool, names: &[String], from: &str) -> Option<Vec<TsDecl>> {
55 let (prefix, ext) = split_specifier(from)?;
56 let mut groups: Vec<(&'static str, Vec<String>)> = RUNTIME_MODULES
57 .iter()
58 .map(|(m, _)| (*m, Vec::new()))
59 .collect();
60 for name in names {
61 let bare = name.strip_prefix("type ").unwrap_or(name);
62 let imported = bare.split(" as ").next().unwrap_or(bare);
63 let module = module_of(imported)?;
64 let group = groups
65 .iter_mut()
66 .find(|(m, _)| *m == module)
67 .expect("module is in the table");
68 group.1.push(name.clone());
69 }
70 Some(
71 groups
72 .into_iter()
73 .filter(|(_, names)| !names.is_empty())
74 .map(|(module, names)| {
75 let all_types = names.iter().all(|n| n.starts_with("type "));
76 let (type_only, names) = if all_types && !type_only {
77 (
78 true,
79 names
80 .iter()
81 .map(|n| n.trim_start_matches("type ").to_string())
82 .collect(),
83 )
84 } else {
85 (type_only, names)
86 };
87 TsDecl::Import {
88 type_only,
89 names,
90 from: format!("{prefix}{RUNTIME_DIR}/{module}.{ext}"),
91 }
92 })
93 .collect(),
94 )
95}
96
97fn import_statements(lines: &[&str]) -> Vec<(usize, usize, String)> {
101 let mut out = Vec::new();
102 let mut i = 0;
103 while i < lines.len() {
104 let t = lines[i].trim_start();
105 if t.starts_with("import ") || t.starts_with("import{") {
106 let mut j = i;
107 let mut text = t.trim_end().to_string();
108 while text.contains('{') && !text.contains('}') && j + 1 < lines.len() && j - i < 500 {
111 j += 1;
112 text.push(' ');
113 text.push_str(lines[j].trim());
114 }
115 out.push((i, j, text));
116 i = j + 1;
117 } else {
118 i += 1;
119 }
120 }
121 out
122}
123
124pub fn rewrite_runtime_imports(source: &str) -> String {
130 let crlf = source.contains("\r\n");
131 let nl = if crlf { "\r\n" } else { "\n" };
132 let lines: Vec<&str> = source
133 .split('\n')
134 .map(|l| l.trim_end_matches('\r'))
135 .collect();
136 let mut out: Vec<String> = Vec::with_capacity(lines.len());
137 let mut i = 0;
138 for (first, last, text) in import_statements(&lines) {
139 out.extend(lines[i..first].iter().map(|l| l.to_string()));
140 match rewrite_import_statement(&text) {
141 Some(rewritten) => {
142 let indent = &lines[first][..lines[first].len() - lines[first].trim_start().len()];
143 out.extend(rewritten.into_iter().map(|l| format!("{indent}{l}")));
144 }
145 None => out.extend(lines[first..=last].iter().map(|l| l.to_string())),
146 }
147 i = last + 1;
148 }
149 out.extend(lines[i..].iter().map(|l| l.to_string()));
150 out.join(nl)
151}
152
153fn rewrite_import_statement(stmt: &str) -> Option<Vec<String>> {
154 let rest = stmt.strip_prefix("import")?.trim_start();
155 let (type_only, rest) = match rest.strip_prefix("type ") {
156 Some(r) => (true, r.trim_start()),
157 None => (false, rest),
158 };
159 let rest = rest.strip_prefix('{')?;
160 let (list, rest) = rest.split_once('}')?;
161 let rest = rest.trim_start().strip_prefix("from")?.trim_start();
162 let quote = rest.chars().next().filter(|c| matches!(c, '"' | '\''))?;
163 let rest = &rest[1..];
164 let (spec, tail) = rest.split_once(quote)?;
165 split_specifier(spec)?;
166 let tail = tail.trim();
168 let comment = tail.strip_prefix(';').unwrap_or(tail).trim();
169 if !(comment.is_empty() || comment.starts_with("//") || comment.starts_with("/*")) {
170 return None;
171 }
172 let names: Vec<String> = list
173 .split(',')
174 .map(|n| n.trim().to_string())
175 .filter(|n| !n.is_empty())
176 .collect();
177 let decls = try_split_runtime_import(type_only, &names, spec)?;
178 let mut lines: Vec<String> = decls
179 .iter()
180 .map(|d| match d {
181 TsDecl::Import {
182 type_only,
183 names,
184 from,
185 } => format!(
186 "import {}{{ {} }} from {quote}{from}{quote};",
187 if *type_only { "type " } else { "" },
188 names.join(", ")
189 ),
190 _ => unreachable!("split_runtime_import yields named imports"),
191 })
192 .collect();
193 if let (false, Some(last)) = (comment.is_empty(), lines.last_mut()) {
194 last.push(' ');
195 last.push_str(comment);
196 }
197 Some(lines)
198}
199
200fn reached_module(specifier: &str) -> Option<(&str, &str)> {
202 let rest = specifier.rsplit_once(&format!("{RUNTIME_DIR}/"))?.1;
203 let (module, ext) = rest.rsplit_once('.')?;
204 Some((module, ext))
205}
206
207pub(crate) fn reached_in(program: &bynk_ts::TsProgram, out: &mut BTreeSet<(String, String)>) {
210 let mut note = |spec: &str| {
211 if let Some((m, e)) = reached_module(spec) {
212 out.insert((m.to_string(), e.to_string()));
213 }
214 };
215 for spec in program.import_specifiers() {
216 note(spec);
217 }
218 for (_, text) in program.verbatim_content() {
219 let lines: Vec<&str> = text.lines().collect();
220 for (_, _, stmt) in import_statements(&lines) {
221 if let Some((_, tail)) = stmt.rsplit_once(" from ") {
222 let spec = tail.trim().trim_start_matches(['"', '\'']);
223 if let Some(end) = spec.find(['"', '\'']) {
224 note(&spec[..end]);
225 }
226 }
227 }
228 }
229}
230
231pub(crate) fn closure_files(reached: &BTreeSet<(String, String)>) -> Vec<(PathBuf, String)> {
235 let mut files = Vec::new();
236 for ext in reached
237 .iter()
238 .map(|(_, e)| e.as_str())
239 .collect::<BTreeSet<_>>()
240 {
241 let mut want: Vec<&str> = reached
242 .iter()
243 .filter(|(_, e)| e == ext)
244 .map(|(m, _)| m.as_str())
245 .collect();
246 let mut done: BTreeSet<&str> = BTreeSet::new();
247 while let Some(m) = want.pop() {
248 if !done.insert(m) {
249 continue;
250 }
251 let Some((_, src)) = RUNTIME_MODULES.iter().find(|(n, _)| *n == m) else {
254 done.remove(m);
255 continue;
256 };
257 for line in src.lines().filter(|l| l.starts_with("import ")) {
258 if let Some(dep) = line
259 .rsplit_once("\"./")
260 .and_then(|(_, t)| t.strip_suffix(".ts\";"))
261 {
262 want.push(dep);
263 }
264 }
265 }
266 for m in done {
267 files.push((module_path(m), module_source(m, ext)));
268 }
269 }
270 files
271}
272
273fn module_path(module: &str) -> PathBuf {
276 PathBuf::from(format!("{RUNTIME_DIR}/{module}.ts"))
277}
278
279fn module_source(module: &str, ext: &str) -> String {
281 let (_, src) = RUNTIME_MODULES
282 .iter()
283 .find(|(n, _)| *n == module)
284 .expect("module is in the table");
285 if ext == "ts" {
286 return src.to_string();
287 }
288 src.lines()
289 .map(|l| {
290 if l.starts_with("import ") {
291 l.replace(".ts\";", &format!(".{ext}\";"))
292 } else {
293 l.to_string()
294 }
295 })
296 .collect::<Vec<_>>()
297 .join("\n")
298 + "\n"
299}
300
301pub fn emit_all_runtime_modules() -> Vec<(PathBuf, String)> {
304 RUNTIME_MODULES
305 .iter()
306 .map(|(m, _)| (module_path(m), module_source(m, "js")))
307 .collect()
308}
309
310pub(crate) fn runtime_import_stmts(
313 type_only: bool,
314 names: &[String],
315 from: &str,
316) -> Vec<bynk_ts::TsStmt> {
317 split_runtime_import(type_only, names, from)
318 .into_iter()
319 .map(|d| bynk_ts::TsStmt::decl(d, None))
320 .collect()
321}