1pub(crate) mod fallbacks;
9mod path_info;
10mod react_native;
11mod specifier;
12mod types;
13
14pub use path_info::{extract_package_name, is_bare_specifier, is_path_alias};
15pub use types::{ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport};
16
17use std::path::{Path, PathBuf};
18
19use rayon::prelude::*;
20use rustc_hash::FxHashMap;
21
22use oxc_span::Span;
23
24use fallow_types::discover::{DiscoveredFile, FileId};
25use fallow_types::extract::{
26 DynamicImportInfo, DynamicImportPattern, ImportInfo, ImportedName, ModuleInfo, ReExportInfo,
27 RequireCallInfo,
28};
29
30use fallbacks::make_glob_from_pattern;
31use specifier::{create_resolver, resolve_specifier};
32use types::ResolveContext;
33
34pub fn resolve_all_imports(
36 modules: &[ModuleInfo],
37 files: &[DiscoveredFile],
38 workspaces: &[fallow_config::WorkspaceInfo],
39 active_plugins: &[String],
40 path_aliases: &[(String, String)],
41 root: &Path,
42) -> Vec<ResolvedModule> {
43 let canonical_ws_roots: Vec<PathBuf> = workspaces
48 .par_iter()
49 .map(|ws| ws.root.canonicalize().unwrap_or_else(|_| ws.root.clone()))
50 .collect();
51 let workspace_roots: FxHashMap<&str, &Path> = workspaces
52 .iter()
53 .zip(canonical_ws_roots.iter())
54 .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
55 .collect();
56
57 let root_is_canonical = root.canonicalize().is_ok_and(|c| c == root);
62
63 let canonical_paths: Vec<PathBuf> = if root_is_canonical {
66 Vec::new()
67 } else {
68 files
69 .par_iter()
70 .map(|f| f.path.canonicalize().unwrap_or_else(|_| f.path.clone()))
71 .collect()
72 };
73
74 let path_to_id: FxHashMap<&Path, FileId> = if root_is_canonical {
77 files.iter().map(|f| (f.path.as_path(), f.id)).collect()
78 } else {
79 canonical_paths
80 .iter()
81 .enumerate()
82 .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
83 .collect()
84 };
85
86 let raw_path_to_id: FxHashMap<&Path, FileId> =
88 files.iter().map(|f| (f.path.as_path(), f.id)).collect();
89
90 let file_paths: Vec<&Path> = files.iter().map(|f| f.path.as_path()).collect();
92
93 let resolver = create_resolver(active_plugins);
95
96 let canonical_fallback = if root_is_canonical {
99 Some(types::CanonicalFallback::new(files))
100 } else {
101 None
102 };
103
104 let ctx = ResolveContext {
106 resolver: &resolver,
107 path_to_id: &path_to_id,
108 raw_path_to_id: &raw_path_to_id,
109 workspace_roots: &workspace_roots,
110 path_aliases,
111 root,
112 canonical_fallback: canonical_fallback.as_ref(),
113 };
114
115 let mut resolved: Vec<ResolvedModule> = modules
120 .par_iter()
121 .filter_map(|module| {
122 let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
123 tracing::warn!(
124 file_id = module.file_id.0,
125 "Skipping module with unknown file_id during resolution"
126 );
127 return None;
128 };
129
130 let mut all_imports = resolve_static_imports(&ctx, file_path, &module.imports);
131 all_imports.extend(resolve_require_imports(
132 &ctx,
133 file_path,
134 &module.require_calls,
135 ));
136
137 let from_dir = if canonical_paths.is_empty() {
138 file_path.parent().unwrap_or(file_path)
140 } else {
141 canonical_paths
142 .get(module.file_id.0 as usize)
143 .and_then(|p| p.parent())
144 .unwrap_or(file_path)
145 };
146
147 Some(ResolvedModule {
148 file_id: module.file_id,
149 path: file_path.to_path_buf(),
150 exports: module.exports.clone(),
151 re_exports: resolve_re_exports(&ctx, file_path, &module.re_exports),
152 resolved_imports: all_imports,
153 resolved_dynamic_imports: resolve_dynamic_imports(
154 &ctx,
155 file_path,
156 &module.dynamic_imports,
157 ),
158 resolved_dynamic_patterns: resolve_dynamic_patterns(
159 from_dir,
160 &module.dynamic_import_patterns,
161 &canonical_paths,
162 files,
163 ),
164 member_accesses: module.member_accesses.clone(),
165 whole_object_uses: module.whole_object_uses.clone(),
166 has_cjs_exports: module.has_cjs_exports,
167 unused_import_bindings: module.unused_import_bindings.clone(),
168 })
169 })
170 .collect();
171
172 apply_specifier_upgrades(&mut resolved);
173
174 resolved
175}
176
177fn resolve_static_imports(
179 ctx: &ResolveContext,
180 file_path: &Path,
181 imports: &[ImportInfo],
182) -> Vec<ResolvedImport> {
183 imports
184 .iter()
185 .map(|imp| ResolvedImport {
186 info: imp.clone(),
187 target: resolve_specifier(ctx, file_path, &imp.source),
188 })
189 .collect()
190}
191
192fn resolve_dynamic_imports(
194 ctx: &ResolveContext,
195 file_path: &Path,
196 dynamic_imports: &[DynamicImportInfo],
197) -> Vec<ResolvedImport> {
198 dynamic_imports
199 .iter()
200 .flat_map(|imp| resolve_single_dynamic_import(ctx, file_path, imp))
201 .collect()
202}
203
204fn resolve_single_dynamic_import(
206 ctx: &ResolveContext,
207 file_path: &Path,
208 imp: &DynamicImportInfo,
209) -> Vec<ResolvedImport> {
210 let target = resolve_specifier(ctx, file_path, &imp.source);
211
212 if !imp.destructured_names.is_empty() {
213 return imp
215 .destructured_names
216 .iter()
217 .map(|name| ResolvedImport {
218 info: ImportInfo {
219 source: imp.source.clone(),
220 imported_name: ImportedName::Named(name.clone()),
221 local_name: name.clone(),
222 is_type_only: false,
223 span: imp.span,
224 source_span: Span::default(),
225 },
226 target: target.clone(),
227 })
228 .collect();
229 }
230
231 if imp.local_name.is_some() {
232 return vec![ResolvedImport {
234 info: ImportInfo {
235 source: imp.source.clone(),
236 imported_name: ImportedName::Namespace,
237 local_name: imp.local_name.clone().unwrap_or_default(),
238 is_type_only: false,
239 span: imp.span,
240 source_span: Span::default(),
241 },
242 target,
243 }];
244 }
245
246 vec![ResolvedImport {
248 info: ImportInfo {
249 source: imp.source.clone(),
250 imported_name: ImportedName::SideEffect,
251 local_name: String::new(),
252 is_type_only: false,
253 span: imp.span,
254 source_span: Span::default(),
255 },
256 target,
257 }]
258}
259
260fn resolve_re_exports(
262 ctx: &ResolveContext,
263 file_path: &Path,
264 re_exports: &[ReExportInfo],
265) -> Vec<ResolvedReExport> {
266 re_exports
267 .iter()
268 .map(|re| ResolvedReExport {
269 info: re.clone(),
270 target: resolve_specifier(ctx, file_path, &re.source),
271 })
272 .collect()
273}
274
275fn resolve_require_imports(
278 ctx: &ResolveContext,
279 file_path: &Path,
280 require_calls: &[RequireCallInfo],
281) -> Vec<ResolvedImport> {
282 require_calls
283 .iter()
284 .flat_map(|req| resolve_single_require(ctx, file_path, req))
285 .collect()
286}
287
288fn resolve_single_require(
290 ctx: &ResolveContext,
291 file_path: &Path,
292 req: &RequireCallInfo,
293) -> Vec<ResolvedImport> {
294 let target = resolve_specifier(ctx, file_path, &req.source);
295
296 if req.destructured_names.is_empty() {
297 return vec![ResolvedImport {
298 info: ImportInfo {
299 source: req.source.clone(),
300 imported_name: ImportedName::Namespace,
301 local_name: req.local_name.clone().unwrap_or_default(),
302 is_type_only: false,
303 span: req.span,
304 source_span: Span::default(),
305 },
306 target,
307 }];
308 }
309
310 req.destructured_names
311 .iter()
312 .map(|name| ResolvedImport {
313 info: ImportInfo {
314 source: req.source.clone(),
315 imported_name: ImportedName::Named(name.clone()),
316 local_name: name.clone(),
317 is_type_only: false,
318 span: req.span,
319 source_span: Span::default(),
320 },
321 target: target.clone(),
322 })
323 .collect()
324}
325
326fn resolve_dynamic_patterns(
330 from_dir: &Path,
331 patterns: &[DynamicImportPattern],
332 canonical_paths: &[PathBuf],
333 files: &[DiscoveredFile],
334) -> Vec<(DynamicImportPattern, Vec<FileId>)> {
335 patterns
336 .iter()
337 .filter_map(|pattern| {
338 let glob_str = make_glob_from_pattern(pattern);
339 let matcher = globset::Glob::new(&glob_str)
340 .ok()
341 .map(|g| g.compile_matcher())?;
342 let matched: Vec<FileId> = if canonical_paths.is_empty() {
343 files
345 .iter()
346 .filter(|f| {
347 f.path.strip_prefix(from_dir).is_ok_and(|relative| {
348 let rel_str = format!("./{}", relative.to_string_lossy());
349 matcher.is_match(&rel_str)
350 })
351 })
352 .map(|f| f.id)
353 .collect()
354 } else {
355 canonical_paths
356 .iter()
357 .enumerate()
358 .filter(|(_idx, canonical)| {
359 canonical.strip_prefix(from_dir).is_ok_and(|relative| {
360 let rel_str = format!("./{}", relative.to_string_lossy());
361 matcher.is_match(&rel_str)
362 })
363 })
364 .map(|(idx, _)| files[idx].id)
365 .collect()
366 };
367 if matched.is_empty() {
368 None
369 } else {
370 Some((pattern.clone(), matched))
371 }
372 })
373 .collect()
374}
375
376fn apply_specifier_upgrades(resolved: &mut [ResolvedModule]) {
392 let mut specifier_upgrades: FxHashMap<String, FileId> = FxHashMap::default();
393 for module in resolved.iter() {
394 for imp in module
395 .resolved_imports
396 .iter()
397 .chain(module.resolved_dynamic_imports.iter())
398 {
399 if is_bare_specifier(&imp.info.source)
400 && let ResolveResult::InternalModule(file_id) = &imp.target
401 {
402 specifier_upgrades
403 .entry(imp.info.source.clone())
404 .or_insert(*file_id);
405 }
406 }
407 for re in &module.re_exports {
408 if is_bare_specifier(&re.info.source)
409 && let ResolveResult::InternalModule(file_id) = &re.target
410 {
411 specifier_upgrades
412 .entry(re.info.source.clone())
413 .or_insert(*file_id);
414 }
415 }
416 }
417
418 if specifier_upgrades.is_empty() {
419 return;
420 }
421
422 for module in resolved.iter_mut() {
424 for imp in module
425 .resolved_imports
426 .iter_mut()
427 .chain(module.resolved_dynamic_imports.iter_mut())
428 {
429 if matches!(imp.target, ResolveResult::NpmPackage(_))
430 && let Some(&file_id) = specifier_upgrades.get(&imp.info.source)
431 {
432 imp.target = ResolveResult::InternalModule(file_id);
433 }
434 }
435 for re in &mut module.re_exports {
436 if matches!(re.target, ResolveResult::NpmPackage(_))
437 && let Some(&file_id) = specifier_upgrades.get(&re.info.source)
438 {
439 re.target = ResolveResult::InternalModule(file_id);
440 }
441 }
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use oxc_span::Span;
449
450 fn dummy_span() -> Span {
455 Span::new(0, 0)
456 }
457
458 #[cfg(not(miri))]
466 fn with_empty_ctx<F: FnOnce(&ResolveContext)>(f: F) {
467 let resolver = specifier::create_resolver(&[]);
468 let path_to_id = FxHashMap::default();
469 let raw_path_to_id = FxHashMap::default();
470 let workspace_roots = FxHashMap::default();
471 let root = PathBuf::from("/project");
472 let ctx = ResolveContext {
473 resolver: &resolver,
474 path_to_id: &path_to_id,
475 raw_path_to_id: &raw_path_to_id,
476 workspace_roots: &workspace_roots,
477 path_aliases: &[],
478 root: &root,
479 canonical_fallback: None,
480 };
481 f(&ctx);
482 }
483
484 #[cfg(miri)]
485 fn with_empty_ctx<F: FnOnce(&ResolveContext)>(_f: F) {
486 }
488
489 fn make_import(source: &str, imported: ImportedName, local: &str) -> ImportInfo {
490 ImportInfo {
491 source: source.to_string(),
492 imported_name: imported,
493 local_name: local.to_string(),
494 is_type_only: false,
495 span: dummy_span(),
496 source_span: Span::default(),
497 }
498 }
499
500 fn make_re_export(source: &str, imported: &str, exported: &str) -> ReExportInfo {
501 ReExportInfo {
502 source: source.to_string(),
503 imported_name: imported.to_string(),
504 exported_name: exported.to_string(),
505 is_type_only: false,
506 }
507 }
508
509 fn make_dynamic(
510 source: &str,
511 destructured: Vec<&str>,
512 local_name: Option<&str>,
513 ) -> DynamicImportInfo {
514 DynamicImportInfo {
515 source: source.to_string(),
516 span: dummy_span(),
517 destructured_names: destructured.into_iter().map(String::from).collect(),
518 local_name: local_name.map(String::from),
519 }
520 }
521
522 fn make_require(
523 source: &str,
524 destructured: Vec<&str>,
525 local_name: Option<&str>,
526 ) -> RequireCallInfo {
527 RequireCallInfo {
528 source: source.to_string(),
529 span: dummy_span(),
530 destructured_names: destructured.into_iter().map(String::from).collect(),
531 local_name: local_name.map(String::from),
532 }
533 }
534
535 fn make_resolved_module(
537 file_id: u32,
538 imports: Vec<ResolvedImport>,
539 dynamic_imports: Vec<ResolvedImport>,
540 re_exports: Vec<ResolvedReExport>,
541 ) -> ResolvedModule {
542 ResolvedModule {
543 file_id: FileId(file_id),
544 path: PathBuf::from(format!("/project/src/file_{file_id}.ts")),
545 exports: vec![],
546 re_exports,
547 resolved_imports: imports,
548 resolved_dynamic_imports: dynamic_imports,
549 resolved_dynamic_patterns: vec![],
550 member_accesses: vec![],
551 whole_object_uses: vec![],
552 has_cjs_exports: false,
553 unused_import_bindings: vec![],
554 }
555 }
556
557 fn make_resolved_import(source: &str, target: ResolveResult) -> ResolvedImport {
558 ResolvedImport {
559 info: make_import(source, ImportedName::Named("x".into()), "x"),
560 target,
561 }
562 }
563
564 fn make_resolved_re_export(source: &str, target: ResolveResult) -> ResolvedReExport {
565 ResolvedReExport {
566 info: make_re_export(source, "x", "x"),
567 target,
568 }
569 }
570
571 #[test]
576 fn static_imports_named() {
577 with_empty_ctx(|ctx| {
578 let imports = vec![make_import(
579 "react",
580 ImportedName::Named("useState".into()),
581 "useState",
582 )];
583 let file = Path::new("/project/src/app.ts");
584 let result = resolve_static_imports(ctx, file, &imports);
585
586 assert_eq!(result.len(), 1);
587 assert_eq!(result[0].info.source, "react");
588 assert!(matches!(
589 result[0].info.imported_name,
590 ImportedName::Named(ref n) if n == "useState"
591 ));
592 });
593 }
594
595 #[test]
596 fn static_imports_default() {
597 with_empty_ctx(|ctx| {
598 let imports = vec![make_import("react", ImportedName::Default, "React")];
599 let file = Path::new("/project/src/app.ts");
600 let result = resolve_static_imports(ctx, file, &imports);
601
602 assert_eq!(result.len(), 1);
603 assert!(matches!(
604 result[0].info.imported_name,
605 ImportedName::Default
606 ));
607 assert_eq!(result[0].info.local_name, "React");
608 });
609 }
610
611 #[test]
612 fn static_imports_namespace() {
613 with_empty_ctx(|ctx| {
614 let imports = vec![make_import("lodash", ImportedName::Namespace, "_")];
615 let file = Path::new("/project/src/utils.ts");
616 let result = resolve_static_imports(ctx, file, &imports);
617
618 assert_eq!(result.len(), 1);
619 assert!(matches!(
620 result[0].info.imported_name,
621 ImportedName::Namespace
622 ));
623 assert_eq!(result[0].info.local_name, "_");
624 });
625 }
626
627 #[test]
628 fn static_imports_side_effect() {
629 with_empty_ctx(|ctx| {
630 let imports = vec![make_import("./styles.css", ImportedName::SideEffect, "")];
631 let file = Path::new("/project/src/app.ts");
632 let result = resolve_static_imports(ctx, file, &imports);
633
634 assert_eq!(result.len(), 1);
635 assert!(matches!(
636 result[0].info.imported_name,
637 ImportedName::SideEffect
638 ));
639 assert_eq!(result[0].info.local_name, "");
640 });
641 }
642
643 #[test]
644 fn static_imports_empty_list() {
645 with_empty_ctx(|ctx| {
646 let file = Path::new("/project/src/app.ts");
647 let result = resolve_static_imports(ctx, file, &[]);
648 assert!(result.is_empty());
649 });
650 }
651
652 #[test]
653 fn static_imports_multiple() {
654 with_empty_ctx(|ctx| {
655 let imports = vec![
656 make_import("react", ImportedName::Default, "React"),
657 make_import("react", ImportedName::Named("useState".into()), "useState"),
658 make_import("lodash", ImportedName::Namespace, "_"),
659 ];
660 let file = Path::new("/project/src/app.ts");
661 let result = resolve_static_imports(ctx, file, &imports);
662
663 assert_eq!(result.len(), 3);
664 assert_eq!(result[0].info.source, "react");
665 assert_eq!(result[1].info.source, "react");
666 assert_eq!(result[2].info.source, "lodash");
667 });
668 }
669
670 #[test]
671 fn static_imports_preserves_type_only() {
672 with_empty_ctx(|ctx| {
673 let imports = vec![ImportInfo {
674 source: "react".into(),
675 imported_name: ImportedName::Named("FC".into()),
676 local_name: "FC".into(),
677 is_type_only: true,
678 span: dummy_span(),
679 source_span: Span::default(),
680 }];
681 let file = Path::new("/project/src/app.ts");
682 let result = resolve_static_imports(ctx, file, &imports);
683
684 assert_eq!(result.len(), 1);
685 assert!(result[0].info.is_type_only);
686 });
687 }
688
689 #[test]
694 fn dynamic_import_with_destructured_names() {
695 with_empty_ctx(|ctx| {
696 let imp = make_dynamic("./utils", vec!["foo", "bar"], None);
697 let file = Path::new("/project/src/app.ts");
698 let result = resolve_single_dynamic_import(ctx, file, &imp);
699
700 assert_eq!(result.len(), 2);
701 assert!(matches!(
702 result[0].info.imported_name,
703 ImportedName::Named(ref n) if n == "foo"
704 ));
705 assert_eq!(result[0].info.local_name, "foo");
706 assert!(matches!(
707 result[1].info.imported_name,
708 ImportedName::Named(ref n) if n == "bar"
709 ));
710 assert_eq!(result[1].info.local_name, "bar");
711 assert_eq!(result[0].info.source, "./utils");
713 assert_eq!(result[1].info.source, "./utils");
714 assert!(!result[0].info.is_type_only);
716 assert!(!result[1].info.is_type_only);
717 });
718 }
719
720 #[test]
721 fn dynamic_import_namespace_with_local_name() {
722 with_empty_ctx(|ctx| {
723 let imp = make_dynamic("./utils", vec![], Some("utils"));
724 let file = Path::new("/project/src/app.ts");
725 let result = resolve_single_dynamic_import(ctx, file, &imp);
726
727 assert_eq!(result.len(), 1);
728 assert!(matches!(
729 result[0].info.imported_name,
730 ImportedName::Namespace
731 ));
732 assert_eq!(result[0].info.local_name, "utils");
733 });
734 }
735
736 #[test]
737 fn dynamic_import_side_effect() {
738 with_empty_ctx(|ctx| {
739 let imp = make_dynamic("./polyfill", vec![], None);
740 let file = Path::new("/project/src/app.ts");
741 let result = resolve_single_dynamic_import(ctx, file, &imp);
742
743 assert_eq!(result.len(), 1);
744 assert!(matches!(
745 result[0].info.imported_name,
746 ImportedName::SideEffect
747 ));
748 assert_eq!(result[0].info.local_name, "");
749 assert_eq!(result[0].info.source, "./polyfill");
750 });
751 }
752
753 #[test]
754 fn dynamic_import_destructured_takes_priority_over_local_name() {
755 with_empty_ctx(|ctx| {
758 let imp = DynamicImportInfo {
759 source: "./mod".into(),
760 span: dummy_span(),
761 destructured_names: vec!["a".into()],
762 local_name: Some("mod".into()),
763 };
764 let file = Path::new("/project/src/app.ts");
765 let result = resolve_single_dynamic_import(ctx, file, &imp);
766
767 assert_eq!(result.len(), 1);
768 assert!(matches!(
769 result[0].info.imported_name,
770 ImportedName::Named(ref n) if n == "a"
771 ));
772 });
773 }
774
775 #[test]
780 fn dynamic_imports_flattens_multiple() {
781 with_empty_ctx(|ctx| {
782 let imports = vec![
783 make_dynamic("./a", vec!["x", "y"], None),
784 make_dynamic("./b", vec![], Some("b")),
785 make_dynamic("./c", vec![], None),
786 ];
787 let file = Path::new("/project/src/app.ts");
788 let result = resolve_dynamic_imports(ctx, file, &imports);
789
790 assert_eq!(result.len(), 4);
792 });
793 }
794
795 #[test]
796 fn dynamic_imports_empty_list() {
797 with_empty_ctx(|ctx| {
798 let file = Path::new("/project/src/app.ts");
799 let result = resolve_dynamic_imports(ctx, file, &[]);
800 assert!(result.is_empty());
801 });
802 }
803
804 #[test]
809 fn re_exports_maps_each_entry() {
810 with_empty_ctx(|ctx| {
811 let re_exports = vec![
812 make_re_export("./utils", "helper", "helper"),
813 make_re_export("./types", "*", "*"),
814 ];
815 let file = Path::new("/project/src/index.ts");
816 let result = resolve_re_exports(ctx, file, &re_exports);
817
818 assert_eq!(result.len(), 2);
819 assert_eq!(result[0].info.source, "./utils");
820 assert_eq!(result[0].info.imported_name, "helper");
821 assert_eq!(result[0].info.exported_name, "helper");
822 assert_eq!(result[1].info.source, "./types");
823 assert_eq!(result[1].info.imported_name, "*");
824 });
825 }
826
827 #[test]
828 fn re_exports_empty_list() {
829 with_empty_ctx(|ctx| {
830 let file = Path::new("/project/src/index.ts");
831 let result = resolve_re_exports(ctx, file, &[]);
832 assert!(result.is_empty());
833 });
834 }
835
836 #[test]
837 fn re_exports_preserves_type_only() {
838 with_empty_ctx(|ctx| {
839 let re_exports = vec![ReExportInfo {
840 source: "./types".into(),
841 imported_name: "MyType".into(),
842 exported_name: "MyType".into(),
843 is_type_only: true,
844 }];
845 let file = Path::new("/project/src/index.ts");
846 let result = resolve_re_exports(ctx, file, &re_exports);
847
848 assert_eq!(result.len(), 1);
849 assert!(result[0].info.is_type_only);
850 });
851 }
852
853 #[test]
858 fn require_namespace_without_destructuring() {
859 with_empty_ctx(|ctx| {
860 let req = make_require("fs", vec![], Some("fs"));
861 let file = Path::new("/project/src/app.js");
862 let result = resolve_single_require(ctx, file, &req);
863
864 assert_eq!(result.len(), 1);
865 assert!(matches!(
866 result[0].info.imported_name,
867 ImportedName::Namespace
868 ));
869 assert_eq!(result[0].info.local_name, "fs");
870 assert_eq!(result[0].info.source, "fs");
871 });
872 }
873
874 #[test]
875 fn require_namespace_without_local_name() {
876 with_empty_ctx(|ctx| {
877 let req = make_require("./side-effect", vec![], None);
878 let file = Path::new("/project/src/app.js");
879 let result = resolve_single_require(ctx, file, &req);
880
881 assert_eq!(result.len(), 1);
882 assert!(matches!(
883 result[0].info.imported_name,
884 ImportedName::Namespace
885 ));
886 assert_eq!(result[0].info.local_name, "");
888 });
889 }
890
891 #[test]
892 fn require_with_destructured_names() {
893 with_empty_ctx(|ctx| {
894 let req = make_require("path", vec!["join", "resolve"], None);
895 let file = Path::new("/project/src/app.js");
896 let result = resolve_single_require(ctx, file, &req);
897
898 assert_eq!(result.len(), 2);
899 assert!(matches!(
900 result[0].info.imported_name,
901 ImportedName::Named(ref n) if n == "join"
902 ));
903 assert_eq!(result[0].info.local_name, "join");
904 assert!(matches!(
905 result[1].info.imported_name,
906 ImportedName::Named(ref n) if n == "resolve"
907 ));
908 assert_eq!(result[1].info.local_name, "resolve");
909 assert_eq!(result[0].info.source, "path");
911 assert_eq!(result[1].info.source, "path");
912 });
913 }
914
915 #[test]
916 fn require_destructured_is_not_type_only() {
917 with_empty_ctx(|ctx| {
918 let req = make_require("path", vec!["join"], None);
919 let file = Path::new("/project/src/app.js");
920 let result = resolve_single_require(ctx, file, &req);
921
922 assert_eq!(result.len(), 1);
923 assert!(!result[0].info.is_type_only);
924 });
925 }
926
927 #[test]
932 fn require_imports_flattens_multiple() {
933 with_empty_ctx(|ctx| {
934 let reqs = vec![
935 make_require("fs", vec![], Some("fs")),
936 make_require("path", vec!["join", "resolve"], None),
937 ];
938 let file = Path::new("/project/src/app.js");
939 let result = resolve_require_imports(ctx, file, &reqs);
940
941 assert_eq!(result.len(), 3);
943 });
944 }
945
946 #[test]
947 fn require_imports_empty_list() {
948 with_empty_ctx(|ctx| {
949 let file = Path::new("/project/src/app.js");
950 let result = resolve_require_imports(ctx, file, &[]);
951 assert!(result.is_empty());
952 });
953 }
954
955 #[test]
960 fn specifier_upgrades_npm_to_internal() {
961 let mut modules = vec![
965 make_resolved_module(
966 0,
967 vec![make_resolved_import(
968 "preact/hooks",
969 ResolveResult::InternalModule(FileId(5)),
970 )],
971 vec![],
972 vec![],
973 ),
974 make_resolved_module(
975 1,
976 vec![make_resolved_import(
977 "preact/hooks",
978 ResolveResult::NpmPackage("preact".into()),
979 )],
980 vec![],
981 vec![],
982 ),
983 ];
984
985 apply_specifier_upgrades(&mut modules);
986
987 assert!(matches!(
988 modules[1].resolved_imports[0].target,
989 ResolveResult::InternalModule(FileId(5))
990 ));
991 }
992
993 #[test]
994 fn specifier_upgrades_noop_when_no_internal() {
995 let mut modules = vec![
997 make_resolved_module(
998 0,
999 vec![make_resolved_import(
1000 "lodash",
1001 ResolveResult::NpmPackage("lodash".into()),
1002 )],
1003 vec![],
1004 vec![],
1005 ),
1006 make_resolved_module(
1007 1,
1008 vec![make_resolved_import(
1009 "lodash",
1010 ResolveResult::NpmPackage("lodash".into()),
1011 )],
1012 vec![],
1013 vec![],
1014 ),
1015 ];
1016
1017 apply_specifier_upgrades(&mut modules);
1018
1019 assert!(matches!(
1020 modules[0].resolved_imports[0].target,
1021 ResolveResult::NpmPackage(_)
1022 ));
1023 assert!(matches!(
1024 modules[1].resolved_imports[0].target,
1025 ResolveResult::NpmPackage(_)
1026 ));
1027 }
1028
1029 #[test]
1030 fn specifier_upgrades_empty_modules() {
1031 let mut modules: Vec<ResolvedModule> = vec![];
1032 apply_specifier_upgrades(&mut modules);
1033 assert!(modules.is_empty());
1034 }
1035
1036 #[test]
1037 fn specifier_upgrades_skips_relative_specifiers() {
1038 let mut modules = vec![
1041 make_resolved_module(
1042 0,
1043 vec![make_resolved_import(
1044 "./utils",
1045 ResolveResult::InternalModule(FileId(5)),
1046 )],
1047 vec![],
1048 vec![],
1049 ),
1050 make_resolved_module(
1051 1,
1052 vec![make_resolved_import(
1053 "./utils",
1054 ResolveResult::NpmPackage("utils".into()),
1055 )],
1056 vec![],
1057 vec![],
1058 ),
1059 ];
1060
1061 apply_specifier_upgrades(&mut modules);
1062
1063 assert!(matches!(
1065 modules[1].resolved_imports[0].target,
1066 ResolveResult::NpmPackage(_)
1067 ));
1068 }
1069
1070 #[test]
1071 fn specifier_upgrades_applies_to_dynamic_imports() {
1072 let mut modules = vec![
1073 make_resolved_module(
1074 0,
1075 vec![],
1076 vec![make_resolved_import(
1077 "preact/hooks",
1078 ResolveResult::InternalModule(FileId(5)),
1079 )],
1080 vec![],
1081 ),
1082 make_resolved_module(
1083 1,
1084 vec![],
1085 vec![make_resolved_import(
1086 "preact/hooks",
1087 ResolveResult::NpmPackage("preact".into()),
1088 )],
1089 vec![],
1090 ),
1091 ];
1092
1093 apply_specifier_upgrades(&mut modules);
1094
1095 assert!(matches!(
1096 modules[1].resolved_dynamic_imports[0].target,
1097 ResolveResult::InternalModule(FileId(5))
1098 ));
1099 }
1100
1101 #[test]
1102 fn specifier_upgrades_applies_to_re_exports() {
1103 let mut modules = vec![
1104 make_resolved_module(
1105 0,
1106 vec![],
1107 vec![],
1108 vec![make_resolved_re_export(
1109 "preact/hooks",
1110 ResolveResult::InternalModule(FileId(5)),
1111 )],
1112 ),
1113 make_resolved_module(
1114 1,
1115 vec![],
1116 vec![],
1117 vec![make_resolved_re_export(
1118 "preact/hooks",
1119 ResolveResult::NpmPackage("preact".into()),
1120 )],
1121 ),
1122 ];
1123
1124 apply_specifier_upgrades(&mut modules);
1125
1126 assert!(matches!(
1127 modules[1].re_exports[0].target,
1128 ResolveResult::InternalModule(FileId(5))
1129 ));
1130 }
1131
1132 #[test]
1133 fn specifier_upgrades_does_not_downgrade_internal() {
1134 let mut modules = vec![
1136 make_resolved_module(
1137 0,
1138 vec![make_resolved_import(
1139 "preact/hooks",
1140 ResolveResult::InternalModule(FileId(5)),
1141 )],
1142 vec![],
1143 vec![],
1144 ),
1145 make_resolved_module(
1146 1,
1147 vec![make_resolved_import(
1148 "preact/hooks",
1149 ResolveResult::InternalModule(FileId(5)),
1150 )],
1151 vec![],
1152 vec![],
1153 ),
1154 ];
1155
1156 apply_specifier_upgrades(&mut modules);
1157
1158 assert!(matches!(
1159 modules[0].resolved_imports[0].target,
1160 ResolveResult::InternalModule(FileId(5))
1161 ));
1162 assert!(matches!(
1163 modules[1].resolved_imports[0].target,
1164 ResolveResult::InternalModule(FileId(5))
1165 ));
1166 }
1167
1168 #[test]
1169 fn specifier_upgrades_first_internal_wins() {
1170 let mut modules = vec![
1173 make_resolved_module(
1174 0,
1175 vec![make_resolved_import(
1176 "shared-lib",
1177 ResolveResult::InternalModule(FileId(10)),
1178 )],
1179 vec![],
1180 vec![],
1181 ),
1182 make_resolved_module(
1183 1,
1184 vec![make_resolved_import(
1185 "shared-lib",
1186 ResolveResult::InternalModule(FileId(20)),
1187 )],
1188 vec![],
1189 vec![],
1190 ),
1191 make_resolved_module(
1192 2,
1193 vec![make_resolved_import(
1194 "shared-lib",
1195 ResolveResult::NpmPackage("shared-lib".into()),
1196 )],
1197 vec![],
1198 vec![],
1199 ),
1200 ];
1201
1202 apply_specifier_upgrades(&mut modules);
1203
1204 assert!(matches!(
1206 modules[2].resolved_imports[0].target,
1207 ResolveResult::InternalModule(FileId(10))
1208 ));
1209 }
1210
1211 #[test]
1212 fn specifier_upgrades_does_not_touch_unresolvable() {
1213 let mut modules = vec![
1216 make_resolved_module(
1217 0,
1218 vec![make_resolved_import(
1219 "my-lib",
1220 ResolveResult::InternalModule(FileId(1)),
1221 )],
1222 vec![],
1223 vec![],
1224 ),
1225 make_resolved_module(
1226 1,
1227 vec![ResolvedImport {
1228 info: make_import("my-lib", ImportedName::Default, "myLib"),
1229 target: ResolveResult::Unresolvable("my-lib".into()),
1230 }],
1231 vec![],
1232 vec![],
1233 ),
1234 ];
1235
1236 apply_specifier_upgrades(&mut modules);
1237
1238 assert!(matches!(
1240 modules[1].resolved_imports[0].target,
1241 ResolveResult::Unresolvable(_)
1242 ));
1243 }
1244
1245 #[test]
1246 fn specifier_upgrades_cross_import_and_re_export() {
1247 let mut modules = vec![
1250 make_resolved_module(
1251 0,
1252 vec![make_resolved_import(
1253 "@myorg/utils",
1254 ResolveResult::InternalModule(FileId(3)),
1255 )],
1256 vec![],
1257 vec![],
1258 ),
1259 make_resolved_module(
1260 1,
1261 vec![],
1262 vec![],
1263 vec![make_resolved_re_export(
1264 "@myorg/utils",
1265 ResolveResult::NpmPackage("@myorg/utils".into()),
1266 )],
1267 ),
1268 ];
1269
1270 apply_specifier_upgrades(&mut modules);
1271
1272 assert!(matches!(
1273 modules[1].re_exports[0].target,
1274 ResolveResult::InternalModule(FileId(3))
1275 ));
1276 }
1277
1278 #[test]
1283 fn dynamic_patterns_matches_files_in_dir() {
1284 let from_dir = Path::new("/project/src");
1285 let patterns = vec![DynamicImportPattern {
1286 prefix: "./locales/".into(),
1287 suffix: Some(".json".into()),
1288 span: dummy_span(),
1289 }];
1290 let canonical_paths = vec![
1291 PathBuf::from("/project/src/locales/en.json"),
1292 PathBuf::from("/project/src/locales/fr.json"),
1293 PathBuf::from("/project/src/utils.ts"),
1294 ];
1295 let files = vec![
1296 DiscoveredFile {
1297 id: FileId(0),
1298 path: PathBuf::from("/project/src/locales/en.json"),
1299 size_bytes: 100,
1300 },
1301 DiscoveredFile {
1302 id: FileId(1),
1303 path: PathBuf::from("/project/src/locales/fr.json"),
1304 size_bytes: 100,
1305 },
1306 DiscoveredFile {
1307 id: FileId(2),
1308 path: PathBuf::from("/project/src/utils.ts"),
1309 size_bytes: 100,
1310 },
1311 ];
1312
1313 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1314
1315 assert_eq!(result.len(), 1);
1316 assert_eq!(result[0].1.len(), 2);
1317 assert!(result[0].1.contains(&FileId(0)));
1318 assert!(result[0].1.contains(&FileId(1)));
1319 }
1320
1321 #[test]
1322 fn dynamic_patterns_no_matches_returns_empty() {
1323 let from_dir = Path::new("/project/src");
1324 let patterns = vec![DynamicImportPattern {
1325 prefix: "./locales/".into(),
1326 suffix: Some(".json".into()),
1327 span: dummy_span(),
1328 }];
1329 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1330 let files = vec![DiscoveredFile {
1331 id: FileId(0),
1332 path: PathBuf::from("/project/src/utils.ts"),
1333 size_bytes: 100,
1334 }];
1335
1336 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1337
1338 assert!(result.is_empty());
1339 }
1340
1341 #[test]
1342 fn dynamic_patterns_empty_patterns_list() {
1343 let from_dir = Path::new("/project/src");
1344 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1345 let files = vec![DiscoveredFile {
1346 id: FileId(0),
1347 path: PathBuf::from("/project/src/utils.ts"),
1348 size_bytes: 100,
1349 }];
1350
1351 let result = resolve_dynamic_patterns(from_dir, &[], &canonical_paths, &files);
1352 assert!(result.is_empty());
1353 }
1354
1355 #[test]
1356 fn dynamic_patterns_glob_prefix_passthrough() {
1357 let from_dir = Path::new("/project/src");
1358 let patterns = vec![DynamicImportPattern {
1359 prefix: "./**/*.ts".into(),
1360 suffix: None,
1361 span: dummy_span(),
1362 }];
1363 let canonical_paths = vec![
1364 PathBuf::from("/project/src/utils.ts"),
1365 PathBuf::from("/project/src/deep/nested.ts"),
1366 ];
1367 let files = vec![
1368 DiscoveredFile {
1369 id: FileId(0),
1370 path: PathBuf::from("/project/src/utils.ts"),
1371 size_bytes: 100,
1372 },
1373 DiscoveredFile {
1374 id: FileId(1),
1375 path: PathBuf::from("/project/src/deep/nested.ts"),
1376 size_bytes: 100,
1377 },
1378 ];
1379
1380 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1381
1382 assert_eq!(result.len(), 1);
1383 assert_eq!(result[0].1.len(), 2);
1384 }
1385
1386 #[test]
1391 fn static_import_unresolvable_relative_path() {
1392 with_empty_ctx(|ctx| {
1393 let imports = vec![make_import(
1394 "./nonexistent",
1395 ImportedName::Default,
1396 "missing",
1397 )];
1398 let file = Path::new("/project/src/app.ts");
1399 let result = resolve_static_imports(ctx, file, &imports);
1400
1401 assert_eq!(result.len(), 1);
1402 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1403 });
1404 }
1405
1406 #[test]
1407 fn static_import_bare_specifier_becomes_npm_package() {
1408 with_empty_ctx(|ctx| {
1409 let imports = vec![make_import("react", ImportedName::Default, "React")];
1410 let file = Path::new("/project/src/app.ts");
1411 let result = resolve_static_imports(ctx, file, &imports);
1412
1413 assert_eq!(result.len(), 1);
1414 assert!(matches!(
1415 result[0].target,
1416 ResolveResult::NpmPackage(ref pkg) if pkg == "react"
1417 ));
1418 });
1419 }
1420
1421 #[test]
1422 fn require_bare_specifier_becomes_npm_package() {
1423 with_empty_ctx(|ctx| {
1424 let req = make_require("express", vec![], Some("express"));
1425 let file = Path::new("/project/src/app.js");
1426 let result = resolve_single_require(ctx, file, &req);
1427
1428 assert_eq!(result.len(), 1);
1429 assert!(matches!(
1430 result[0].target,
1431 ResolveResult::NpmPackage(ref pkg) if pkg == "express"
1432 ));
1433 });
1434 }
1435
1436 #[test]
1437 fn dynamic_import_unresolvable() {
1438 with_empty_ctx(|ctx| {
1439 let imp = make_dynamic("./missing-module", vec![], None);
1440 let file = Path::new("/project/src/app.ts");
1441 let result = resolve_single_dynamic_import(ctx, file, &imp);
1442
1443 assert_eq!(result.len(), 1);
1444 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1445 });
1446 }
1447
1448 #[test]
1449 fn re_export_unresolvable() {
1450 with_empty_ctx(|ctx| {
1451 let re_exports = vec![make_re_export("./missing", "foo", "foo")];
1452 let file = Path::new("/project/src/index.ts");
1453 let result = resolve_re_exports(ctx, file, &re_exports);
1454
1455 assert_eq!(result.len(), 1);
1456 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1457 });
1458 }
1459
1460 #[test]
1465 fn dynamic_patterns_template_literal_prefix_suffix() {
1466 let from_dir = Path::new("/project/src");
1468 let patterns = vec![DynamicImportPattern {
1469 prefix: "./locales/".into(),
1470 suffix: Some(".json".into()),
1471 span: dummy_span(),
1472 }];
1473 let canonical_paths = vec![
1474 PathBuf::from("/project/src/locales/en.json"),
1475 PathBuf::from("/project/src/locales/de.json"),
1476 PathBuf::from("/project/src/locales/README.md"),
1477 PathBuf::from("/project/src/config.ts"),
1478 ];
1479 let files = vec![
1480 DiscoveredFile {
1481 id: FileId(0),
1482 path: PathBuf::from("/project/src/locales/en.json"),
1483 size_bytes: 100,
1484 },
1485 DiscoveredFile {
1486 id: FileId(1),
1487 path: PathBuf::from("/project/src/locales/de.json"),
1488 size_bytes: 100,
1489 },
1490 DiscoveredFile {
1491 id: FileId(2),
1492 path: PathBuf::from("/project/src/locales/README.md"),
1493 size_bytes: 100,
1494 },
1495 DiscoveredFile {
1496 id: FileId(3),
1497 path: PathBuf::from("/project/src/config.ts"),
1498 size_bytes: 100,
1499 },
1500 ];
1501
1502 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1503
1504 assert_eq!(result.len(), 1, "should produce exactly one pattern match");
1505 let matched_ids = &result[0].1;
1506 assert_eq!(
1507 matched_ids.len(),
1508 2,
1509 "should match en.json and de.json only"
1510 );
1511 assert!(matched_ids.contains(&FileId(0)));
1512 assert!(matched_ids.contains(&FileId(1)));
1513 assert!(
1514 !matched_ids.contains(&FileId(2)),
1515 "README.md should not match .json suffix"
1516 );
1517 assert!(
1518 !matched_ids.contains(&FileId(3)),
1519 "config.ts should not match locales/ prefix"
1520 );
1521 }
1522
1523 #[test]
1524 fn dynamic_patterns_string_concat_prefix_only() {
1525 let from_dir = Path::new("/project/src");
1527 let patterns = vec![DynamicImportPattern {
1528 prefix: "./pages/".into(),
1529 suffix: None,
1530 span: dummy_span(),
1531 }];
1532 let canonical_paths = vec![
1533 PathBuf::from("/project/src/pages/home.ts"),
1534 PathBuf::from("/project/src/pages/about.ts"),
1535 PathBuf::from("/project/src/pages/nested/deep.ts"),
1536 PathBuf::from("/project/src/utils.ts"),
1537 ];
1538 let files = vec![
1539 DiscoveredFile {
1540 id: FileId(0),
1541 path: PathBuf::from("/project/src/pages/home.ts"),
1542 size_bytes: 100,
1543 },
1544 DiscoveredFile {
1545 id: FileId(1),
1546 path: PathBuf::from("/project/src/pages/about.ts"),
1547 size_bytes: 100,
1548 },
1549 DiscoveredFile {
1550 id: FileId(2),
1551 path: PathBuf::from("/project/src/pages/nested/deep.ts"),
1552 size_bytes: 100,
1553 },
1554 DiscoveredFile {
1555 id: FileId(3),
1556 path: PathBuf::from("/project/src/utils.ts"),
1557 size_bytes: 100,
1558 },
1559 ];
1560
1561 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1562
1563 assert_eq!(result.len(), 1);
1564 let matched_ids = &result[0].1;
1565 assert!(matched_ids.contains(&FileId(0)), "home.ts should match");
1567 assert!(matched_ids.contains(&FileId(1)), "about.ts should match");
1568 assert!(
1569 !matched_ids.contains(&FileId(3)),
1570 "utils.ts should not match pages/ prefix"
1571 );
1572 }
1573
1574 #[test]
1575 fn dynamic_patterns_import_meta_glob_recursive() {
1576 let from_dir = Path::new("/project/src");
1578 let patterns = vec![DynamicImportPattern {
1579 prefix: "./components/**/*.ts".into(),
1580 suffix: None,
1581 span: dummy_span(),
1582 }];
1583 let canonical_paths = vec![
1584 PathBuf::from("/project/src/components/Button.ts"),
1585 PathBuf::from("/project/src/components/forms/Input.ts"),
1586 PathBuf::from("/project/src/components/Button.css"),
1587 PathBuf::from("/project/src/utils.ts"),
1588 ];
1589 let files = vec![
1590 DiscoveredFile {
1591 id: FileId(0),
1592 path: PathBuf::from("/project/src/components/Button.ts"),
1593 size_bytes: 100,
1594 },
1595 DiscoveredFile {
1596 id: FileId(1),
1597 path: PathBuf::from("/project/src/components/forms/Input.ts"),
1598 size_bytes: 100,
1599 },
1600 DiscoveredFile {
1601 id: FileId(2),
1602 path: PathBuf::from("/project/src/components/Button.css"),
1603 size_bytes: 100,
1604 },
1605 DiscoveredFile {
1606 id: FileId(3),
1607 path: PathBuf::from("/project/src/utils.ts"),
1608 size_bytes: 100,
1609 },
1610 ];
1611
1612 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1613
1614 assert_eq!(result.len(), 1);
1615 let matched_ids = &result[0].1;
1616 assert!(
1617 matched_ids.contains(&FileId(0)),
1618 "Button.ts should match **/*.ts"
1619 );
1620 assert!(
1621 matched_ids.contains(&FileId(1)),
1622 "forms/Input.ts should match **/*.ts recursively"
1623 );
1624 assert!(
1625 !matched_ids.contains(&FileId(2)),
1626 "Button.css should not match *.ts pattern"
1627 );
1628 assert!(
1629 !matched_ids.contains(&FileId(3)),
1630 "utils.ts outside components/ should not match"
1631 );
1632 }
1633
1634 #[test]
1635 fn dynamic_patterns_import_meta_glob_brace_expansion() {
1636 let from_dir = Path::new("/project/src");
1638 let patterns = vec![DynamicImportPattern {
1639 prefix: "./routes/**/*.{ts,tsx}".into(),
1640 suffix: None,
1641 span: dummy_span(),
1642 }];
1643 let canonical_paths = vec![
1644 PathBuf::from("/project/src/routes/home.ts"),
1645 PathBuf::from("/project/src/routes/about.tsx"),
1646 PathBuf::from("/project/src/routes/layout.css"),
1647 ];
1648 let files = vec![
1649 DiscoveredFile {
1650 id: FileId(0),
1651 path: PathBuf::from("/project/src/routes/home.ts"),
1652 size_bytes: 100,
1653 },
1654 DiscoveredFile {
1655 id: FileId(1),
1656 path: PathBuf::from("/project/src/routes/about.tsx"),
1657 size_bytes: 100,
1658 },
1659 DiscoveredFile {
1660 id: FileId(2),
1661 path: PathBuf::from("/project/src/routes/layout.css"),
1662 size_bytes: 100,
1663 },
1664 ];
1665
1666 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1667
1668 assert_eq!(result.len(), 1);
1669 let matched_ids = &result[0].1;
1670 assert!(matched_ids.contains(&FileId(0)), "home.ts should match");
1671 assert!(matched_ids.contains(&FileId(1)), "about.tsx should match");
1672 assert!(
1673 !matched_ids.contains(&FileId(2)),
1674 "layout.css should not match ts/tsx brace expansion"
1675 );
1676 }
1677
1678 #[test]
1679 fn dynamic_patterns_no_static_part_matches_everything() {
1680 let from_dir = Path::new("/project/src");
1685 let patterns = vec![DynamicImportPattern {
1686 prefix: String::new(),
1687 suffix: None,
1688 span: dummy_span(),
1689 }];
1690 let canonical_paths = vec![
1691 PathBuf::from("/project/src/a.ts"),
1692 PathBuf::from("/project/src/b.ts"),
1693 ];
1694 let files = vec![
1695 DiscoveredFile {
1696 id: FileId(0),
1697 path: PathBuf::from("/project/src/a.ts"),
1698 size_bytes: 100,
1699 },
1700 DiscoveredFile {
1701 id: FileId(1),
1702 path: PathBuf::from("/project/src/b.ts"),
1703 size_bytes: 100,
1704 },
1705 ];
1706
1707 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1708
1709 assert_eq!(result.len(), 1);
1711 assert_eq!(result[0].1.len(), 2);
1712 }
1713
1714 #[test]
1715 fn dynamic_patterns_multiple_patterns_independent() {
1716 let from_dir = Path::new("/project/src");
1718 let patterns = vec![
1719 DynamicImportPattern {
1720 prefix: "./locales/".into(),
1721 suffix: Some(".json".into()),
1722 span: dummy_span(),
1723 },
1724 DynamicImportPattern {
1725 prefix: "./pages/".into(),
1726 suffix: Some(".ts".into()),
1727 span: dummy_span(),
1728 },
1729 ];
1730 let canonical_paths = vec![
1731 PathBuf::from("/project/src/locales/en.json"),
1732 PathBuf::from("/project/src/pages/home.ts"),
1733 PathBuf::from("/project/src/utils.ts"),
1734 ];
1735 let files = vec![
1736 DiscoveredFile {
1737 id: FileId(0),
1738 path: PathBuf::from("/project/src/locales/en.json"),
1739 size_bytes: 100,
1740 },
1741 DiscoveredFile {
1742 id: FileId(1),
1743 path: PathBuf::from("/project/src/pages/home.ts"),
1744 size_bytes: 100,
1745 },
1746 DiscoveredFile {
1747 id: FileId(2),
1748 path: PathBuf::from("/project/src/utils.ts"),
1749 size_bytes: 100,
1750 },
1751 ];
1752
1753 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1754
1755 assert_eq!(result.len(), 2, "both patterns should produce matches");
1756 assert!(result[0].1.contains(&FileId(0)));
1758 assert_eq!(result[0].1.len(), 1);
1759 assert!(result[1].1.contains(&FileId(1)));
1761 assert_eq!(result[1].1.len(), 1);
1762 }
1763
1764 #[test]
1765 fn dynamic_patterns_files_outside_from_dir_not_matched() {
1766 let from_dir = Path::new("/project/src");
1768 let patterns = vec![DynamicImportPattern {
1769 prefix: "./utils/".into(),
1770 suffix: None,
1771 span: dummy_span(),
1772 }];
1773 let canonical_paths = vec![
1774 PathBuf::from("/project/other/utils/helper.ts"),
1775 PathBuf::from("/project/src/utils/helper.ts"),
1776 ];
1777 let files = vec![
1778 DiscoveredFile {
1779 id: FileId(0),
1780 path: PathBuf::from("/project/other/utils/helper.ts"),
1781 size_bytes: 100,
1782 },
1783 DiscoveredFile {
1784 id: FileId(1),
1785 path: PathBuf::from("/project/src/utils/helper.ts"),
1786 size_bytes: 100,
1787 },
1788 ];
1789
1790 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1791
1792 assert_eq!(result.len(), 1);
1793 let matched_ids = &result[0].1;
1794 assert!(
1795 !matched_ids.contains(&FileId(0)),
1796 "file outside from_dir should not match"
1797 );
1798 assert!(
1799 matched_ids.contains(&FileId(1)),
1800 "file inside from_dir should match"
1801 );
1802 }
1803
1804 #[test]
1809 fn dynamic_patterns_raw_paths_when_canonical_empty() {
1810 let from_dir = Path::new("/project/src");
1812 let patterns = vec![DynamicImportPattern {
1813 prefix: "./locales/".into(),
1814 suffix: Some(".json".into()),
1815 span: dummy_span(),
1816 }];
1817 let canonical_paths: Vec<PathBuf> = vec![]; let files = vec![
1819 DiscoveredFile {
1820 id: FileId(0),
1821 path: PathBuf::from("/project/src/locales/en.json"),
1822 size_bytes: 100,
1823 },
1824 DiscoveredFile {
1825 id: FileId(1),
1826 path: PathBuf::from("/project/src/locales/fr.json"),
1827 size_bytes: 100,
1828 },
1829 DiscoveredFile {
1830 id: FileId(2),
1831 path: PathBuf::from("/project/src/main.ts"),
1832 size_bytes: 100,
1833 },
1834 ];
1835
1836 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1837
1838 assert_eq!(result.len(), 1);
1839 assert_eq!(result[0].1.len(), 2);
1840 assert!(result[0].1.contains(&FileId(0)));
1841 assert!(result[0].1.contains(&FileId(1)));
1842 }
1843
1844 #[test]
1845 fn dynamic_patterns_raw_paths_no_match() {
1846 let from_dir = Path::new("/project/src");
1847 let patterns = vec![DynamicImportPattern {
1848 prefix: "./missing-dir/".into(),
1849 suffix: None,
1850 span: dummy_span(),
1851 }];
1852 let canonical_paths: Vec<PathBuf> = vec![];
1853 let files = vec![DiscoveredFile {
1854 id: FileId(0),
1855 path: PathBuf::from("/project/src/utils.ts"),
1856 size_bytes: 100,
1857 }];
1858
1859 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1860
1861 assert!(
1862 result.is_empty(),
1863 "no files match the pattern, should return empty"
1864 );
1865 }
1866
1867 #[test]
1872 fn dynamic_import_destructured_all_share_same_target() {
1873 with_empty_ctx(|ctx| {
1874 let imp = make_dynamic("react", vec!["useState", "useEffect", "useRef"], None);
1875 let file = Path::new("/project/src/app.ts");
1876 let result = resolve_single_dynamic_import(ctx, file, &imp);
1877
1878 assert_eq!(result.len(), 3);
1879 for resolved in &result {
1881 assert_eq!(resolved.info.source, "react");
1882 assert!(!resolved.info.is_type_only);
1883 assert!(matches!(
1884 resolved.info.imported_name,
1885 ImportedName::Named(_)
1886 ));
1887 }
1888 let names: Vec<&str> = result
1890 .iter()
1891 .filter_map(|r| match &r.info.imported_name {
1892 ImportedName::Named(n) => Some(n.as_str()),
1893 _ => None,
1894 })
1895 .collect();
1896 assert_eq!(names, vec!["useState", "useEffect", "useRef"]);
1897 });
1898 }
1899
1900 #[test]
1901 fn dynamic_import_empty_destructured_with_no_local_is_side_effect() {
1902 with_empty_ctx(|ctx| {
1904 let imp = make_dynamic("./setup", vec![], None);
1905 let file = Path::new("/project/src/main.ts");
1906 let result = resolve_single_dynamic_import(ctx, file, &imp);
1907
1908 assert_eq!(result.len(), 1);
1909 assert!(matches!(
1910 result[0].info.imported_name,
1911 ImportedName::SideEffect
1912 ));
1913 assert_eq!(result[0].info.local_name, "");
1914 });
1915 }
1916
1917 #[test]
1918 fn dynamic_import_bare_specifier_becomes_npm_package() {
1919 with_empty_ctx(|ctx| {
1920 let imp = make_dynamic("lodash", vec![], Some("_"));
1921 let file = Path::new("/project/src/app.ts");
1922 let result = resolve_single_dynamic_import(ctx, file, &imp);
1923
1924 assert_eq!(result.len(), 1);
1925 assert!(matches!(
1926 result[0].target,
1927 ResolveResult::NpmPackage(ref pkg) if pkg == "lodash"
1928 ));
1929 assert!(matches!(
1930 result[0].info.imported_name,
1931 ImportedName::Namespace
1932 ));
1933 });
1934 }
1935
1936 #[test]
1941 fn dynamic_patterns_invalid_glob_skipped() {
1942 let from_dir = Path::new("/project/src");
1944 let patterns = vec![DynamicImportPattern {
1945 prefix: "./[invalid".into(), suffix: None,
1947 span: dummy_span(),
1948 }];
1949 let canonical_paths = vec![PathBuf::from("/project/src/test.ts")];
1950 let files = vec![DiscoveredFile {
1951 id: FileId(0),
1952 path: PathBuf::from("/project/src/test.ts"),
1953 size_bytes: 100,
1954 }];
1955
1956 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1957
1958 assert!(
1960 result.is_empty(),
1961 "invalid glob pattern should be skipped gracefully"
1962 );
1963 }
1964}