1use anyhow::{anyhow, Result};
7use oxc_allocator::Allocator;
8use oxc_ast::ast::{Declaration, ExportDefaultDeclarationKind, Statement};
9use oxc_codegen::Codegen;
10use oxc_parser::Parser;
11use oxc_semantic::SemanticBuilder;
12use oxc_span::SourceType;
13use oxc_transformer::{TransformOptions, Transformer};
14use std::collections::HashSet;
15use std::path::{Path, PathBuf};
16
17pub fn transpile_typescript(source: &str, filename: &str) -> Result<String> {
19 let allocator = Allocator::default();
20 let source_type = SourceType::from_path(filename).unwrap_or_default();
21
22 let parser_ret = Parser::new(&allocator, source, source_type).parse();
24 if !parser_ret.errors.is_empty() {
25 let errors: Vec<String> = parser_ret.errors.iter().map(|e| e.to_string()).collect();
26 return Err(anyhow!("TypeScript parse errors: {}", errors.join("; ")));
27 }
28
29 let mut program = parser_ret.program;
30
31 let semantic_ret = SemanticBuilder::new().build(&program);
33
34 if !semantic_ret.errors.is_empty() {
35 let errors: Vec<String> = semantic_ret.errors.iter().map(|e| e.to_string()).collect();
36 return Err(anyhow!("Semantic errors: {}", errors.join("; ")));
37 }
38
39 let scoping = semantic_ret.semantic.into_scoping();
41
42 let transform_options = TransformOptions::default();
44 let transformer_ret = Transformer::new(&allocator, Path::new(filename), &transform_options)
45 .build_with_scoping(scoping, &mut program);
46
47 if !transformer_ret.errors.is_empty() {
48 let errors: Vec<String> = transformer_ret
49 .errors
50 .iter()
51 .map(|e| e.to_string())
52 .collect();
53 return Err(anyhow!("Transform errors: {}", errors.join("; ")));
54 }
55
56 let codegen_ret = Codegen::new().build(&program);
58
59 Ok(codegen_ret.code)
60}
61
62pub fn has_es_module_syntax(source: &str) -> bool {
65 let has_imports = source.contains("import ") && source.contains(" from ");
67 let has_exports = source.lines().any(|line| {
69 let trimmed = line.trim();
70 trimmed.starts_with("export ")
71 });
72 has_imports || has_exports
73}
74
75pub fn has_es_imports(source: &str) -> bool {
78 source.contains("import ") && source.contains(" from ")
79}
80
81pub fn extract_plugin_dependencies(source: &str) -> Vec<String> {
91 let prefix = "fresh:plugin/";
92 let mut deps = Vec::new();
93 let mut seen = HashSet::new();
94
95 for line in source.lines() {
96 let trimmed = line.trim();
97 if !trimmed.starts_with("import ") || !trimmed.contains(prefix) {
99 continue;
100 }
101 if let Some(from_idx) = trimmed.find(" from ") {
103 let after_from = &trimmed[from_idx + 6..]; let after_from = after_from.trim();
105 let quote_char = after_from.chars().next();
107 if let Some(q) = quote_char {
108 if q == '"' || q == '\'' {
109 if let Some(end) = after_from[1..].find(q) {
110 let module_path = &after_from[1..1 + end];
111 if let Some(plugin_name) = module_path.strip_prefix(prefix) {
112 if !plugin_name.is_empty() && seen.insert(plugin_name.to_string()) {
113 deps.push(plugin_name.to_string());
114 }
115 }
116 }
117 }
118 }
119 }
120 }
121
122 deps
123}
124
125pub fn topological_sort_plugins(
132 plugin_names: &[String],
133 dependencies: &std::collections::HashMap<String, Vec<String>>,
134) -> Result<Vec<String>> {
135 use std::collections::HashMap;
136
137 let mut in_degree: HashMap<&str, usize> = HashMap::new();
139 let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
140
141 for name in plugin_names {
142 in_degree.entry(name.as_str()).or_insert(0);
143 }
144
145 for name in plugin_names {
146 if let Some(deps) = dependencies.get(name) {
147 for dep in deps {
148 if in_degree.contains_key(dep.as_str()) {
150 *in_degree.entry(name.as_str()).or_insert(0) += 1;
151 dependents
152 .entry(dep.as_str())
153 .or_default()
154 .push(name.as_str());
155 } else {
156 return Err(anyhow!(
157 "Plugin '{}' depends on '{}', which is not installed or not enabled",
158 name,
159 dep
160 ));
161 }
162 }
163 }
164 }
165
166 let mut queue: Vec<&str> = in_degree
168 .iter()
169 .filter(|(_, °)| deg == 0)
170 .map(|(&name, _)| name)
171 .collect();
172 queue.sort();
174
175 let mut result: Vec<String> = Vec::with_capacity(plugin_names.len());
176
177 while let Some(current) = queue.first().copied() {
178 queue.remove(0);
179 result.push(current.to_string());
180
181 if let Some(deps) = dependents.get(current) {
182 let mut newly_ready = Vec::new();
183 for &dependent in deps {
184 if let Some(deg) = in_degree.get_mut(dependent) {
185 *deg -= 1;
186 if *deg == 0 {
187 newly_ready.push(dependent);
188 }
189 }
190 }
191 newly_ready.sort();
193 queue.extend(newly_ready);
194 queue.sort(); }
196 }
197
198 if result.len() != plugin_names.len() {
199 let in_result: HashSet<&str> = result.iter().map(|s| s.as_str()).collect();
201 let cycle_plugins: Vec<String> = plugin_names
202 .iter()
203 .filter(|n| !in_result.contains(n.as_str()))
204 .cloned()
205 .collect();
206 return Err(anyhow!(
207 "Plugin dependency cycle detected among: {}. These plugins will not be loaded.",
208 cycle_plugins.join(", ")
209 ));
210 }
211
212 Ok(result)
213}
214
215#[derive(Debug, Clone)]
217struct ModuleMetadata {
218 path: PathBuf,
220 var_name: String,
222 imports: Vec<ImportBinding>,
224 exports: Vec<ExportBinding>,
226 reexports: Vec<ReexportBinding>,
228 code: String,
230}
231
232#[derive(Debug, Clone)]
233struct ImportBinding {
234 local_name: String,
236 imported_name: Option<String>,
238 source_path: String,
240 is_namespace: bool,
242}
243
244#[derive(Debug, Clone)]
245struct ExportBinding {
246 exported_name: String,
248 local_name: String,
250}
251
252#[derive(Debug, Clone)]
253struct ReexportBinding {
254 exported_name: Option<String>,
256 source_name: Option<String>,
258 source_path: String,
260}
261
262pub fn bundle_module(entry_path: &Path) -> Result<String> {
265 let mut modules: Vec<ModuleMetadata> = Vec::new();
266 let mut visited = HashSet::new();
267 let mut path_to_var: std::collections::HashMap<PathBuf, String> =
268 std::collections::HashMap::new();
269
270 collect_modules(entry_path, &mut visited, &mut modules, &mut path_to_var)?;
272
273 let mut output = String::new();
275
276 for (i, module) in modules.iter().enumerate() {
277 let is_entry = i == modules.len() - 1;
278 output.push_str(&generate_scoped_module(module, &path_to_var, is_entry)?);
279 output.push('\n');
280 }
281
282 Ok(output)
283}
284
285fn collect_modules(
287 path: &Path,
288 visited: &mut HashSet<PathBuf>,
289 modules: &mut Vec<ModuleMetadata>,
290 path_to_var: &mut std::collections::HashMap<PathBuf, String>,
291) -> Result<()> {
292 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
293 if visited.contains(&canonical) {
294 return Ok(()); }
296 visited.insert(canonical.clone());
297
298 let source = std::fs::read_to_string(path)
299 .map_err(|e| anyhow!("Failed to read {}: {}", path.display(), e))?;
300
301 let (imports, exports, reexports) = extract_module_bindings(&source);
303
304 let parent_dir = path.parent().unwrap_or(Path::new("."));
305
306 for import in &imports {
308 if import.source_path.starts_with("./") || import.source_path.starts_with("../") {
309 let resolved = resolve_import(&import.source_path, parent_dir)?;
310 collect_modules(&resolved, visited, modules, path_to_var)?;
311 }
312 }
313 for reexport in &reexports {
314 if reexport.source_path.starts_with("./") || reexport.source_path.starts_with("../") {
315 let resolved = resolve_import(&reexport.source_path, parent_dir)?;
316 collect_modules(&resolved, visited, modules, path_to_var)?;
317 }
318 }
319
320 let var_name = path_to_module_var(path);
322 path_to_var.insert(canonical.clone(), var_name.clone());
323
324 let stripped = strip_imports_and_exports(&source);
326 let filename = path.to_str().unwrap_or("unknown.ts");
327 let transpiled = transpile_typescript(&stripped, filename)?;
328
329 modules.push(ModuleMetadata {
330 path: canonical,
331 var_name,
332 imports,
333 exports,
334 reexports,
335 code: transpiled,
336 });
337
338 Ok(())
339}
340
341fn path_to_module_var(path: &Path) -> String {
343 let name = path
344 .file_stem()
345 .and_then(|s| s.to_str())
346 .unwrap_or("module");
347
348 let sanitized: String = name
350 .chars()
351 .map(|c| if c.is_alphanumeric() { c } else { '_' })
352 .collect();
353
354 use std::hash::{Hash, Hasher};
356 let mut hasher = std::collections::hash_map::DefaultHasher::new();
357 path.hash(&mut hasher);
358 let hash = hasher.finish();
359
360 format!("__mod_{}_{:x}", sanitized, hash & 0xFFFF)
361}
362
363fn generate_scoped_module(
365 module: &ModuleMetadata,
366 path_to_var: &std::collections::HashMap<PathBuf, String>,
367 is_entry: bool,
368) -> Result<String> {
369 let mut code = String::new();
370
371 if is_entry {
373 code.push_str("(function() {\n");
374 } else {
375 code.push_str(&format!("const {} = (function() {{\n", module.var_name));
376 }
377
378 for import in &module.imports {
380 if let Some(dep_var) = resolve_import_to_var(&import.source_path, &module.path, path_to_var)
381 {
382 if import.is_namespace {
383 code.push_str(&format!("const {} = {};\n", import.local_name, dep_var));
385 } else if let Some(ref imported_name) = import.imported_name {
386 if imported_name == "default" {
388 code.push_str(&format!(
389 "const {} = {}.default;\n",
390 import.local_name, dep_var
391 ));
392 } else if &import.local_name == imported_name {
393 code.push_str(&format!("const {{{}}} = {};\n", import.local_name, dep_var));
394 } else {
395 code.push_str(&format!(
396 "const {{{}: {}}} = {};\n",
397 imported_name, import.local_name, dep_var
398 ));
399 }
400 } else {
401 code.push_str(&format!(
403 "const {} = {}.default;\n",
404 import.local_name, dep_var
405 ));
406 }
407 }
408 }
409
410 code.push_str(&module.code);
412 code.push('\n');
413
414 if !is_entry {
416 code.push_str("return {");
417
418 let mut export_parts: Vec<String> = Vec::new();
419
420 for export in &module.exports {
422 if export.exported_name == export.local_name {
423 export_parts.push(export.exported_name.clone());
424 } else {
425 export_parts.push(format!("{}: {}", export.exported_name, export.local_name));
426 }
427 }
428
429 for reexport in &module.reexports {
431 if let Some(dep_var) =
432 resolve_import_to_var(&reexport.source_path, &module.path, path_to_var)
433 {
434 match (&reexport.exported_name, &reexport.source_name) {
435 (Some(exported), Some(source)) => {
436 export_parts.push(format!("{}: {}.{}", exported, dep_var, source));
438 }
439 (Some(exported), None) => {
440 export_parts.push(format!("{}: {}.{}", exported, dep_var, exported));
442 }
443 (None, None) => {
444 export_parts.push(format!("...{}", dep_var));
446 }
447 _ => {}
448 }
449 }
450 }
451
452 code.push_str(&export_parts.join(", "));
453 code.push_str("};\n");
454 }
455
456 code.push_str("})();\n");
458
459 Ok(code)
460}
461
462fn resolve_import_to_var(
464 source_path: &str,
465 importer_path: &Path,
466 path_to_var: &std::collections::HashMap<PathBuf, String>,
467) -> Option<String> {
468 if !source_path.starts_with("./") && !source_path.starts_with("../") {
469 return None; }
471
472 let parent_dir = importer_path.parent().unwrap_or(Path::new("."));
473 if let Ok(resolved) = resolve_import(source_path, parent_dir) {
474 let canonical = resolved.canonicalize().unwrap_or(resolved);
475 path_to_var.get(&canonical).cloned()
476 } else {
477 None
478 }
479}
480
481fn extract_module_bindings(
483 source: &str,
484) -> (Vec<ImportBinding>, Vec<ExportBinding>, Vec<ReexportBinding>) {
485 let allocator = Allocator::default();
486 let source_type = SourceType::default()
487 .with_module(true)
488 .with_typescript(true);
489
490 let parser_ret = Parser::new(&allocator, source, source_type).parse();
491 if !parser_ret.errors.is_empty() {
492 return (Vec::new(), Vec::new(), Vec::new());
493 }
494
495 let mut imports = Vec::new();
496 let mut exports = Vec::new();
497 let mut reexports = Vec::new();
498
499 for stmt in &parser_ret.program.body {
500 match stmt {
501 Statement::ImportDeclaration(import_decl) => {
502 let source_path = import_decl.source.value.to_string();
503
504 if let Some(specifiers) = &import_decl.specifiers {
506 for spec in specifiers {
507 match spec {
508 oxc_ast::ast::ImportDeclarationSpecifier::ImportSpecifier(s) => {
509 imports.push(ImportBinding {
510 local_name: s.local.name.to_string(),
511 imported_name: Some(s.imported.name().to_string()),
512 source_path: source_path.clone(),
513 is_namespace: false,
514 });
515 }
516 oxc_ast::ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
517 imports.push(ImportBinding {
518 local_name: s.local.name.to_string(),
519 imported_name: None, source_path: source_path.clone(),
521 is_namespace: false,
522 });
523 }
524 oxc_ast::ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(
525 s,
526 ) => {
527 imports.push(ImportBinding {
528 local_name: s.local.name.to_string(),
529 imported_name: None,
530 source_path: source_path.clone(),
531 is_namespace: true,
532 });
533 }
534 }
535 }
536 }
537 }
538
539 Statement::ExportNamedDeclaration(export_decl) => {
540 if let Some(ref source) = export_decl.source {
541 let source_path = source.value.to_string();
543 for spec in &export_decl.specifiers {
544 reexports.push(ReexportBinding {
545 exported_name: Some(spec.exported.name().to_string()),
546 source_name: Some(spec.local.name().to_string()),
547 source_path: source_path.clone(),
548 });
549 }
550 } else {
551 if let Some(ref decl) = export_decl.declaration {
553 for name in get_declaration_names(decl) {
555 exports.push(ExportBinding {
556 exported_name: name.clone(),
557 local_name: name,
558 });
559 }
560 }
561 for spec in &export_decl.specifiers {
563 exports.push(ExportBinding {
564 exported_name: spec.exported.name().to_string(),
565 local_name: spec.local.name().to_string(),
566 });
567 }
568 }
569 }
570
571 Statement::ExportDefaultDeclaration(export_default) => {
572 match &export_default.declaration {
574 ExportDefaultDeclarationKind::FunctionDeclaration(f) => {
575 if let Some(ref id) = f.id {
576 exports.push(ExportBinding {
577 exported_name: "default".to_string(),
578 local_name: id.name.to_string(),
579 });
580 }
581 }
582 ExportDefaultDeclarationKind::ClassDeclaration(c) => {
583 if let Some(ref id) = c.id {
584 exports.push(ExportBinding {
585 exported_name: "default".to_string(),
586 local_name: id.name.to_string(),
587 });
588 }
589 }
590 _ => {
591 exports.push(ExportBinding {
593 exported_name: "default".to_string(),
594 local_name: "__default__".to_string(),
595 });
596 }
597 }
598 }
599
600 Statement::ExportAllDeclaration(export_all) => {
601 reexports.push(ReexportBinding {
603 exported_name: None,
604 source_name: None,
605 source_path: export_all.source.value.to_string(),
606 });
607 }
608
609 _ => {}
610 }
611 }
612
613 (imports, exports, reexports)
614}
615
616fn get_declaration_names(decl: &Declaration<'_>) -> Vec<String> {
618 match decl {
619 Declaration::VariableDeclaration(var_decl) => var_decl
620 .declarations
621 .iter()
622 .filter_map(|d| d.id.get_binding_identifier().map(|id| id.name.to_string()))
623 .collect(),
624 Declaration::FunctionDeclaration(f) => {
625 f.id.as_ref()
626 .map(|id| vec![id.name.to_string()])
627 .unwrap_or_default()
628 }
629 Declaration::ClassDeclaration(c) => {
630 c.id.as_ref()
631 .map(|id| vec![id.name.to_string()])
632 .unwrap_or_default()
633 }
634 Declaration::TSEnumDeclaration(e) => {
635 vec![e.id.name.to_string()]
636 }
637 _ => Vec::new(),
638 }
639}
640
641fn resolve_import(import_path: &str, parent_dir: &Path) -> Result<PathBuf> {
643 let base = parent_dir.join(import_path);
644
645 if base.exists() {
647 return Ok(base);
648 }
649
650 let with_ts = base.with_extension("ts");
651 if with_ts.exists() {
652 return Ok(with_ts);
653 }
654
655 let with_js = base.with_extension("js");
656 if with_js.exists() {
657 return Ok(with_js);
658 }
659
660 let index_ts = base.join("index.ts");
662 if index_ts.exists() {
663 return Ok(index_ts);
664 }
665
666 let index_js = base.join("index.js");
667 if index_js.exists() {
668 return Ok(index_js);
669 }
670
671 Err(anyhow!(
672 "Cannot resolve import '{}' from {}",
673 import_path,
674 parent_dir.display()
675 ))
676}
677
678pub fn strip_imports_and_exports(source: &str) -> String {
681 let allocator = Allocator::default();
682 let source_type = SourceType::default()
684 .with_module(true)
685 .with_typescript(true);
686
687 let parser_ret = Parser::new(&allocator, source, source_type).parse();
688 if !parser_ret.errors.is_empty() {
689 return source.to_string();
691 }
692
693 let mut program = parser_ret.program;
694
695 strip_module_syntax_ast(&allocator, &mut program);
697
698 let codegen_ret = Codegen::new().build(&program);
700 codegen_ret.code
701}
702
703fn strip_module_syntax_ast<'a>(allocator: &'a Allocator, program: &mut oxc_ast::ast::Program<'a>) {
708 use oxc_allocator::Vec as OxcVec;
709
710 let mut new_body: OxcVec<'a, Statement<'a>> =
712 OxcVec::with_capacity_in(program.body.len(), allocator);
713
714 for stmt in program.body.drain(..) {
715 match stmt {
716 Statement::ImportDeclaration(_) => {
718 }
720
721 Statement::ExportNamedDeclaration(export_decl) => {
723 let inner = export_decl.unbox();
724 if let Some(decl) = inner.declaration {
725 let stmt = declaration_to_statement(decl);
728 new_body.push(stmt);
729 }
730 }
732
733 Statement::ExportDefaultDeclaration(export_default) => {
735 let inner = export_default.unbox();
736 match inner.declaration {
737 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
738 new_body.push(Statement::FunctionDeclaration(func));
739 }
740 ExportDefaultDeclarationKind::ClassDeclaration(class) => {
741 new_body.push(Statement::ClassDeclaration(class));
742 }
743 ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => {
744 }
746 _ => {
747 }
749 }
750 }
751
752 Statement::ExportAllDeclaration(_) => {
754 }
756
757 other => {
759 new_body.push(other);
760 }
761 }
762 }
763
764 program.body = new_body;
765}
766
767fn declaration_to_statement(decl: Declaration<'_>) -> Statement<'_> {
769 match decl {
770 Declaration::VariableDeclaration(d) => Statement::VariableDeclaration(d),
771 Declaration::FunctionDeclaration(d) => Statement::FunctionDeclaration(d),
772 Declaration::ClassDeclaration(d) => Statement::ClassDeclaration(d),
773 Declaration::TSTypeAliasDeclaration(d) => Statement::TSTypeAliasDeclaration(d),
774 Declaration::TSInterfaceDeclaration(d) => Statement::TSInterfaceDeclaration(d),
775 Declaration::TSEnumDeclaration(d) => Statement::TSEnumDeclaration(d),
776 Declaration::TSModuleDeclaration(d) => Statement::TSModuleDeclaration(d),
777 Declaration::TSImportEqualsDeclaration(d) => Statement::TSImportEqualsDeclaration(d),
778 Declaration::TSGlobalDeclaration(d) => Statement::TSGlobalDeclaration(d),
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785
786 #[test]
787 fn test_transpile_basic_typescript() {
788 let source = r#"
789 const x: number = 42;
790 function greet(name: string): string {
791 return `Hello, ${name}!`;
792 }
793 "#;
794
795 let result = transpile_typescript(source, "test.ts").unwrap();
796 assert!(result.contains("const x = 42"));
797 assert!(result.contains("function greet(name)"));
798 assert!(!result.contains(": number"));
799 assert!(!result.contains(": string"));
800 }
801
802 #[test]
803 fn test_transpile_interface() {
804 let source = r#"
805 interface User {
806 name: string;
807 age: number;
808 }
809 const user: User = { name: "Alice", age: 30 };
810 "#;
811
812 let result = transpile_typescript(source, "test.ts").unwrap();
813 assert!(!result.contains("interface"));
814 assert!(result.contains("const user = {"));
815 }
816
817 #[test]
818 fn test_transpile_type_alias() {
819 let source = r#"
820 type ID = number | string;
821 const id: ID = 123;
822 "#;
823
824 let result = transpile_typescript(source, "test.ts").unwrap();
825 assert!(!result.contains("type ID"));
826 assert!(result.contains("const id = 123"));
827 }
828
829 #[test]
830 fn test_has_es_imports() {
831 assert!(has_es_imports("import { foo } from './lib'"));
832 assert!(has_es_imports("import foo from 'bar'"));
833 assert!(!has_es_imports("const x = 1;"));
834 assert!(has_es_imports("// import foo from 'bar'")); }
838
839 #[test]
840 fn test_extract_module_bindings() {
841 let source = r#"
842 import { foo } from "./lib/utils";
843 import bar from "../shared/bar";
844 import external from "external-package";
845 export { PanelManager } from "./panel-manager.ts";
846 export * from "./types.ts";
847 export const API_VERSION = 1;
848 const x = 1;
849 "#;
850
851 let (imports, exports, reexports) = extract_module_bindings(source);
852
853 assert_eq!(imports.len(), 3);
855 assert!(imports
856 .iter()
857 .any(|i| i.source_path == "./lib/utils" && i.local_name == "foo"));
858 assert!(imports
859 .iter()
860 .any(|i| i.source_path == "../shared/bar" && i.local_name == "bar"));
861 assert!(imports.iter().any(|i| i.source_path == "external-package"));
862
863 assert_eq!(exports.len(), 1);
865 assert!(exports.iter().any(|e| e.exported_name == "API_VERSION"));
866
867 assert_eq!(reexports.len(), 2);
869 assert!(reexports
870 .iter()
871 .any(|r| r.source_path == "./panel-manager.ts"));
872 assert!(reexports
873 .iter()
874 .any(|r| r.source_path == "./types.ts" && r.exported_name.is_none()));
875 }
877
878 #[test]
879 fn test_extract_module_bindings_multiline() {
880 let source = r#"
882export type {
883 RGB,
884 Location,
885 PanelOptions,
886} from "./types.ts";
887
888export {
889 Finder,
890 defaultFuzzyFilter,
891} from "./finder.ts";
892
893import {
894 something,
895 somethingElse,
896} from "./multiline-import.ts";
897 "#;
898
899 let (imports, _exports, reexports) = extract_module_bindings(source);
900
901 assert_eq!(imports.len(), 2);
903 assert!(imports.iter().any(|i| i.local_name == "something"));
904 assert!(imports.iter().any(|i| i.local_name == "somethingElse"));
905
906 assert_eq!(reexports.len(), 5); assert!(reexports.iter().any(|r| r.source_path == "./types.ts"));
909 assert!(reexports.iter().any(|r| r.source_path == "./finder.ts"));
910 }
911
912 #[test]
913 fn test_strip_imports_and_exports() {
914 let source = r#"import { foo } from "./lib";
915import bar from "../bar";
916export const API_VERSION = 1;
917export function greet() { return "hi"; }
918export interface User { name: string; }
919const x = foo() + bar();"#;
920
921 let stripped = strip_imports_and_exports(source);
922 assert!(!stripped.contains("import { foo }"));
924 assert!(!stripped.contains("import bar from"));
925 assert!(!stripped.contains("export const"));
927 assert!(!stripped.contains("export function"));
928 assert!(!stripped.contains("export interface"));
929 assert!(stripped.contains("const API_VERSION = 1"));
931 assert!(stripped.contains("function greet()"));
932 assert!(stripped.contains("interface User"));
933 assert!(stripped.contains("const x = foo() + bar();"));
934 }
935
936 #[test]
937 fn test_extract_plugin_dependencies_basic() {
938 let source = r#"
939import type { SomeType } from "fresh:plugin/utility-plugin";
940import { helper } from "fresh:plugin/core-lib";
941const editor = getEditor();
942"#;
943 let deps = extract_plugin_dependencies(source);
944 assert_eq!(deps, vec!["utility-plugin", "core-lib"]);
945 }
946
947 #[test]
948 fn test_extract_plugin_dependencies_various_import_forms() {
949 let source = r#"
950import type { A } from "fresh:plugin/plugin-a";
951import { B } from "fresh:plugin/plugin-b";
952import * as C from "fresh:plugin/plugin-c";
953import D from "fresh:plugin/plugin-d";
954import { E } from './local-file';
955import { F } from "../other-file";
956"#;
957 let deps = extract_plugin_dependencies(source);
958 assert_eq!(deps, vec!["plugin-a", "plugin-b", "plugin-c", "plugin-d"]);
959 }
960
961 #[test]
962 fn test_extract_plugin_dependencies_deduplicates() {
963 let source = r#"
964import type { A } from "fresh:plugin/shared";
965import { B } from "fresh:plugin/shared";
966"#;
967 let deps = extract_plugin_dependencies(source);
968 assert_eq!(deps, vec!["shared"]);
969 }
970
971 #[test]
972 fn test_extract_plugin_dependencies_single_quotes() {
973 let source = r#"
974import type { A } from 'fresh:plugin/single-quoted';
975"#;
976 let deps = extract_plugin_dependencies(source);
977 assert_eq!(deps, vec!["single-quoted"]);
978 }
979
980 #[test]
981 fn test_extract_plugin_dependencies_no_deps() {
982 let source = r#"
983const editor = getEditor();
984import { helper } from "./lib/utils";
985"#;
986 let deps = extract_plugin_dependencies(source);
987 assert!(deps.is_empty());
988 }
989
990 #[test]
991 fn test_topological_sort_no_deps() {
992 let names = vec!["c".to_string(), "a".to_string(), "b".to_string()];
993 let deps = std::collections::HashMap::new();
994 let result = topological_sort_plugins(&names, &deps).unwrap();
995 assert_eq!(result, vec!["a", "b", "c"]);
997 }
998
999 #[test]
1000 fn test_topological_sort_linear_chain() {
1001 let names = vec!["c".to_string(), "b".to_string(), "a".to_string()];
1002 let mut deps = std::collections::HashMap::new();
1003 deps.insert("b".to_string(), vec!["a".to_string()]);
1004 deps.insert("c".to_string(), vec!["b".to_string()]);
1005 let result = topological_sort_plugins(&names, &deps).unwrap();
1006 assert_eq!(result, vec!["a", "b", "c"]);
1007 }
1008
1009 #[test]
1010 fn test_topological_sort_diamond() {
1011 let names = vec![
1013 "d".to_string(),
1014 "c".to_string(),
1015 "b".to_string(),
1016 "a".to_string(),
1017 ];
1018 let mut deps = std::collections::HashMap::new();
1019 deps.insert("b".to_string(), vec!["a".to_string()]);
1020 deps.insert("c".to_string(), vec!["a".to_string()]);
1021 deps.insert("d".to_string(), vec!["b".to_string(), "c".to_string()]);
1022 let result = topological_sort_plugins(&names, &deps).unwrap();
1023 assert_eq!(result, vec!["a", "b", "c", "d"]);
1025 }
1026
1027 #[test]
1028 fn test_topological_sort_cycle_detection() {
1029 let names = vec!["a".to_string(), "b".to_string(), "c".to_string()];
1030 let mut deps = std::collections::HashMap::new();
1031 deps.insert("a".to_string(), vec!["b".to_string()]);
1032 deps.insert("b".to_string(), vec!["c".to_string()]);
1033 deps.insert("c".to_string(), vec!["a".to_string()]);
1034 let result = topological_sort_plugins(&names, &deps);
1035 assert!(result.is_err());
1036 let err = result.unwrap_err().to_string();
1037 assert!(err.contains("cycle"), "Error should mention cycle: {}", err);
1038 }
1039
1040 #[test]
1041 fn test_topological_sort_missing_dependency() {
1042 let names = vec!["a".to_string()];
1043 let mut deps = std::collections::HashMap::new();
1044 deps.insert("a".to_string(), vec!["nonexistent".to_string()]);
1045 let result = topological_sort_plugins(&names, &deps);
1046 assert!(result.is_err());
1047 let err = result.unwrap_err().to_string();
1048 assert!(
1049 err.contains("not installed"),
1050 "Error should mention missing dep: {}",
1051 err
1052 );
1053 }
1054
1055 #[test]
1056 fn test_topological_sort_independent_plugins_alphabetical() {
1057 let names = vec![
1059 "zebra".to_string(),
1060 "alpha".to_string(),
1061 "beta".to_string(),
1062 "gamma".to_string(),
1063 ];
1064 let mut deps = std::collections::HashMap::new();
1065 deps.insert("gamma".to_string(), vec!["alpha".to_string()]);
1066 let result = topological_sort_plugins(&names, &deps).unwrap();
1067 let alpha_pos = result.iter().position(|s| s == "alpha").unwrap();
1069 let gamma_pos = result.iter().position(|s| s == "gamma").unwrap();
1070 assert!(alpha_pos < gamma_pos);
1071 }
1072}