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 canonical_paths: Vec<PathBuf> = files
60 .par_iter()
61 .map(|f| f.path.canonicalize().unwrap_or_else(|_| f.path.clone()))
62 .collect();
63
64 let path_to_id: FxHashMap<&Path, FileId> = canonical_paths
66 .iter()
67 .enumerate()
68 .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
69 .collect();
70
71 let raw_path_to_id: FxHashMap<&Path, FileId> =
73 files.iter().map(|f| (f.path.as_path(), f.id)).collect();
74
75 let file_paths: Vec<&Path> = files.iter().map(|f| f.path.as_path()).collect();
77
78 let resolver = create_resolver(active_plugins);
80
81 let ctx = ResolveContext {
83 resolver: &resolver,
84 path_to_id: &path_to_id,
85 raw_path_to_id: &raw_path_to_id,
86 workspace_roots: &workspace_roots,
87 path_aliases,
88 root,
89 };
90
91 let mut resolved: Vec<ResolvedModule> = modules
96 .par_iter()
97 .filter_map(|module| {
98 let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
99 tracing::warn!(
100 file_id = module.file_id.0,
101 "Skipping module with unknown file_id during resolution"
102 );
103 return None;
104 };
105
106 let mut all_imports = resolve_static_imports(&ctx, file_path, &module.imports);
107 all_imports.extend(resolve_require_imports(
108 &ctx,
109 file_path,
110 &module.require_calls,
111 ));
112
113 let from_dir = canonical_paths
114 .get(module.file_id.0 as usize)
115 .and_then(|p| p.parent())
116 .unwrap_or(file_path);
117
118 Some(ResolvedModule {
119 file_id: module.file_id,
120 path: file_path.to_path_buf(),
121 exports: module.exports.clone(),
122 re_exports: resolve_re_exports(&ctx, file_path, &module.re_exports),
123 resolved_imports: all_imports,
124 resolved_dynamic_imports: resolve_dynamic_imports(
125 &ctx,
126 file_path,
127 &module.dynamic_imports,
128 ),
129 resolved_dynamic_patterns: resolve_dynamic_patterns(
130 from_dir,
131 &module.dynamic_import_patterns,
132 &canonical_paths,
133 files,
134 ),
135 member_accesses: module.member_accesses.clone(),
136 whole_object_uses: module.whole_object_uses.clone(),
137 has_cjs_exports: module.has_cjs_exports,
138 unused_import_bindings: module.unused_import_bindings.clone(),
139 })
140 })
141 .collect();
142
143 apply_specifier_upgrades(&mut resolved);
144
145 resolved
146}
147
148fn resolve_static_imports(
150 ctx: &ResolveContext,
151 file_path: &Path,
152 imports: &[ImportInfo],
153) -> Vec<ResolvedImport> {
154 imports
155 .iter()
156 .map(|imp| ResolvedImport {
157 info: imp.clone(),
158 target: resolve_specifier(ctx, file_path, &imp.source),
159 })
160 .collect()
161}
162
163fn resolve_dynamic_imports(
165 ctx: &ResolveContext,
166 file_path: &Path,
167 dynamic_imports: &[DynamicImportInfo],
168) -> Vec<ResolvedImport> {
169 dynamic_imports
170 .iter()
171 .flat_map(|imp| resolve_single_dynamic_import(ctx, file_path, imp))
172 .collect()
173}
174
175fn resolve_single_dynamic_import(
177 ctx: &ResolveContext,
178 file_path: &Path,
179 imp: &DynamicImportInfo,
180) -> Vec<ResolvedImport> {
181 let target = resolve_specifier(ctx, file_path, &imp.source);
182
183 if !imp.destructured_names.is_empty() {
184 return imp
186 .destructured_names
187 .iter()
188 .map(|name| ResolvedImport {
189 info: ImportInfo {
190 source: imp.source.clone(),
191 imported_name: ImportedName::Named(name.clone()),
192 local_name: name.clone(),
193 is_type_only: false,
194 span: imp.span,
195 source_span: Span::default(),
196 },
197 target: target.clone(),
198 })
199 .collect();
200 }
201
202 if imp.local_name.is_some() {
203 return vec![ResolvedImport {
205 info: ImportInfo {
206 source: imp.source.clone(),
207 imported_name: ImportedName::Namespace,
208 local_name: imp.local_name.clone().unwrap_or_default(),
209 is_type_only: false,
210 span: imp.span,
211 source_span: Span::default(),
212 },
213 target,
214 }];
215 }
216
217 vec![ResolvedImport {
219 info: ImportInfo {
220 source: imp.source.clone(),
221 imported_name: ImportedName::SideEffect,
222 local_name: String::new(),
223 is_type_only: false,
224 span: imp.span,
225 source_span: Span::default(),
226 },
227 target,
228 }]
229}
230
231fn resolve_re_exports(
233 ctx: &ResolveContext,
234 file_path: &Path,
235 re_exports: &[ReExportInfo],
236) -> Vec<ResolvedReExport> {
237 re_exports
238 .iter()
239 .map(|re| ResolvedReExport {
240 info: re.clone(),
241 target: resolve_specifier(ctx, file_path, &re.source),
242 })
243 .collect()
244}
245
246fn resolve_require_imports(
249 ctx: &ResolveContext,
250 file_path: &Path,
251 require_calls: &[RequireCallInfo],
252) -> Vec<ResolvedImport> {
253 require_calls
254 .iter()
255 .flat_map(|req| resolve_single_require(ctx, file_path, req))
256 .collect()
257}
258
259fn resolve_single_require(
261 ctx: &ResolveContext,
262 file_path: &Path,
263 req: &RequireCallInfo,
264) -> Vec<ResolvedImport> {
265 let target = resolve_specifier(ctx, file_path, &req.source);
266
267 if req.destructured_names.is_empty() {
268 return vec![ResolvedImport {
269 info: ImportInfo {
270 source: req.source.clone(),
271 imported_name: ImportedName::Namespace,
272 local_name: req.local_name.clone().unwrap_or_default(),
273 is_type_only: false,
274 span: req.span,
275 source_span: Span::default(),
276 },
277 target,
278 }];
279 }
280
281 req.destructured_names
282 .iter()
283 .map(|name| ResolvedImport {
284 info: ImportInfo {
285 source: req.source.clone(),
286 imported_name: ImportedName::Named(name.clone()),
287 local_name: name.clone(),
288 is_type_only: false,
289 span: req.span,
290 source_span: Span::default(),
291 },
292 target: target.clone(),
293 })
294 .collect()
295}
296
297fn resolve_dynamic_patterns(
300 from_dir: &Path,
301 patterns: &[DynamicImportPattern],
302 canonical_paths: &[PathBuf],
303 files: &[DiscoveredFile],
304) -> Vec<(DynamicImportPattern, Vec<FileId>)> {
305 patterns
306 .iter()
307 .filter_map(|pattern| {
308 let glob_str = make_glob_from_pattern(pattern);
309 let matcher = globset::Glob::new(&glob_str)
310 .ok()
311 .map(|g| g.compile_matcher())?;
312 let matched: Vec<FileId> = canonical_paths
313 .iter()
314 .enumerate()
315 .filter(|(_idx, canonical)| {
316 canonical.strip_prefix(from_dir).is_ok_and(|relative| {
317 let rel_str = format!("./{}", relative.to_string_lossy());
318 matcher.is_match(&rel_str)
319 })
320 })
321 .map(|(idx, _)| files[idx].id)
322 .collect();
323 if matched.is_empty() {
324 None
325 } else {
326 Some((pattern.clone(), matched))
327 }
328 })
329 .collect()
330}
331
332fn apply_specifier_upgrades(resolved: &mut [ResolvedModule]) {
348 let mut specifier_upgrades: FxHashMap<String, FileId> = FxHashMap::default();
349 for module in resolved.iter() {
350 for imp in module
351 .resolved_imports
352 .iter()
353 .chain(module.resolved_dynamic_imports.iter())
354 {
355 if is_bare_specifier(&imp.info.source)
356 && let ResolveResult::InternalModule(file_id) = &imp.target
357 {
358 specifier_upgrades
359 .entry(imp.info.source.clone())
360 .or_insert(*file_id);
361 }
362 }
363 for re in &module.re_exports {
364 if is_bare_specifier(&re.info.source)
365 && let ResolveResult::InternalModule(file_id) = &re.target
366 {
367 specifier_upgrades
368 .entry(re.info.source.clone())
369 .or_insert(*file_id);
370 }
371 }
372 }
373
374 if specifier_upgrades.is_empty() {
375 return;
376 }
377
378 for module in resolved.iter_mut() {
380 for imp in module
381 .resolved_imports
382 .iter_mut()
383 .chain(module.resolved_dynamic_imports.iter_mut())
384 {
385 if matches!(imp.target, ResolveResult::NpmPackage(_))
386 && let Some(&file_id) = specifier_upgrades.get(&imp.info.source)
387 {
388 imp.target = ResolveResult::InternalModule(file_id);
389 }
390 }
391 for re in &mut module.re_exports {
392 if matches!(re.target, ResolveResult::NpmPackage(_))
393 && let Some(&file_id) = specifier_upgrades.get(&re.info.source)
394 {
395 re.target = ResolveResult::InternalModule(file_id);
396 }
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use oxc_span::Span;
405
406 fn dummy_span() -> Span {
411 Span::new(0, 0)
412 }
413
414 #[cfg(not(miri))]
422 fn with_empty_ctx<F: FnOnce(&ResolveContext)>(f: F) {
423 let resolver = specifier::create_resolver(&[]);
424 let path_to_id = FxHashMap::default();
425 let raw_path_to_id = FxHashMap::default();
426 let workspace_roots = FxHashMap::default();
427 let root = PathBuf::from("/project");
428 let ctx = ResolveContext {
429 resolver: &resolver,
430 path_to_id: &path_to_id,
431 raw_path_to_id: &raw_path_to_id,
432 workspace_roots: &workspace_roots,
433 path_aliases: &[],
434 root: &root,
435 };
436 f(&ctx);
437 }
438
439 #[cfg(miri)]
440 fn with_empty_ctx<F: FnOnce(&ResolveContext)>(_f: F) {
441 }
443
444 fn make_import(source: &str, imported: ImportedName, local: &str) -> ImportInfo {
445 ImportInfo {
446 source: source.to_string(),
447 imported_name: imported,
448 local_name: local.to_string(),
449 is_type_only: false,
450 span: dummy_span(),
451 source_span: Span::default(),
452 }
453 }
454
455 fn make_re_export(source: &str, imported: &str, exported: &str) -> ReExportInfo {
456 ReExportInfo {
457 source: source.to_string(),
458 imported_name: imported.to_string(),
459 exported_name: exported.to_string(),
460 is_type_only: false,
461 }
462 }
463
464 fn make_dynamic(
465 source: &str,
466 destructured: Vec<&str>,
467 local_name: Option<&str>,
468 ) -> DynamicImportInfo {
469 DynamicImportInfo {
470 source: source.to_string(),
471 span: dummy_span(),
472 destructured_names: destructured.into_iter().map(String::from).collect(),
473 local_name: local_name.map(String::from),
474 }
475 }
476
477 fn make_require(
478 source: &str,
479 destructured: Vec<&str>,
480 local_name: Option<&str>,
481 ) -> RequireCallInfo {
482 RequireCallInfo {
483 source: source.to_string(),
484 span: dummy_span(),
485 destructured_names: destructured.into_iter().map(String::from).collect(),
486 local_name: local_name.map(String::from),
487 }
488 }
489
490 fn make_resolved_module(
492 file_id: u32,
493 imports: Vec<ResolvedImport>,
494 dynamic_imports: Vec<ResolvedImport>,
495 re_exports: Vec<ResolvedReExport>,
496 ) -> ResolvedModule {
497 ResolvedModule {
498 file_id: FileId(file_id),
499 path: PathBuf::from(format!("/project/src/file_{file_id}.ts")),
500 exports: vec![],
501 re_exports,
502 resolved_imports: imports,
503 resolved_dynamic_imports: dynamic_imports,
504 resolved_dynamic_patterns: vec![],
505 member_accesses: vec![],
506 whole_object_uses: vec![],
507 has_cjs_exports: false,
508 unused_import_bindings: vec![],
509 }
510 }
511
512 fn make_resolved_import(source: &str, target: ResolveResult) -> ResolvedImport {
513 ResolvedImport {
514 info: make_import(source, ImportedName::Named("x".into()), "x"),
515 target,
516 }
517 }
518
519 fn make_resolved_re_export(source: &str, target: ResolveResult) -> ResolvedReExport {
520 ResolvedReExport {
521 info: make_re_export(source, "x", "x"),
522 target,
523 }
524 }
525
526 #[test]
531 fn static_imports_named() {
532 with_empty_ctx(|ctx| {
533 let imports = vec![make_import(
534 "react",
535 ImportedName::Named("useState".into()),
536 "useState",
537 )];
538 let file = Path::new("/project/src/app.ts");
539 let result = resolve_static_imports(ctx, file, &imports);
540
541 assert_eq!(result.len(), 1);
542 assert_eq!(result[0].info.source, "react");
543 assert!(matches!(
544 result[0].info.imported_name,
545 ImportedName::Named(ref n) if n == "useState"
546 ));
547 });
548 }
549
550 #[test]
551 fn static_imports_default() {
552 with_empty_ctx(|ctx| {
553 let imports = vec![make_import("react", ImportedName::Default, "React")];
554 let file = Path::new("/project/src/app.ts");
555 let result = resolve_static_imports(ctx, file, &imports);
556
557 assert_eq!(result.len(), 1);
558 assert!(matches!(
559 result[0].info.imported_name,
560 ImportedName::Default
561 ));
562 assert_eq!(result[0].info.local_name, "React");
563 });
564 }
565
566 #[test]
567 fn static_imports_namespace() {
568 with_empty_ctx(|ctx| {
569 let imports = vec![make_import("lodash", ImportedName::Namespace, "_")];
570 let file = Path::new("/project/src/utils.ts");
571 let result = resolve_static_imports(ctx, file, &imports);
572
573 assert_eq!(result.len(), 1);
574 assert!(matches!(
575 result[0].info.imported_name,
576 ImportedName::Namespace
577 ));
578 assert_eq!(result[0].info.local_name, "_");
579 });
580 }
581
582 #[test]
583 fn static_imports_side_effect() {
584 with_empty_ctx(|ctx| {
585 let imports = vec![make_import("./styles.css", ImportedName::SideEffect, "")];
586 let file = Path::new("/project/src/app.ts");
587 let result = resolve_static_imports(ctx, file, &imports);
588
589 assert_eq!(result.len(), 1);
590 assert!(matches!(
591 result[0].info.imported_name,
592 ImportedName::SideEffect
593 ));
594 assert_eq!(result[0].info.local_name, "");
595 });
596 }
597
598 #[test]
599 fn static_imports_empty_list() {
600 with_empty_ctx(|ctx| {
601 let file = Path::new("/project/src/app.ts");
602 let result = resolve_static_imports(ctx, file, &[]);
603 assert!(result.is_empty());
604 });
605 }
606
607 #[test]
608 fn static_imports_multiple() {
609 with_empty_ctx(|ctx| {
610 let imports = vec![
611 make_import("react", ImportedName::Default, "React"),
612 make_import("react", ImportedName::Named("useState".into()), "useState"),
613 make_import("lodash", ImportedName::Namespace, "_"),
614 ];
615 let file = Path::new("/project/src/app.ts");
616 let result = resolve_static_imports(ctx, file, &imports);
617
618 assert_eq!(result.len(), 3);
619 assert_eq!(result[0].info.source, "react");
620 assert_eq!(result[1].info.source, "react");
621 assert_eq!(result[2].info.source, "lodash");
622 });
623 }
624
625 #[test]
626 fn static_imports_preserves_type_only() {
627 with_empty_ctx(|ctx| {
628 let imports = vec![ImportInfo {
629 source: "react".into(),
630 imported_name: ImportedName::Named("FC".into()),
631 local_name: "FC".into(),
632 is_type_only: true,
633 span: dummy_span(),
634 source_span: Span::default(),
635 }];
636 let file = Path::new("/project/src/app.ts");
637 let result = resolve_static_imports(ctx, file, &imports);
638
639 assert_eq!(result.len(), 1);
640 assert!(result[0].info.is_type_only);
641 });
642 }
643
644 #[test]
649 fn dynamic_import_with_destructured_names() {
650 with_empty_ctx(|ctx| {
651 let imp = make_dynamic("./utils", vec!["foo", "bar"], None);
652 let file = Path::new("/project/src/app.ts");
653 let result = resolve_single_dynamic_import(ctx, file, &imp);
654
655 assert_eq!(result.len(), 2);
656 assert!(matches!(
657 result[0].info.imported_name,
658 ImportedName::Named(ref n) if n == "foo"
659 ));
660 assert_eq!(result[0].info.local_name, "foo");
661 assert!(matches!(
662 result[1].info.imported_name,
663 ImportedName::Named(ref n) if n == "bar"
664 ));
665 assert_eq!(result[1].info.local_name, "bar");
666 assert_eq!(result[0].info.source, "./utils");
668 assert_eq!(result[1].info.source, "./utils");
669 assert!(!result[0].info.is_type_only);
671 assert!(!result[1].info.is_type_only);
672 });
673 }
674
675 #[test]
676 fn dynamic_import_namespace_with_local_name() {
677 with_empty_ctx(|ctx| {
678 let imp = make_dynamic("./utils", vec![], Some("utils"));
679 let file = Path::new("/project/src/app.ts");
680 let result = resolve_single_dynamic_import(ctx, file, &imp);
681
682 assert_eq!(result.len(), 1);
683 assert!(matches!(
684 result[0].info.imported_name,
685 ImportedName::Namespace
686 ));
687 assert_eq!(result[0].info.local_name, "utils");
688 });
689 }
690
691 #[test]
692 fn dynamic_import_side_effect() {
693 with_empty_ctx(|ctx| {
694 let imp = make_dynamic("./polyfill", vec![], None);
695 let file = Path::new("/project/src/app.ts");
696 let result = resolve_single_dynamic_import(ctx, file, &imp);
697
698 assert_eq!(result.len(), 1);
699 assert!(matches!(
700 result[0].info.imported_name,
701 ImportedName::SideEffect
702 ));
703 assert_eq!(result[0].info.local_name, "");
704 assert_eq!(result[0].info.source, "./polyfill");
705 });
706 }
707
708 #[test]
709 fn dynamic_import_destructured_takes_priority_over_local_name() {
710 with_empty_ctx(|ctx| {
713 let imp = DynamicImportInfo {
714 source: "./mod".into(),
715 span: dummy_span(),
716 destructured_names: vec!["a".into()],
717 local_name: Some("mod".into()),
718 };
719 let file = Path::new("/project/src/app.ts");
720 let result = resolve_single_dynamic_import(ctx, file, &imp);
721
722 assert_eq!(result.len(), 1);
723 assert!(matches!(
724 result[0].info.imported_name,
725 ImportedName::Named(ref n) if n == "a"
726 ));
727 });
728 }
729
730 #[test]
735 fn dynamic_imports_flattens_multiple() {
736 with_empty_ctx(|ctx| {
737 let imports = vec![
738 make_dynamic("./a", vec!["x", "y"], None),
739 make_dynamic("./b", vec![], Some("b")),
740 make_dynamic("./c", vec![], None),
741 ];
742 let file = Path::new("/project/src/app.ts");
743 let result = resolve_dynamic_imports(ctx, file, &imports);
744
745 assert_eq!(result.len(), 4);
747 });
748 }
749
750 #[test]
751 fn dynamic_imports_empty_list() {
752 with_empty_ctx(|ctx| {
753 let file = Path::new("/project/src/app.ts");
754 let result = resolve_dynamic_imports(ctx, file, &[]);
755 assert!(result.is_empty());
756 });
757 }
758
759 #[test]
764 fn re_exports_maps_each_entry() {
765 with_empty_ctx(|ctx| {
766 let re_exports = vec![
767 make_re_export("./utils", "helper", "helper"),
768 make_re_export("./types", "*", "*"),
769 ];
770 let file = Path::new("/project/src/index.ts");
771 let result = resolve_re_exports(ctx, file, &re_exports);
772
773 assert_eq!(result.len(), 2);
774 assert_eq!(result[0].info.source, "./utils");
775 assert_eq!(result[0].info.imported_name, "helper");
776 assert_eq!(result[0].info.exported_name, "helper");
777 assert_eq!(result[1].info.source, "./types");
778 assert_eq!(result[1].info.imported_name, "*");
779 });
780 }
781
782 #[test]
783 fn re_exports_empty_list() {
784 with_empty_ctx(|ctx| {
785 let file = Path::new("/project/src/index.ts");
786 let result = resolve_re_exports(ctx, file, &[]);
787 assert!(result.is_empty());
788 });
789 }
790
791 #[test]
792 fn re_exports_preserves_type_only() {
793 with_empty_ctx(|ctx| {
794 let re_exports = vec![ReExportInfo {
795 source: "./types".into(),
796 imported_name: "MyType".into(),
797 exported_name: "MyType".into(),
798 is_type_only: true,
799 }];
800 let file = Path::new("/project/src/index.ts");
801 let result = resolve_re_exports(ctx, file, &re_exports);
802
803 assert_eq!(result.len(), 1);
804 assert!(result[0].info.is_type_only);
805 });
806 }
807
808 #[test]
813 fn require_namespace_without_destructuring() {
814 with_empty_ctx(|ctx| {
815 let req = make_require("fs", vec![], Some("fs"));
816 let file = Path::new("/project/src/app.js");
817 let result = resolve_single_require(ctx, file, &req);
818
819 assert_eq!(result.len(), 1);
820 assert!(matches!(
821 result[0].info.imported_name,
822 ImportedName::Namespace
823 ));
824 assert_eq!(result[0].info.local_name, "fs");
825 assert_eq!(result[0].info.source, "fs");
826 });
827 }
828
829 #[test]
830 fn require_namespace_without_local_name() {
831 with_empty_ctx(|ctx| {
832 let req = make_require("./side-effect", vec![], None);
833 let file = Path::new("/project/src/app.js");
834 let result = resolve_single_require(ctx, file, &req);
835
836 assert_eq!(result.len(), 1);
837 assert!(matches!(
838 result[0].info.imported_name,
839 ImportedName::Namespace
840 ));
841 assert_eq!(result[0].info.local_name, "");
843 });
844 }
845
846 #[test]
847 fn require_with_destructured_names() {
848 with_empty_ctx(|ctx| {
849 let req = make_require("path", vec!["join", "resolve"], None);
850 let file = Path::new("/project/src/app.js");
851 let result = resolve_single_require(ctx, file, &req);
852
853 assert_eq!(result.len(), 2);
854 assert!(matches!(
855 result[0].info.imported_name,
856 ImportedName::Named(ref n) if n == "join"
857 ));
858 assert_eq!(result[0].info.local_name, "join");
859 assert!(matches!(
860 result[1].info.imported_name,
861 ImportedName::Named(ref n) if n == "resolve"
862 ));
863 assert_eq!(result[1].info.local_name, "resolve");
864 assert_eq!(result[0].info.source, "path");
866 assert_eq!(result[1].info.source, "path");
867 });
868 }
869
870 #[test]
871 fn require_destructured_is_not_type_only() {
872 with_empty_ctx(|ctx| {
873 let req = make_require("path", vec!["join"], None);
874 let file = Path::new("/project/src/app.js");
875 let result = resolve_single_require(ctx, file, &req);
876
877 assert_eq!(result.len(), 1);
878 assert!(!result[0].info.is_type_only);
879 });
880 }
881
882 #[test]
887 fn require_imports_flattens_multiple() {
888 with_empty_ctx(|ctx| {
889 let reqs = vec![
890 make_require("fs", vec![], Some("fs")),
891 make_require("path", vec!["join", "resolve"], None),
892 ];
893 let file = Path::new("/project/src/app.js");
894 let result = resolve_require_imports(ctx, file, &reqs);
895
896 assert_eq!(result.len(), 3);
898 });
899 }
900
901 #[test]
902 fn require_imports_empty_list() {
903 with_empty_ctx(|ctx| {
904 let file = Path::new("/project/src/app.js");
905 let result = resolve_require_imports(ctx, file, &[]);
906 assert!(result.is_empty());
907 });
908 }
909
910 #[test]
915 fn specifier_upgrades_npm_to_internal() {
916 let mut modules = vec![
920 make_resolved_module(
921 0,
922 vec![make_resolved_import(
923 "preact/hooks",
924 ResolveResult::InternalModule(FileId(5)),
925 )],
926 vec![],
927 vec![],
928 ),
929 make_resolved_module(
930 1,
931 vec![make_resolved_import(
932 "preact/hooks",
933 ResolveResult::NpmPackage("preact".into()),
934 )],
935 vec![],
936 vec![],
937 ),
938 ];
939
940 apply_specifier_upgrades(&mut modules);
941
942 assert!(matches!(
943 modules[1].resolved_imports[0].target,
944 ResolveResult::InternalModule(FileId(5))
945 ));
946 }
947
948 #[test]
949 fn specifier_upgrades_noop_when_no_internal() {
950 let mut modules = vec![
952 make_resolved_module(
953 0,
954 vec![make_resolved_import(
955 "lodash",
956 ResolveResult::NpmPackage("lodash".into()),
957 )],
958 vec![],
959 vec![],
960 ),
961 make_resolved_module(
962 1,
963 vec![make_resolved_import(
964 "lodash",
965 ResolveResult::NpmPackage("lodash".into()),
966 )],
967 vec![],
968 vec![],
969 ),
970 ];
971
972 apply_specifier_upgrades(&mut modules);
973
974 assert!(matches!(
975 modules[0].resolved_imports[0].target,
976 ResolveResult::NpmPackage(_)
977 ));
978 assert!(matches!(
979 modules[1].resolved_imports[0].target,
980 ResolveResult::NpmPackage(_)
981 ));
982 }
983
984 #[test]
985 fn specifier_upgrades_empty_modules() {
986 let mut modules: Vec<ResolvedModule> = vec![];
987 apply_specifier_upgrades(&mut modules);
988 assert!(modules.is_empty());
989 }
990
991 #[test]
992 fn specifier_upgrades_skips_relative_specifiers() {
993 let mut modules = vec![
996 make_resolved_module(
997 0,
998 vec![make_resolved_import(
999 "./utils",
1000 ResolveResult::InternalModule(FileId(5)),
1001 )],
1002 vec![],
1003 vec![],
1004 ),
1005 make_resolved_module(
1006 1,
1007 vec![make_resolved_import(
1008 "./utils",
1009 ResolveResult::NpmPackage("utils".into()),
1010 )],
1011 vec![],
1012 vec![],
1013 ),
1014 ];
1015
1016 apply_specifier_upgrades(&mut modules);
1017
1018 assert!(matches!(
1020 modules[1].resolved_imports[0].target,
1021 ResolveResult::NpmPackage(_)
1022 ));
1023 }
1024
1025 #[test]
1026 fn specifier_upgrades_applies_to_dynamic_imports() {
1027 let mut modules = vec![
1028 make_resolved_module(
1029 0,
1030 vec![],
1031 vec![make_resolved_import(
1032 "preact/hooks",
1033 ResolveResult::InternalModule(FileId(5)),
1034 )],
1035 vec![],
1036 ),
1037 make_resolved_module(
1038 1,
1039 vec![],
1040 vec![make_resolved_import(
1041 "preact/hooks",
1042 ResolveResult::NpmPackage("preact".into()),
1043 )],
1044 vec![],
1045 ),
1046 ];
1047
1048 apply_specifier_upgrades(&mut modules);
1049
1050 assert!(matches!(
1051 modules[1].resolved_dynamic_imports[0].target,
1052 ResolveResult::InternalModule(FileId(5))
1053 ));
1054 }
1055
1056 #[test]
1057 fn specifier_upgrades_applies_to_re_exports() {
1058 let mut modules = vec![
1059 make_resolved_module(
1060 0,
1061 vec![],
1062 vec![],
1063 vec![make_resolved_re_export(
1064 "preact/hooks",
1065 ResolveResult::InternalModule(FileId(5)),
1066 )],
1067 ),
1068 make_resolved_module(
1069 1,
1070 vec![],
1071 vec![],
1072 vec![make_resolved_re_export(
1073 "preact/hooks",
1074 ResolveResult::NpmPackage("preact".into()),
1075 )],
1076 ),
1077 ];
1078
1079 apply_specifier_upgrades(&mut modules);
1080
1081 assert!(matches!(
1082 modules[1].re_exports[0].target,
1083 ResolveResult::InternalModule(FileId(5))
1084 ));
1085 }
1086
1087 #[test]
1088 fn specifier_upgrades_does_not_downgrade_internal() {
1089 let mut modules = vec![
1091 make_resolved_module(
1092 0,
1093 vec![make_resolved_import(
1094 "preact/hooks",
1095 ResolveResult::InternalModule(FileId(5)),
1096 )],
1097 vec![],
1098 vec![],
1099 ),
1100 make_resolved_module(
1101 1,
1102 vec![make_resolved_import(
1103 "preact/hooks",
1104 ResolveResult::InternalModule(FileId(5)),
1105 )],
1106 vec![],
1107 vec![],
1108 ),
1109 ];
1110
1111 apply_specifier_upgrades(&mut modules);
1112
1113 assert!(matches!(
1114 modules[0].resolved_imports[0].target,
1115 ResolveResult::InternalModule(FileId(5))
1116 ));
1117 assert!(matches!(
1118 modules[1].resolved_imports[0].target,
1119 ResolveResult::InternalModule(FileId(5))
1120 ));
1121 }
1122
1123 #[test]
1124 fn specifier_upgrades_first_internal_wins() {
1125 let mut modules = vec![
1128 make_resolved_module(
1129 0,
1130 vec![make_resolved_import(
1131 "shared-lib",
1132 ResolveResult::InternalModule(FileId(10)),
1133 )],
1134 vec![],
1135 vec![],
1136 ),
1137 make_resolved_module(
1138 1,
1139 vec![make_resolved_import(
1140 "shared-lib",
1141 ResolveResult::InternalModule(FileId(20)),
1142 )],
1143 vec![],
1144 vec![],
1145 ),
1146 make_resolved_module(
1147 2,
1148 vec![make_resolved_import(
1149 "shared-lib",
1150 ResolveResult::NpmPackage("shared-lib".into()),
1151 )],
1152 vec![],
1153 vec![],
1154 ),
1155 ];
1156
1157 apply_specifier_upgrades(&mut modules);
1158
1159 assert!(matches!(
1161 modules[2].resolved_imports[0].target,
1162 ResolveResult::InternalModule(FileId(10))
1163 ));
1164 }
1165
1166 #[test]
1167 fn specifier_upgrades_does_not_touch_unresolvable() {
1168 let mut modules = vec![
1171 make_resolved_module(
1172 0,
1173 vec![make_resolved_import(
1174 "my-lib",
1175 ResolveResult::InternalModule(FileId(1)),
1176 )],
1177 vec![],
1178 vec![],
1179 ),
1180 make_resolved_module(
1181 1,
1182 vec![ResolvedImport {
1183 info: make_import("my-lib", ImportedName::Default, "myLib"),
1184 target: ResolveResult::Unresolvable("my-lib".into()),
1185 }],
1186 vec![],
1187 vec![],
1188 ),
1189 ];
1190
1191 apply_specifier_upgrades(&mut modules);
1192
1193 assert!(matches!(
1195 modules[1].resolved_imports[0].target,
1196 ResolveResult::Unresolvable(_)
1197 ));
1198 }
1199
1200 #[test]
1201 fn specifier_upgrades_cross_import_and_re_export() {
1202 let mut modules = vec![
1205 make_resolved_module(
1206 0,
1207 vec![make_resolved_import(
1208 "@myorg/utils",
1209 ResolveResult::InternalModule(FileId(3)),
1210 )],
1211 vec![],
1212 vec![],
1213 ),
1214 make_resolved_module(
1215 1,
1216 vec![],
1217 vec![],
1218 vec![make_resolved_re_export(
1219 "@myorg/utils",
1220 ResolveResult::NpmPackage("@myorg/utils".into()),
1221 )],
1222 ),
1223 ];
1224
1225 apply_specifier_upgrades(&mut modules);
1226
1227 assert!(matches!(
1228 modules[1].re_exports[0].target,
1229 ResolveResult::InternalModule(FileId(3))
1230 ));
1231 }
1232
1233 #[test]
1238 fn dynamic_patterns_matches_files_in_dir() {
1239 let from_dir = Path::new("/project/src");
1240 let patterns = vec![DynamicImportPattern {
1241 prefix: "./locales/".into(),
1242 suffix: Some(".json".into()),
1243 span: dummy_span(),
1244 }];
1245 let canonical_paths = vec![
1246 PathBuf::from("/project/src/locales/en.json"),
1247 PathBuf::from("/project/src/locales/fr.json"),
1248 PathBuf::from("/project/src/utils.ts"),
1249 ];
1250 let files = vec![
1251 DiscoveredFile {
1252 id: FileId(0),
1253 path: PathBuf::from("/project/src/locales/en.json"),
1254 size_bytes: 100,
1255 },
1256 DiscoveredFile {
1257 id: FileId(1),
1258 path: PathBuf::from("/project/src/locales/fr.json"),
1259 size_bytes: 100,
1260 },
1261 DiscoveredFile {
1262 id: FileId(2),
1263 path: PathBuf::from("/project/src/utils.ts"),
1264 size_bytes: 100,
1265 },
1266 ];
1267
1268 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1269
1270 assert_eq!(result.len(), 1);
1271 assert_eq!(result[0].1.len(), 2);
1272 assert!(result[0].1.contains(&FileId(0)));
1273 assert!(result[0].1.contains(&FileId(1)));
1274 }
1275
1276 #[test]
1277 fn dynamic_patterns_no_matches_returns_empty() {
1278 let from_dir = Path::new("/project/src");
1279 let patterns = vec![DynamicImportPattern {
1280 prefix: "./locales/".into(),
1281 suffix: Some(".json".into()),
1282 span: dummy_span(),
1283 }];
1284 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1285 let files = vec![DiscoveredFile {
1286 id: FileId(0),
1287 path: PathBuf::from("/project/src/utils.ts"),
1288 size_bytes: 100,
1289 }];
1290
1291 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1292
1293 assert!(result.is_empty());
1294 }
1295
1296 #[test]
1297 fn dynamic_patterns_empty_patterns_list() {
1298 let from_dir = Path::new("/project/src");
1299 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1300 let files = vec![DiscoveredFile {
1301 id: FileId(0),
1302 path: PathBuf::from("/project/src/utils.ts"),
1303 size_bytes: 100,
1304 }];
1305
1306 let result = resolve_dynamic_patterns(from_dir, &[], &canonical_paths, &files);
1307 assert!(result.is_empty());
1308 }
1309
1310 #[test]
1311 fn dynamic_patterns_glob_prefix_passthrough() {
1312 let from_dir = Path::new("/project/src");
1313 let patterns = vec![DynamicImportPattern {
1314 prefix: "./**/*.ts".into(),
1315 suffix: None,
1316 span: dummy_span(),
1317 }];
1318 let canonical_paths = vec![
1319 PathBuf::from("/project/src/utils.ts"),
1320 PathBuf::from("/project/src/deep/nested.ts"),
1321 ];
1322 let files = vec![
1323 DiscoveredFile {
1324 id: FileId(0),
1325 path: PathBuf::from("/project/src/utils.ts"),
1326 size_bytes: 100,
1327 },
1328 DiscoveredFile {
1329 id: FileId(1),
1330 path: PathBuf::from("/project/src/deep/nested.ts"),
1331 size_bytes: 100,
1332 },
1333 ];
1334
1335 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1336
1337 assert_eq!(result.len(), 1);
1338 assert_eq!(result[0].1.len(), 2);
1339 }
1340
1341 #[test]
1346 fn static_import_unresolvable_relative_path() {
1347 with_empty_ctx(|ctx| {
1348 let imports = vec![make_import(
1349 "./nonexistent",
1350 ImportedName::Default,
1351 "missing",
1352 )];
1353 let file = Path::new("/project/src/app.ts");
1354 let result = resolve_static_imports(ctx, file, &imports);
1355
1356 assert_eq!(result.len(), 1);
1357 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1358 });
1359 }
1360
1361 #[test]
1362 fn static_import_bare_specifier_becomes_npm_package() {
1363 with_empty_ctx(|ctx| {
1364 let imports = vec![make_import("react", ImportedName::Default, "React")];
1365 let file = Path::new("/project/src/app.ts");
1366 let result = resolve_static_imports(ctx, file, &imports);
1367
1368 assert_eq!(result.len(), 1);
1369 assert!(matches!(
1370 result[0].target,
1371 ResolveResult::NpmPackage(ref pkg) if pkg == "react"
1372 ));
1373 });
1374 }
1375
1376 #[test]
1377 fn require_bare_specifier_becomes_npm_package() {
1378 with_empty_ctx(|ctx| {
1379 let req = make_require("express", vec![], Some("express"));
1380 let file = Path::new("/project/src/app.js");
1381 let result = resolve_single_require(ctx, file, &req);
1382
1383 assert_eq!(result.len(), 1);
1384 assert!(matches!(
1385 result[0].target,
1386 ResolveResult::NpmPackage(ref pkg) if pkg == "express"
1387 ));
1388 });
1389 }
1390
1391 #[test]
1392 fn dynamic_import_unresolvable() {
1393 with_empty_ctx(|ctx| {
1394 let imp = make_dynamic("./missing-module", vec![], None);
1395 let file = Path::new("/project/src/app.ts");
1396 let result = resolve_single_dynamic_import(ctx, file, &imp);
1397
1398 assert_eq!(result.len(), 1);
1399 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1400 });
1401 }
1402
1403 #[test]
1404 fn re_export_unresolvable() {
1405 with_empty_ctx(|ctx| {
1406 let re_exports = vec![make_re_export("./missing", "foo", "foo")];
1407 let file = Path::new("/project/src/index.ts");
1408 let result = resolve_re_exports(ctx, file, &re_exports);
1409
1410 assert_eq!(result.len(), 1);
1411 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1412 });
1413 }
1414}