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.iter().cloned().collect(),
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 rustc_hash::FxHashSet;
448
449 use super::*;
450 use oxc_span::Span;
451
452 fn dummy_span() -> Span {
457 Span::new(0, 0)
458 }
459
460 #[cfg(not(miri))]
468 fn with_empty_ctx<F: FnOnce(&ResolveContext)>(f: F) {
469 let resolver = specifier::create_resolver(&[]);
470 let path_to_id = FxHashMap::default();
471 let raw_path_to_id = FxHashMap::default();
472 let workspace_roots = FxHashMap::default();
473 let root = PathBuf::from("/project");
474 let ctx = ResolveContext {
475 resolver: &resolver,
476 path_to_id: &path_to_id,
477 raw_path_to_id: &raw_path_to_id,
478 workspace_roots: &workspace_roots,
479 path_aliases: &[],
480 root: &root,
481 canonical_fallback: None,
482 };
483 f(&ctx);
484 }
485
486 #[cfg(miri)]
487 fn with_empty_ctx<F: FnOnce(&ResolveContext)>(_f: F) {
488 }
490
491 fn make_import(source: &str, imported: ImportedName, local: &str) -> ImportInfo {
492 ImportInfo {
493 source: source.to_string(),
494 imported_name: imported,
495 local_name: local.to_string(),
496 is_type_only: false,
497 span: dummy_span(),
498 source_span: Span::default(),
499 }
500 }
501
502 fn make_re_export(source: &str, imported: &str, exported: &str) -> ReExportInfo {
503 ReExportInfo {
504 source: source.to_string(),
505 imported_name: imported.to_string(),
506 exported_name: exported.to_string(),
507 is_type_only: false,
508 }
509 }
510
511 fn make_dynamic(
512 source: &str,
513 destructured: Vec<&str>,
514 local_name: Option<&str>,
515 ) -> DynamicImportInfo {
516 DynamicImportInfo {
517 source: source.to_string(),
518 span: dummy_span(),
519 destructured_names: destructured.into_iter().map(String::from).collect(),
520 local_name: local_name.map(String::from),
521 }
522 }
523
524 fn make_require(
525 source: &str,
526 destructured: Vec<&str>,
527 local_name: Option<&str>,
528 ) -> RequireCallInfo {
529 RequireCallInfo {
530 source: source.to_string(),
531 span: dummy_span(),
532 destructured_names: destructured.into_iter().map(String::from).collect(),
533 local_name: local_name.map(String::from),
534 }
535 }
536
537 fn make_resolved_module(
539 file_id: u32,
540 imports: Vec<ResolvedImport>,
541 dynamic_imports: Vec<ResolvedImport>,
542 re_exports: Vec<ResolvedReExport>,
543 ) -> ResolvedModule {
544 ResolvedModule {
545 file_id: FileId(file_id),
546 path: PathBuf::from(format!("/project/src/file_{file_id}.ts")),
547 exports: vec![],
548 re_exports,
549 resolved_imports: imports,
550 resolved_dynamic_imports: dynamic_imports,
551 resolved_dynamic_patterns: vec![],
552 member_accesses: vec![],
553 whole_object_uses: vec![],
554 has_cjs_exports: false,
555 unused_import_bindings: FxHashSet::default(),
556 }
557 }
558
559 fn make_resolved_import(source: &str, target: ResolveResult) -> ResolvedImport {
560 ResolvedImport {
561 info: make_import(source, ImportedName::Named("x".into()), "x"),
562 target,
563 }
564 }
565
566 fn make_resolved_re_export(source: &str, target: ResolveResult) -> ResolvedReExport {
567 ResolvedReExport {
568 info: make_re_export(source, "x", "x"),
569 target,
570 }
571 }
572
573 #[test]
578 fn static_imports_named() {
579 with_empty_ctx(|ctx| {
580 let imports = vec![make_import(
581 "react",
582 ImportedName::Named("useState".into()),
583 "useState",
584 )];
585 let file = Path::new("/project/src/app.ts");
586 let result = resolve_static_imports(ctx, file, &imports);
587
588 assert_eq!(result.len(), 1);
589 assert_eq!(result[0].info.source, "react");
590 assert!(matches!(
591 result[0].info.imported_name,
592 ImportedName::Named(ref n) if n == "useState"
593 ));
594 });
595 }
596
597 #[test]
598 fn static_imports_default() {
599 with_empty_ctx(|ctx| {
600 let imports = vec![make_import("react", ImportedName::Default, "React")];
601 let file = Path::new("/project/src/app.ts");
602 let result = resolve_static_imports(ctx, file, &imports);
603
604 assert_eq!(result.len(), 1);
605 assert!(matches!(
606 result[0].info.imported_name,
607 ImportedName::Default
608 ));
609 assert_eq!(result[0].info.local_name, "React");
610 });
611 }
612
613 #[test]
614 fn static_imports_namespace() {
615 with_empty_ctx(|ctx| {
616 let imports = vec![make_import("lodash", ImportedName::Namespace, "_")];
617 let file = Path::new("/project/src/utils.ts");
618 let result = resolve_static_imports(ctx, file, &imports);
619
620 assert_eq!(result.len(), 1);
621 assert!(matches!(
622 result[0].info.imported_name,
623 ImportedName::Namespace
624 ));
625 assert_eq!(result[0].info.local_name, "_");
626 });
627 }
628
629 #[test]
630 fn static_imports_side_effect() {
631 with_empty_ctx(|ctx| {
632 let imports = vec![make_import("./styles.css", ImportedName::SideEffect, "")];
633 let file = Path::new("/project/src/app.ts");
634 let result = resolve_static_imports(ctx, file, &imports);
635
636 assert_eq!(result.len(), 1);
637 assert!(matches!(
638 result[0].info.imported_name,
639 ImportedName::SideEffect
640 ));
641 assert_eq!(result[0].info.local_name, "");
642 });
643 }
644
645 #[test]
646 fn static_imports_empty_list() {
647 with_empty_ctx(|ctx| {
648 let file = Path::new("/project/src/app.ts");
649 let result = resolve_static_imports(ctx, file, &[]);
650 assert!(result.is_empty());
651 });
652 }
653
654 #[test]
655 fn static_imports_multiple() {
656 with_empty_ctx(|ctx| {
657 let imports = vec![
658 make_import("react", ImportedName::Default, "React"),
659 make_import("react", ImportedName::Named("useState".into()), "useState"),
660 make_import("lodash", ImportedName::Namespace, "_"),
661 ];
662 let file = Path::new("/project/src/app.ts");
663 let result = resolve_static_imports(ctx, file, &imports);
664
665 assert_eq!(result.len(), 3);
666 assert_eq!(result[0].info.source, "react");
667 assert_eq!(result[1].info.source, "react");
668 assert_eq!(result[2].info.source, "lodash");
669 });
670 }
671
672 #[test]
673 fn static_imports_preserves_type_only() {
674 with_empty_ctx(|ctx| {
675 let imports = vec![ImportInfo {
676 source: "react".into(),
677 imported_name: ImportedName::Named("FC".into()),
678 local_name: "FC".into(),
679 is_type_only: true,
680 span: dummy_span(),
681 source_span: Span::default(),
682 }];
683 let file = Path::new("/project/src/app.ts");
684 let result = resolve_static_imports(ctx, file, &imports);
685
686 assert_eq!(result.len(), 1);
687 assert!(result[0].info.is_type_only);
688 });
689 }
690
691 #[test]
696 fn dynamic_import_with_destructured_names() {
697 with_empty_ctx(|ctx| {
698 let imp = make_dynamic("./utils", vec!["foo", "bar"], None);
699 let file = Path::new("/project/src/app.ts");
700 let result = resolve_single_dynamic_import(ctx, file, &imp);
701
702 assert_eq!(result.len(), 2);
703 assert!(matches!(
704 result[0].info.imported_name,
705 ImportedName::Named(ref n) if n == "foo"
706 ));
707 assert_eq!(result[0].info.local_name, "foo");
708 assert!(matches!(
709 result[1].info.imported_name,
710 ImportedName::Named(ref n) if n == "bar"
711 ));
712 assert_eq!(result[1].info.local_name, "bar");
713 assert_eq!(result[0].info.source, "./utils");
715 assert_eq!(result[1].info.source, "./utils");
716 assert!(!result[0].info.is_type_only);
718 assert!(!result[1].info.is_type_only);
719 });
720 }
721
722 #[test]
723 fn dynamic_import_namespace_with_local_name() {
724 with_empty_ctx(|ctx| {
725 let imp = make_dynamic("./utils", vec![], Some("utils"));
726 let file = Path::new("/project/src/app.ts");
727 let result = resolve_single_dynamic_import(ctx, file, &imp);
728
729 assert_eq!(result.len(), 1);
730 assert!(matches!(
731 result[0].info.imported_name,
732 ImportedName::Namespace
733 ));
734 assert_eq!(result[0].info.local_name, "utils");
735 });
736 }
737
738 #[test]
739 fn dynamic_import_side_effect() {
740 with_empty_ctx(|ctx| {
741 let imp = make_dynamic("./polyfill", vec![], None);
742 let file = Path::new("/project/src/app.ts");
743 let result = resolve_single_dynamic_import(ctx, file, &imp);
744
745 assert_eq!(result.len(), 1);
746 assert!(matches!(
747 result[0].info.imported_name,
748 ImportedName::SideEffect
749 ));
750 assert_eq!(result[0].info.local_name, "");
751 assert_eq!(result[0].info.source, "./polyfill");
752 });
753 }
754
755 #[test]
756 fn dynamic_import_destructured_takes_priority_over_local_name() {
757 with_empty_ctx(|ctx| {
760 let imp = DynamicImportInfo {
761 source: "./mod".into(),
762 span: dummy_span(),
763 destructured_names: vec!["a".into()],
764 local_name: Some("mod".into()),
765 };
766 let file = Path::new("/project/src/app.ts");
767 let result = resolve_single_dynamic_import(ctx, file, &imp);
768
769 assert_eq!(result.len(), 1);
770 assert!(matches!(
771 result[0].info.imported_name,
772 ImportedName::Named(ref n) if n == "a"
773 ));
774 });
775 }
776
777 #[test]
782 fn dynamic_imports_flattens_multiple() {
783 with_empty_ctx(|ctx| {
784 let imports = vec![
785 make_dynamic("./a", vec!["x", "y"], None),
786 make_dynamic("./b", vec![], Some("b")),
787 make_dynamic("./c", vec![], None),
788 ];
789 let file = Path::new("/project/src/app.ts");
790 let result = resolve_dynamic_imports(ctx, file, &imports);
791
792 assert_eq!(result.len(), 4);
794 });
795 }
796
797 #[test]
798 fn dynamic_imports_empty_list() {
799 with_empty_ctx(|ctx| {
800 let file = Path::new("/project/src/app.ts");
801 let result = resolve_dynamic_imports(ctx, file, &[]);
802 assert!(result.is_empty());
803 });
804 }
805
806 #[test]
811 fn re_exports_maps_each_entry() {
812 with_empty_ctx(|ctx| {
813 let re_exports = vec![
814 make_re_export("./utils", "helper", "helper"),
815 make_re_export("./types", "*", "*"),
816 ];
817 let file = Path::new("/project/src/index.ts");
818 let result = resolve_re_exports(ctx, file, &re_exports);
819
820 assert_eq!(result.len(), 2);
821 assert_eq!(result[0].info.source, "./utils");
822 assert_eq!(result[0].info.imported_name, "helper");
823 assert_eq!(result[0].info.exported_name, "helper");
824 assert_eq!(result[1].info.source, "./types");
825 assert_eq!(result[1].info.imported_name, "*");
826 });
827 }
828
829 #[test]
830 fn re_exports_empty_list() {
831 with_empty_ctx(|ctx| {
832 let file = Path::new("/project/src/index.ts");
833 let result = resolve_re_exports(ctx, file, &[]);
834 assert!(result.is_empty());
835 });
836 }
837
838 #[test]
839 fn re_exports_preserves_type_only() {
840 with_empty_ctx(|ctx| {
841 let re_exports = vec![ReExportInfo {
842 source: "./types".into(),
843 imported_name: "MyType".into(),
844 exported_name: "MyType".into(),
845 is_type_only: true,
846 }];
847 let file = Path::new("/project/src/index.ts");
848 let result = resolve_re_exports(ctx, file, &re_exports);
849
850 assert_eq!(result.len(), 1);
851 assert!(result[0].info.is_type_only);
852 });
853 }
854
855 #[test]
860 fn require_namespace_without_destructuring() {
861 with_empty_ctx(|ctx| {
862 let req = make_require("fs", vec![], Some("fs"));
863 let file = Path::new("/project/src/app.js");
864 let result = resolve_single_require(ctx, file, &req);
865
866 assert_eq!(result.len(), 1);
867 assert!(matches!(
868 result[0].info.imported_name,
869 ImportedName::Namespace
870 ));
871 assert_eq!(result[0].info.local_name, "fs");
872 assert_eq!(result[0].info.source, "fs");
873 });
874 }
875
876 #[test]
877 fn require_namespace_without_local_name() {
878 with_empty_ctx(|ctx| {
879 let req = make_require("./side-effect", vec![], None);
880 let file = Path::new("/project/src/app.js");
881 let result = resolve_single_require(ctx, file, &req);
882
883 assert_eq!(result.len(), 1);
884 assert!(matches!(
885 result[0].info.imported_name,
886 ImportedName::Namespace
887 ));
888 assert_eq!(result[0].info.local_name, "");
890 });
891 }
892
893 #[test]
894 fn require_with_destructured_names() {
895 with_empty_ctx(|ctx| {
896 let req = make_require("path", vec!["join", "resolve"], None);
897 let file = Path::new("/project/src/app.js");
898 let result = resolve_single_require(ctx, file, &req);
899
900 assert_eq!(result.len(), 2);
901 assert!(matches!(
902 result[0].info.imported_name,
903 ImportedName::Named(ref n) if n == "join"
904 ));
905 assert_eq!(result[0].info.local_name, "join");
906 assert!(matches!(
907 result[1].info.imported_name,
908 ImportedName::Named(ref n) if n == "resolve"
909 ));
910 assert_eq!(result[1].info.local_name, "resolve");
911 assert_eq!(result[0].info.source, "path");
913 assert_eq!(result[1].info.source, "path");
914 });
915 }
916
917 #[test]
918 fn require_destructured_is_not_type_only() {
919 with_empty_ctx(|ctx| {
920 let req = make_require("path", vec!["join"], None);
921 let file = Path::new("/project/src/app.js");
922 let result = resolve_single_require(ctx, file, &req);
923
924 assert_eq!(result.len(), 1);
925 assert!(!result[0].info.is_type_only);
926 });
927 }
928
929 #[test]
934 fn require_imports_flattens_multiple() {
935 with_empty_ctx(|ctx| {
936 let reqs = vec![
937 make_require("fs", vec![], Some("fs")),
938 make_require("path", vec!["join", "resolve"], None),
939 ];
940 let file = Path::new("/project/src/app.js");
941 let result = resolve_require_imports(ctx, file, &reqs);
942
943 assert_eq!(result.len(), 3);
945 });
946 }
947
948 #[test]
949 fn require_imports_empty_list() {
950 with_empty_ctx(|ctx| {
951 let file = Path::new("/project/src/app.js");
952 let result = resolve_require_imports(ctx, file, &[]);
953 assert!(result.is_empty());
954 });
955 }
956
957 #[test]
962 fn specifier_upgrades_npm_to_internal() {
963 let mut modules = vec![
967 make_resolved_module(
968 0,
969 vec![make_resolved_import(
970 "preact/hooks",
971 ResolveResult::InternalModule(FileId(5)),
972 )],
973 vec![],
974 vec![],
975 ),
976 make_resolved_module(
977 1,
978 vec![make_resolved_import(
979 "preact/hooks",
980 ResolveResult::NpmPackage("preact".into()),
981 )],
982 vec![],
983 vec![],
984 ),
985 ];
986
987 apply_specifier_upgrades(&mut modules);
988
989 assert!(matches!(
990 modules[1].resolved_imports[0].target,
991 ResolveResult::InternalModule(FileId(5))
992 ));
993 }
994
995 #[test]
996 fn specifier_upgrades_noop_when_no_internal() {
997 let mut modules = vec![
999 make_resolved_module(
1000 0,
1001 vec![make_resolved_import(
1002 "lodash",
1003 ResolveResult::NpmPackage("lodash".into()),
1004 )],
1005 vec![],
1006 vec![],
1007 ),
1008 make_resolved_module(
1009 1,
1010 vec![make_resolved_import(
1011 "lodash",
1012 ResolveResult::NpmPackage("lodash".into()),
1013 )],
1014 vec![],
1015 vec![],
1016 ),
1017 ];
1018
1019 apply_specifier_upgrades(&mut modules);
1020
1021 assert!(matches!(
1022 modules[0].resolved_imports[0].target,
1023 ResolveResult::NpmPackage(_)
1024 ));
1025 assert!(matches!(
1026 modules[1].resolved_imports[0].target,
1027 ResolveResult::NpmPackage(_)
1028 ));
1029 }
1030
1031 #[test]
1032 fn specifier_upgrades_empty_modules() {
1033 let mut modules: Vec<ResolvedModule> = vec![];
1034 apply_specifier_upgrades(&mut modules);
1035 assert!(modules.is_empty());
1036 }
1037
1038 #[test]
1039 fn specifier_upgrades_skips_relative_specifiers() {
1040 let mut modules = vec![
1043 make_resolved_module(
1044 0,
1045 vec![make_resolved_import(
1046 "./utils",
1047 ResolveResult::InternalModule(FileId(5)),
1048 )],
1049 vec![],
1050 vec![],
1051 ),
1052 make_resolved_module(
1053 1,
1054 vec![make_resolved_import(
1055 "./utils",
1056 ResolveResult::NpmPackage("utils".into()),
1057 )],
1058 vec![],
1059 vec![],
1060 ),
1061 ];
1062
1063 apply_specifier_upgrades(&mut modules);
1064
1065 assert!(matches!(
1067 modules[1].resolved_imports[0].target,
1068 ResolveResult::NpmPackage(_)
1069 ));
1070 }
1071
1072 #[test]
1073 fn specifier_upgrades_applies_to_dynamic_imports() {
1074 let mut modules = vec![
1075 make_resolved_module(
1076 0,
1077 vec![],
1078 vec![make_resolved_import(
1079 "preact/hooks",
1080 ResolveResult::InternalModule(FileId(5)),
1081 )],
1082 vec![],
1083 ),
1084 make_resolved_module(
1085 1,
1086 vec![],
1087 vec![make_resolved_import(
1088 "preact/hooks",
1089 ResolveResult::NpmPackage("preact".into()),
1090 )],
1091 vec![],
1092 ),
1093 ];
1094
1095 apply_specifier_upgrades(&mut modules);
1096
1097 assert!(matches!(
1098 modules[1].resolved_dynamic_imports[0].target,
1099 ResolveResult::InternalModule(FileId(5))
1100 ));
1101 }
1102
1103 #[test]
1104 fn specifier_upgrades_applies_to_re_exports() {
1105 let mut modules = vec![
1106 make_resolved_module(
1107 0,
1108 vec![],
1109 vec![],
1110 vec![make_resolved_re_export(
1111 "preact/hooks",
1112 ResolveResult::InternalModule(FileId(5)),
1113 )],
1114 ),
1115 make_resolved_module(
1116 1,
1117 vec![],
1118 vec![],
1119 vec![make_resolved_re_export(
1120 "preact/hooks",
1121 ResolveResult::NpmPackage("preact".into()),
1122 )],
1123 ),
1124 ];
1125
1126 apply_specifier_upgrades(&mut modules);
1127
1128 assert!(matches!(
1129 modules[1].re_exports[0].target,
1130 ResolveResult::InternalModule(FileId(5))
1131 ));
1132 }
1133
1134 #[test]
1135 fn specifier_upgrades_does_not_downgrade_internal() {
1136 let mut modules = vec![
1138 make_resolved_module(
1139 0,
1140 vec![make_resolved_import(
1141 "preact/hooks",
1142 ResolveResult::InternalModule(FileId(5)),
1143 )],
1144 vec![],
1145 vec![],
1146 ),
1147 make_resolved_module(
1148 1,
1149 vec![make_resolved_import(
1150 "preact/hooks",
1151 ResolveResult::InternalModule(FileId(5)),
1152 )],
1153 vec![],
1154 vec![],
1155 ),
1156 ];
1157
1158 apply_specifier_upgrades(&mut modules);
1159
1160 assert!(matches!(
1161 modules[0].resolved_imports[0].target,
1162 ResolveResult::InternalModule(FileId(5))
1163 ));
1164 assert!(matches!(
1165 modules[1].resolved_imports[0].target,
1166 ResolveResult::InternalModule(FileId(5))
1167 ));
1168 }
1169
1170 #[test]
1171 fn specifier_upgrades_first_internal_wins() {
1172 let mut modules = vec![
1175 make_resolved_module(
1176 0,
1177 vec![make_resolved_import(
1178 "shared-lib",
1179 ResolveResult::InternalModule(FileId(10)),
1180 )],
1181 vec![],
1182 vec![],
1183 ),
1184 make_resolved_module(
1185 1,
1186 vec![make_resolved_import(
1187 "shared-lib",
1188 ResolveResult::InternalModule(FileId(20)),
1189 )],
1190 vec![],
1191 vec![],
1192 ),
1193 make_resolved_module(
1194 2,
1195 vec![make_resolved_import(
1196 "shared-lib",
1197 ResolveResult::NpmPackage("shared-lib".into()),
1198 )],
1199 vec![],
1200 vec![],
1201 ),
1202 ];
1203
1204 apply_specifier_upgrades(&mut modules);
1205
1206 assert!(matches!(
1208 modules[2].resolved_imports[0].target,
1209 ResolveResult::InternalModule(FileId(10))
1210 ));
1211 }
1212
1213 #[test]
1214 fn specifier_upgrades_does_not_touch_unresolvable() {
1215 let mut modules = vec![
1218 make_resolved_module(
1219 0,
1220 vec![make_resolved_import(
1221 "my-lib",
1222 ResolveResult::InternalModule(FileId(1)),
1223 )],
1224 vec![],
1225 vec![],
1226 ),
1227 make_resolved_module(
1228 1,
1229 vec![ResolvedImport {
1230 info: make_import("my-lib", ImportedName::Default, "myLib"),
1231 target: ResolveResult::Unresolvable("my-lib".into()),
1232 }],
1233 vec![],
1234 vec![],
1235 ),
1236 ];
1237
1238 apply_specifier_upgrades(&mut modules);
1239
1240 assert!(matches!(
1242 modules[1].resolved_imports[0].target,
1243 ResolveResult::Unresolvable(_)
1244 ));
1245 }
1246
1247 #[test]
1248 fn specifier_upgrades_cross_import_and_re_export() {
1249 let mut modules = vec![
1252 make_resolved_module(
1253 0,
1254 vec![make_resolved_import(
1255 "@myorg/utils",
1256 ResolveResult::InternalModule(FileId(3)),
1257 )],
1258 vec![],
1259 vec![],
1260 ),
1261 make_resolved_module(
1262 1,
1263 vec![],
1264 vec![],
1265 vec![make_resolved_re_export(
1266 "@myorg/utils",
1267 ResolveResult::NpmPackage("@myorg/utils".into()),
1268 )],
1269 ),
1270 ];
1271
1272 apply_specifier_upgrades(&mut modules);
1273
1274 assert!(matches!(
1275 modules[1].re_exports[0].target,
1276 ResolveResult::InternalModule(FileId(3))
1277 ));
1278 }
1279
1280 #[test]
1285 fn dynamic_patterns_matches_files_in_dir() {
1286 let from_dir = Path::new("/project/src");
1287 let patterns = vec![DynamicImportPattern {
1288 prefix: "./locales/".into(),
1289 suffix: Some(".json".into()),
1290 span: dummy_span(),
1291 }];
1292 let canonical_paths = vec![
1293 PathBuf::from("/project/src/locales/en.json"),
1294 PathBuf::from("/project/src/locales/fr.json"),
1295 PathBuf::from("/project/src/utils.ts"),
1296 ];
1297 let files = vec![
1298 DiscoveredFile {
1299 id: FileId(0),
1300 path: PathBuf::from("/project/src/locales/en.json"),
1301 size_bytes: 100,
1302 },
1303 DiscoveredFile {
1304 id: FileId(1),
1305 path: PathBuf::from("/project/src/locales/fr.json"),
1306 size_bytes: 100,
1307 },
1308 DiscoveredFile {
1309 id: FileId(2),
1310 path: PathBuf::from("/project/src/utils.ts"),
1311 size_bytes: 100,
1312 },
1313 ];
1314
1315 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1316
1317 assert_eq!(result.len(), 1);
1318 assert_eq!(result[0].1.len(), 2);
1319 assert!(result[0].1.contains(&FileId(0)));
1320 assert!(result[0].1.contains(&FileId(1)));
1321 }
1322
1323 #[test]
1324 fn dynamic_patterns_no_matches_returns_empty() {
1325 let from_dir = Path::new("/project/src");
1326 let patterns = vec![DynamicImportPattern {
1327 prefix: "./locales/".into(),
1328 suffix: Some(".json".into()),
1329 span: dummy_span(),
1330 }];
1331 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1332 let files = vec![DiscoveredFile {
1333 id: FileId(0),
1334 path: PathBuf::from("/project/src/utils.ts"),
1335 size_bytes: 100,
1336 }];
1337
1338 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1339
1340 assert!(result.is_empty());
1341 }
1342
1343 #[test]
1344 fn dynamic_patterns_empty_patterns_list() {
1345 let from_dir = Path::new("/project/src");
1346 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1347 let files = vec![DiscoveredFile {
1348 id: FileId(0),
1349 path: PathBuf::from("/project/src/utils.ts"),
1350 size_bytes: 100,
1351 }];
1352
1353 let result = resolve_dynamic_patterns(from_dir, &[], &canonical_paths, &files);
1354 assert!(result.is_empty());
1355 }
1356
1357 #[test]
1358 fn dynamic_patterns_glob_prefix_passthrough() {
1359 let from_dir = Path::new("/project/src");
1360 let patterns = vec![DynamicImportPattern {
1361 prefix: "./**/*.ts".into(),
1362 suffix: None,
1363 span: dummy_span(),
1364 }];
1365 let canonical_paths = vec![
1366 PathBuf::from("/project/src/utils.ts"),
1367 PathBuf::from("/project/src/deep/nested.ts"),
1368 ];
1369 let files = vec![
1370 DiscoveredFile {
1371 id: FileId(0),
1372 path: PathBuf::from("/project/src/utils.ts"),
1373 size_bytes: 100,
1374 },
1375 DiscoveredFile {
1376 id: FileId(1),
1377 path: PathBuf::from("/project/src/deep/nested.ts"),
1378 size_bytes: 100,
1379 },
1380 ];
1381
1382 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1383
1384 assert_eq!(result.len(), 1);
1385 assert_eq!(result[0].1.len(), 2);
1386 }
1387
1388 #[test]
1393 fn static_import_unresolvable_relative_path() {
1394 with_empty_ctx(|ctx| {
1395 let imports = vec![make_import(
1396 "./nonexistent",
1397 ImportedName::Default,
1398 "missing",
1399 )];
1400 let file = Path::new("/project/src/app.ts");
1401 let result = resolve_static_imports(ctx, file, &imports);
1402
1403 assert_eq!(result.len(), 1);
1404 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1405 });
1406 }
1407
1408 #[test]
1409 fn static_import_bare_specifier_becomes_npm_package() {
1410 with_empty_ctx(|ctx| {
1411 let imports = vec![make_import("react", ImportedName::Default, "React")];
1412 let file = Path::new("/project/src/app.ts");
1413 let result = resolve_static_imports(ctx, file, &imports);
1414
1415 assert_eq!(result.len(), 1);
1416 assert!(matches!(
1417 result[0].target,
1418 ResolveResult::NpmPackage(ref pkg) if pkg == "react"
1419 ));
1420 });
1421 }
1422
1423 #[test]
1424 fn require_bare_specifier_becomes_npm_package() {
1425 with_empty_ctx(|ctx| {
1426 let req = make_require("express", vec![], Some("express"));
1427 let file = Path::new("/project/src/app.js");
1428 let result = resolve_single_require(ctx, file, &req);
1429
1430 assert_eq!(result.len(), 1);
1431 assert!(matches!(
1432 result[0].target,
1433 ResolveResult::NpmPackage(ref pkg) if pkg == "express"
1434 ));
1435 });
1436 }
1437
1438 #[test]
1439 fn dynamic_import_unresolvable() {
1440 with_empty_ctx(|ctx| {
1441 let imp = make_dynamic("./missing-module", vec![], None);
1442 let file = Path::new("/project/src/app.ts");
1443 let result = resolve_single_dynamic_import(ctx, file, &imp);
1444
1445 assert_eq!(result.len(), 1);
1446 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1447 });
1448 }
1449
1450 #[test]
1451 fn re_export_unresolvable() {
1452 with_empty_ctx(|ctx| {
1453 let re_exports = vec![make_re_export("./missing", "foo", "foo")];
1454 let file = Path::new("/project/src/index.ts");
1455 let result = resolve_re_exports(ctx, file, &re_exports);
1456
1457 assert_eq!(result.len(), 1);
1458 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1459 });
1460 }
1461
1462 #[test]
1467 fn dynamic_patterns_template_literal_prefix_suffix() {
1468 let from_dir = Path::new("/project/src");
1470 let patterns = vec![DynamicImportPattern {
1471 prefix: "./locales/".into(),
1472 suffix: Some(".json".into()),
1473 span: dummy_span(),
1474 }];
1475 let canonical_paths = vec![
1476 PathBuf::from("/project/src/locales/en.json"),
1477 PathBuf::from("/project/src/locales/de.json"),
1478 PathBuf::from("/project/src/locales/README.md"),
1479 PathBuf::from("/project/src/config.ts"),
1480 ];
1481 let files = vec![
1482 DiscoveredFile {
1483 id: FileId(0),
1484 path: PathBuf::from("/project/src/locales/en.json"),
1485 size_bytes: 100,
1486 },
1487 DiscoveredFile {
1488 id: FileId(1),
1489 path: PathBuf::from("/project/src/locales/de.json"),
1490 size_bytes: 100,
1491 },
1492 DiscoveredFile {
1493 id: FileId(2),
1494 path: PathBuf::from("/project/src/locales/README.md"),
1495 size_bytes: 100,
1496 },
1497 DiscoveredFile {
1498 id: FileId(3),
1499 path: PathBuf::from("/project/src/config.ts"),
1500 size_bytes: 100,
1501 },
1502 ];
1503
1504 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1505
1506 assert_eq!(result.len(), 1, "should produce exactly one pattern match");
1507 let matched_ids = &result[0].1;
1508 assert_eq!(
1509 matched_ids.len(),
1510 2,
1511 "should match en.json and de.json only"
1512 );
1513 assert!(matched_ids.contains(&FileId(0)));
1514 assert!(matched_ids.contains(&FileId(1)));
1515 assert!(
1516 !matched_ids.contains(&FileId(2)),
1517 "README.md should not match .json suffix"
1518 );
1519 assert!(
1520 !matched_ids.contains(&FileId(3)),
1521 "config.ts should not match locales/ prefix"
1522 );
1523 }
1524
1525 #[test]
1526 fn dynamic_patterns_string_concat_prefix_only() {
1527 let from_dir = Path::new("/project/src");
1529 let patterns = vec![DynamicImportPattern {
1530 prefix: "./pages/".into(),
1531 suffix: None,
1532 span: dummy_span(),
1533 }];
1534 let canonical_paths = vec![
1535 PathBuf::from("/project/src/pages/home.ts"),
1536 PathBuf::from("/project/src/pages/about.ts"),
1537 PathBuf::from("/project/src/pages/nested/deep.ts"),
1538 PathBuf::from("/project/src/utils.ts"),
1539 ];
1540 let files = vec![
1541 DiscoveredFile {
1542 id: FileId(0),
1543 path: PathBuf::from("/project/src/pages/home.ts"),
1544 size_bytes: 100,
1545 },
1546 DiscoveredFile {
1547 id: FileId(1),
1548 path: PathBuf::from("/project/src/pages/about.ts"),
1549 size_bytes: 100,
1550 },
1551 DiscoveredFile {
1552 id: FileId(2),
1553 path: PathBuf::from("/project/src/pages/nested/deep.ts"),
1554 size_bytes: 100,
1555 },
1556 DiscoveredFile {
1557 id: FileId(3),
1558 path: PathBuf::from("/project/src/utils.ts"),
1559 size_bytes: 100,
1560 },
1561 ];
1562
1563 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1564
1565 assert_eq!(result.len(), 1);
1566 let matched_ids = &result[0].1;
1567 assert!(matched_ids.contains(&FileId(0)), "home.ts should match");
1569 assert!(matched_ids.contains(&FileId(1)), "about.ts should match");
1570 assert!(
1571 !matched_ids.contains(&FileId(3)),
1572 "utils.ts should not match pages/ prefix"
1573 );
1574 }
1575
1576 #[test]
1577 fn dynamic_patterns_import_meta_glob_recursive() {
1578 let from_dir = Path::new("/project/src");
1580 let patterns = vec![DynamicImportPattern {
1581 prefix: "./components/**/*.ts".into(),
1582 suffix: None,
1583 span: dummy_span(),
1584 }];
1585 let canonical_paths = vec![
1586 PathBuf::from("/project/src/components/Button.ts"),
1587 PathBuf::from("/project/src/components/forms/Input.ts"),
1588 PathBuf::from("/project/src/components/Button.css"),
1589 PathBuf::from("/project/src/utils.ts"),
1590 ];
1591 let files = vec![
1592 DiscoveredFile {
1593 id: FileId(0),
1594 path: PathBuf::from("/project/src/components/Button.ts"),
1595 size_bytes: 100,
1596 },
1597 DiscoveredFile {
1598 id: FileId(1),
1599 path: PathBuf::from("/project/src/components/forms/Input.ts"),
1600 size_bytes: 100,
1601 },
1602 DiscoveredFile {
1603 id: FileId(2),
1604 path: PathBuf::from("/project/src/components/Button.css"),
1605 size_bytes: 100,
1606 },
1607 DiscoveredFile {
1608 id: FileId(3),
1609 path: PathBuf::from("/project/src/utils.ts"),
1610 size_bytes: 100,
1611 },
1612 ];
1613
1614 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1615
1616 assert_eq!(result.len(), 1);
1617 let matched_ids = &result[0].1;
1618 assert!(
1619 matched_ids.contains(&FileId(0)),
1620 "Button.ts should match **/*.ts"
1621 );
1622 assert!(
1623 matched_ids.contains(&FileId(1)),
1624 "forms/Input.ts should match **/*.ts recursively"
1625 );
1626 assert!(
1627 !matched_ids.contains(&FileId(2)),
1628 "Button.css should not match *.ts pattern"
1629 );
1630 assert!(
1631 !matched_ids.contains(&FileId(3)),
1632 "utils.ts outside components/ should not match"
1633 );
1634 }
1635
1636 #[test]
1637 fn dynamic_patterns_import_meta_glob_brace_expansion() {
1638 let from_dir = Path::new("/project/src");
1640 let patterns = vec![DynamicImportPattern {
1641 prefix: "./routes/**/*.{ts,tsx}".into(),
1642 suffix: None,
1643 span: dummy_span(),
1644 }];
1645 let canonical_paths = vec![
1646 PathBuf::from("/project/src/routes/home.ts"),
1647 PathBuf::from("/project/src/routes/about.tsx"),
1648 PathBuf::from("/project/src/routes/layout.css"),
1649 ];
1650 let files = vec![
1651 DiscoveredFile {
1652 id: FileId(0),
1653 path: PathBuf::from("/project/src/routes/home.ts"),
1654 size_bytes: 100,
1655 },
1656 DiscoveredFile {
1657 id: FileId(1),
1658 path: PathBuf::from("/project/src/routes/about.tsx"),
1659 size_bytes: 100,
1660 },
1661 DiscoveredFile {
1662 id: FileId(2),
1663 path: PathBuf::from("/project/src/routes/layout.css"),
1664 size_bytes: 100,
1665 },
1666 ];
1667
1668 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1669
1670 assert_eq!(result.len(), 1);
1671 let matched_ids = &result[0].1;
1672 assert!(matched_ids.contains(&FileId(0)), "home.ts should match");
1673 assert!(matched_ids.contains(&FileId(1)), "about.tsx should match");
1674 assert!(
1675 !matched_ids.contains(&FileId(2)),
1676 "layout.css should not match ts/tsx brace expansion"
1677 );
1678 }
1679
1680 #[test]
1681 fn dynamic_patterns_no_static_part_matches_everything() {
1682 let from_dir = Path::new("/project/src");
1687 let patterns = vec![DynamicImportPattern {
1688 prefix: String::new(),
1689 suffix: None,
1690 span: dummy_span(),
1691 }];
1692 let canonical_paths = vec![
1693 PathBuf::from("/project/src/a.ts"),
1694 PathBuf::from("/project/src/b.ts"),
1695 ];
1696 let files = vec![
1697 DiscoveredFile {
1698 id: FileId(0),
1699 path: PathBuf::from("/project/src/a.ts"),
1700 size_bytes: 100,
1701 },
1702 DiscoveredFile {
1703 id: FileId(1),
1704 path: PathBuf::from("/project/src/b.ts"),
1705 size_bytes: 100,
1706 },
1707 ];
1708
1709 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1710
1711 assert_eq!(result.len(), 1);
1713 assert_eq!(result[0].1.len(), 2);
1714 }
1715
1716 #[test]
1717 fn dynamic_patterns_multiple_patterns_independent() {
1718 let from_dir = Path::new("/project/src");
1720 let patterns = vec![
1721 DynamicImportPattern {
1722 prefix: "./locales/".into(),
1723 suffix: Some(".json".into()),
1724 span: dummy_span(),
1725 },
1726 DynamicImportPattern {
1727 prefix: "./pages/".into(),
1728 suffix: Some(".ts".into()),
1729 span: dummy_span(),
1730 },
1731 ];
1732 let canonical_paths = vec![
1733 PathBuf::from("/project/src/locales/en.json"),
1734 PathBuf::from("/project/src/pages/home.ts"),
1735 PathBuf::from("/project/src/utils.ts"),
1736 ];
1737 let files = vec![
1738 DiscoveredFile {
1739 id: FileId(0),
1740 path: PathBuf::from("/project/src/locales/en.json"),
1741 size_bytes: 100,
1742 },
1743 DiscoveredFile {
1744 id: FileId(1),
1745 path: PathBuf::from("/project/src/pages/home.ts"),
1746 size_bytes: 100,
1747 },
1748 DiscoveredFile {
1749 id: FileId(2),
1750 path: PathBuf::from("/project/src/utils.ts"),
1751 size_bytes: 100,
1752 },
1753 ];
1754
1755 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1756
1757 assert_eq!(result.len(), 2, "both patterns should produce matches");
1758 assert!(result[0].1.contains(&FileId(0)));
1760 assert_eq!(result[0].1.len(), 1);
1761 assert!(result[1].1.contains(&FileId(1)));
1763 assert_eq!(result[1].1.len(), 1);
1764 }
1765
1766 #[test]
1767 fn dynamic_patterns_files_outside_from_dir_not_matched() {
1768 let from_dir = Path::new("/project/src");
1770 let patterns = vec![DynamicImportPattern {
1771 prefix: "./utils/".into(),
1772 suffix: None,
1773 span: dummy_span(),
1774 }];
1775 let canonical_paths = vec![
1776 PathBuf::from("/project/other/utils/helper.ts"),
1777 PathBuf::from("/project/src/utils/helper.ts"),
1778 ];
1779 let files = vec![
1780 DiscoveredFile {
1781 id: FileId(0),
1782 path: PathBuf::from("/project/other/utils/helper.ts"),
1783 size_bytes: 100,
1784 },
1785 DiscoveredFile {
1786 id: FileId(1),
1787 path: PathBuf::from("/project/src/utils/helper.ts"),
1788 size_bytes: 100,
1789 },
1790 ];
1791
1792 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1793
1794 assert_eq!(result.len(), 1);
1795 let matched_ids = &result[0].1;
1796 assert!(
1797 !matched_ids.contains(&FileId(0)),
1798 "file outside from_dir should not match"
1799 );
1800 assert!(
1801 matched_ids.contains(&FileId(1)),
1802 "file inside from_dir should match"
1803 );
1804 }
1805
1806 #[test]
1811 fn dynamic_patterns_raw_paths_when_canonical_empty() {
1812 let from_dir = Path::new("/project/src");
1814 let patterns = vec![DynamicImportPattern {
1815 prefix: "./locales/".into(),
1816 suffix: Some(".json".into()),
1817 span: dummy_span(),
1818 }];
1819 let canonical_paths: Vec<PathBuf> = vec![]; let files = vec![
1821 DiscoveredFile {
1822 id: FileId(0),
1823 path: PathBuf::from("/project/src/locales/en.json"),
1824 size_bytes: 100,
1825 },
1826 DiscoveredFile {
1827 id: FileId(1),
1828 path: PathBuf::from("/project/src/locales/fr.json"),
1829 size_bytes: 100,
1830 },
1831 DiscoveredFile {
1832 id: FileId(2),
1833 path: PathBuf::from("/project/src/main.ts"),
1834 size_bytes: 100,
1835 },
1836 ];
1837
1838 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1839
1840 assert_eq!(result.len(), 1);
1841 assert_eq!(result[0].1.len(), 2);
1842 assert!(result[0].1.contains(&FileId(0)));
1843 assert!(result[0].1.contains(&FileId(1)));
1844 }
1845
1846 #[test]
1847 fn dynamic_patterns_raw_paths_no_match() {
1848 let from_dir = Path::new("/project/src");
1849 let patterns = vec![DynamicImportPattern {
1850 prefix: "./missing-dir/".into(),
1851 suffix: None,
1852 span: dummy_span(),
1853 }];
1854 let canonical_paths: Vec<PathBuf> = vec![];
1855 let files = vec![DiscoveredFile {
1856 id: FileId(0),
1857 path: PathBuf::from("/project/src/utils.ts"),
1858 size_bytes: 100,
1859 }];
1860
1861 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1862
1863 assert!(
1864 result.is_empty(),
1865 "no files match the pattern, should return empty"
1866 );
1867 }
1868
1869 #[test]
1874 fn dynamic_import_destructured_all_share_same_target() {
1875 with_empty_ctx(|ctx| {
1876 let imp = make_dynamic("react", vec!["useState", "useEffect", "useRef"], None);
1877 let file = Path::new("/project/src/app.ts");
1878 let result = resolve_single_dynamic_import(ctx, file, &imp);
1879
1880 assert_eq!(result.len(), 3);
1881 for resolved in &result {
1883 assert_eq!(resolved.info.source, "react");
1884 assert!(!resolved.info.is_type_only);
1885 assert!(matches!(
1886 resolved.info.imported_name,
1887 ImportedName::Named(_)
1888 ));
1889 }
1890 let names: Vec<&str> = result
1892 .iter()
1893 .filter_map(|r| match &r.info.imported_name {
1894 ImportedName::Named(n) => Some(n.as_str()),
1895 _ => None,
1896 })
1897 .collect();
1898 assert_eq!(names, vec!["useState", "useEffect", "useRef"]);
1899 });
1900 }
1901
1902 #[test]
1903 fn dynamic_import_empty_destructured_with_no_local_is_side_effect() {
1904 with_empty_ctx(|ctx| {
1906 let imp = make_dynamic("./setup", vec![], None);
1907 let file = Path::new("/project/src/main.ts");
1908 let result = resolve_single_dynamic_import(ctx, file, &imp);
1909
1910 assert_eq!(result.len(), 1);
1911 assert!(matches!(
1912 result[0].info.imported_name,
1913 ImportedName::SideEffect
1914 ));
1915 assert_eq!(result[0].info.local_name, "");
1916 });
1917 }
1918
1919 #[test]
1920 fn dynamic_import_bare_specifier_becomes_npm_package() {
1921 with_empty_ctx(|ctx| {
1922 let imp = make_dynamic("lodash", vec![], Some("_"));
1923 let file = Path::new("/project/src/app.ts");
1924 let result = resolve_single_dynamic_import(ctx, file, &imp);
1925
1926 assert_eq!(result.len(), 1);
1927 assert!(matches!(
1928 result[0].target,
1929 ResolveResult::NpmPackage(ref pkg) if pkg == "lodash"
1930 ));
1931 assert!(matches!(
1932 result[0].info.imported_name,
1933 ImportedName::Namespace
1934 ));
1935 });
1936 }
1937
1938 #[test]
1943 fn dynamic_patterns_invalid_glob_skipped() {
1944 let from_dir = Path::new("/project/src");
1946 let patterns = vec![DynamicImportPattern {
1947 prefix: "./[invalid".into(), suffix: None,
1949 span: dummy_span(),
1950 }];
1951 let canonical_paths = vec![PathBuf::from("/project/src/test.ts")];
1952 let files = vec![DiscoveredFile {
1953 id: FileId(0),
1954 path: PathBuf::from("/project/src/test.ts"),
1955 size_bytes: 100,
1956 }];
1957
1958 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1959
1960 assert!(
1962 result.is_empty(),
1963 "invalid glob pattern should be skipped gracefully"
1964 );
1965 }
1966}