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 fn with_empty_ctx<F: FnOnce(&ResolveContext)>(f: F) {
419 let resolver = specifier::create_resolver(&[]);
420 let path_to_id = FxHashMap::default();
421 let raw_path_to_id = FxHashMap::default();
422 let workspace_roots = FxHashMap::default();
423 let root = PathBuf::from("/project");
424 let ctx = ResolveContext {
425 resolver: &resolver,
426 path_to_id: &path_to_id,
427 raw_path_to_id: &raw_path_to_id,
428 workspace_roots: &workspace_roots,
429 path_aliases: &[],
430 root: &root,
431 };
432 f(&ctx);
433 }
434
435 fn make_import(source: &str, imported: ImportedName, local: &str) -> ImportInfo {
436 ImportInfo {
437 source: source.to_string(),
438 imported_name: imported,
439 local_name: local.to_string(),
440 is_type_only: false,
441 span: dummy_span(),
442 source_span: Span::default(),
443 }
444 }
445
446 fn make_re_export(source: &str, imported: &str, exported: &str) -> ReExportInfo {
447 ReExportInfo {
448 source: source.to_string(),
449 imported_name: imported.to_string(),
450 exported_name: exported.to_string(),
451 is_type_only: false,
452 }
453 }
454
455 fn make_dynamic(
456 source: &str,
457 destructured: Vec<&str>,
458 local_name: Option<&str>,
459 ) -> DynamicImportInfo {
460 DynamicImportInfo {
461 source: source.to_string(),
462 span: dummy_span(),
463 destructured_names: destructured.into_iter().map(String::from).collect(),
464 local_name: local_name.map(String::from),
465 }
466 }
467
468 fn make_require(
469 source: &str,
470 destructured: Vec<&str>,
471 local_name: Option<&str>,
472 ) -> RequireCallInfo {
473 RequireCallInfo {
474 source: source.to_string(),
475 span: dummy_span(),
476 destructured_names: destructured.into_iter().map(String::from).collect(),
477 local_name: local_name.map(String::from),
478 }
479 }
480
481 fn make_resolved_module(
483 file_id: u32,
484 imports: Vec<ResolvedImport>,
485 dynamic_imports: Vec<ResolvedImport>,
486 re_exports: Vec<ResolvedReExport>,
487 ) -> ResolvedModule {
488 ResolvedModule {
489 file_id: FileId(file_id),
490 path: PathBuf::from(format!("/project/src/file_{file_id}.ts")),
491 exports: vec![],
492 re_exports,
493 resolved_imports: imports,
494 resolved_dynamic_imports: dynamic_imports,
495 resolved_dynamic_patterns: vec![],
496 member_accesses: vec![],
497 whole_object_uses: vec![],
498 has_cjs_exports: false,
499 unused_import_bindings: vec![],
500 }
501 }
502
503 fn make_resolved_import(source: &str, target: ResolveResult) -> ResolvedImport {
504 ResolvedImport {
505 info: make_import(source, ImportedName::Named("x".into()), "x"),
506 target,
507 }
508 }
509
510 fn make_resolved_re_export(source: &str, target: ResolveResult) -> ResolvedReExport {
511 ResolvedReExport {
512 info: make_re_export(source, "x", "x"),
513 target,
514 }
515 }
516
517 #[test]
522 fn static_imports_named() {
523 with_empty_ctx(|ctx| {
524 let imports = vec![make_import(
525 "react",
526 ImportedName::Named("useState".into()),
527 "useState",
528 )];
529 let file = Path::new("/project/src/app.ts");
530 let result = resolve_static_imports(ctx, file, &imports);
531
532 assert_eq!(result.len(), 1);
533 assert_eq!(result[0].info.source, "react");
534 assert!(matches!(
535 result[0].info.imported_name,
536 ImportedName::Named(ref n) if n == "useState"
537 ));
538 });
539 }
540
541 #[test]
542 fn static_imports_default() {
543 with_empty_ctx(|ctx| {
544 let imports = vec![make_import("react", ImportedName::Default, "React")];
545 let file = Path::new("/project/src/app.ts");
546 let result = resolve_static_imports(ctx, file, &imports);
547
548 assert_eq!(result.len(), 1);
549 assert!(matches!(
550 result[0].info.imported_name,
551 ImportedName::Default
552 ));
553 assert_eq!(result[0].info.local_name, "React");
554 });
555 }
556
557 #[test]
558 fn static_imports_namespace() {
559 with_empty_ctx(|ctx| {
560 let imports = vec![make_import("lodash", ImportedName::Namespace, "_")];
561 let file = Path::new("/project/src/utils.ts");
562 let result = resolve_static_imports(ctx, file, &imports);
563
564 assert_eq!(result.len(), 1);
565 assert!(matches!(
566 result[0].info.imported_name,
567 ImportedName::Namespace
568 ));
569 assert_eq!(result[0].info.local_name, "_");
570 });
571 }
572
573 #[test]
574 fn static_imports_side_effect() {
575 with_empty_ctx(|ctx| {
576 let imports = vec![make_import("./styles.css", ImportedName::SideEffect, "")];
577 let file = Path::new("/project/src/app.ts");
578 let result = resolve_static_imports(ctx, file, &imports);
579
580 assert_eq!(result.len(), 1);
581 assert!(matches!(
582 result[0].info.imported_name,
583 ImportedName::SideEffect
584 ));
585 assert_eq!(result[0].info.local_name, "");
586 });
587 }
588
589 #[test]
590 fn static_imports_empty_list() {
591 with_empty_ctx(|ctx| {
592 let file = Path::new("/project/src/app.ts");
593 let result = resolve_static_imports(ctx, file, &[]);
594 assert!(result.is_empty());
595 });
596 }
597
598 #[test]
599 fn static_imports_multiple() {
600 with_empty_ctx(|ctx| {
601 let imports = vec![
602 make_import("react", ImportedName::Default, "React"),
603 make_import("react", ImportedName::Named("useState".into()), "useState"),
604 make_import("lodash", ImportedName::Namespace, "_"),
605 ];
606 let file = Path::new("/project/src/app.ts");
607 let result = resolve_static_imports(ctx, file, &imports);
608
609 assert_eq!(result.len(), 3);
610 assert_eq!(result[0].info.source, "react");
611 assert_eq!(result[1].info.source, "react");
612 assert_eq!(result[2].info.source, "lodash");
613 });
614 }
615
616 #[test]
617 fn static_imports_preserves_type_only() {
618 with_empty_ctx(|ctx| {
619 let imports = vec![ImportInfo {
620 source: "react".into(),
621 imported_name: ImportedName::Named("FC".into()),
622 local_name: "FC".into(),
623 is_type_only: true,
624 span: dummy_span(),
625 source_span: Span::default(),
626 }];
627 let file = Path::new("/project/src/app.ts");
628 let result = resolve_static_imports(ctx, file, &imports);
629
630 assert_eq!(result.len(), 1);
631 assert!(result[0].info.is_type_only);
632 });
633 }
634
635 #[test]
640 fn dynamic_import_with_destructured_names() {
641 with_empty_ctx(|ctx| {
642 let imp = make_dynamic("./utils", vec!["foo", "bar"], None);
643 let file = Path::new("/project/src/app.ts");
644 let result = resolve_single_dynamic_import(ctx, file, &imp);
645
646 assert_eq!(result.len(), 2);
647 assert!(matches!(
648 result[0].info.imported_name,
649 ImportedName::Named(ref n) if n == "foo"
650 ));
651 assert_eq!(result[0].info.local_name, "foo");
652 assert!(matches!(
653 result[1].info.imported_name,
654 ImportedName::Named(ref n) if n == "bar"
655 ));
656 assert_eq!(result[1].info.local_name, "bar");
657 assert_eq!(result[0].info.source, "./utils");
659 assert_eq!(result[1].info.source, "./utils");
660 assert!(!result[0].info.is_type_only);
662 assert!(!result[1].info.is_type_only);
663 });
664 }
665
666 #[test]
667 fn dynamic_import_namespace_with_local_name() {
668 with_empty_ctx(|ctx| {
669 let imp = make_dynamic("./utils", vec![], Some("utils"));
670 let file = Path::new("/project/src/app.ts");
671 let result = resolve_single_dynamic_import(ctx, file, &imp);
672
673 assert_eq!(result.len(), 1);
674 assert!(matches!(
675 result[0].info.imported_name,
676 ImportedName::Namespace
677 ));
678 assert_eq!(result[0].info.local_name, "utils");
679 });
680 }
681
682 #[test]
683 fn dynamic_import_side_effect() {
684 with_empty_ctx(|ctx| {
685 let imp = make_dynamic("./polyfill", vec![], None);
686 let file = Path::new("/project/src/app.ts");
687 let result = resolve_single_dynamic_import(ctx, file, &imp);
688
689 assert_eq!(result.len(), 1);
690 assert!(matches!(
691 result[0].info.imported_name,
692 ImportedName::SideEffect
693 ));
694 assert_eq!(result[0].info.local_name, "");
695 assert_eq!(result[0].info.source, "./polyfill");
696 });
697 }
698
699 #[test]
700 fn dynamic_import_destructured_takes_priority_over_local_name() {
701 with_empty_ctx(|ctx| {
704 let imp = DynamicImportInfo {
705 source: "./mod".into(),
706 span: dummy_span(),
707 destructured_names: vec!["a".into()],
708 local_name: Some("mod".into()),
709 };
710 let file = Path::new("/project/src/app.ts");
711 let result = resolve_single_dynamic_import(ctx, file, &imp);
712
713 assert_eq!(result.len(), 1);
714 assert!(matches!(
715 result[0].info.imported_name,
716 ImportedName::Named(ref n) if n == "a"
717 ));
718 });
719 }
720
721 #[test]
726 fn dynamic_imports_flattens_multiple() {
727 with_empty_ctx(|ctx| {
728 let imports = vec![
729 make_dynamic("./a", vec!["x", "y"], None),
730 make_dynamic("./b", vec![], Some("b")),
731 make_dynamic("./c", vec![], None),
732 ];
733 let file = Path::new("/project/src/app.ts");
734 let result = resolve_dynamic_imports(ctx, file, &imports);
735
736 assert_eq!(result.len(), 4);
738 });
739 }
740
741 #[test]
742 fn dynamic_imports_empty_list() {
743 with_empty_ctx(|ctx| {
744 let file = Path::new("/project/src/app.ts");
745 let result = resolve_dynamic_imports(ctx, file, &[]);
746 assert!(result.is_empty());
747 });
748 }
749
750 #[test]
755 fn re_exports_maps_each_entry() {
756 with_empty_ctx(|ctx| {
757 let re_exports = vec![
758 make_re_export("./utils", "helper", "helper"),
759 make_re_export("./types", "*", "*"),
760 ];
761 let file = Path::new("/project/src/index.ts");
762 let result = resolve_re_exports(ctx, file, &re_exports);
763
764 assert_eq!(result.len(), 2);
765 assert_eq!(result[0].info.source, "./utils");
766 assert_eq!(result[0].info.imported_name, "helper");
767 assert_eq!(result[0].info.exported_name, "helper");
768 assert_eq!(result[1].info.source, "./types");
769 assert_eq!(result[1].info.imported_name, "*");
770 });
771 }
772
773 #[test]
774 fn re_exports_empty_list() {
775 with_empty_ctx(|ctx| {
776 let file = Path::new("/project/src/index.ts");
777 let result = resolve_re_exports(ctx, file, &[]);
778 assert!(result.is_empty());
779 });
780 }
781
782 #[test]
783 fn re_exports_preserves_type_only() {
784 with_empty_ctx(|ctx| {
785 let re_exports = vec![ReExportInfo {
786 source: "./types".into(),
787 imported_name: "MyType".into(),
788 exported_name: "MyType".into(),
789 is_type_only: true,
790 }];
791 let file = Path::new("/project/src/index.ts");
792 let result = resolve_re_exports(ctx, file, &re_exports);
793
794 assert_eq!(result.len(), 1);
795 assert!(result[0].info.is_type_only);
796 });
797 }
798
799 #[test]
804 fn require_namespace_without_destructuring() {
805 with_empty_ctx(|ctx| {
806 let req = make_require("fs", vec![], Some("fs"));
807 let file = Path::new("/project/src/app.js");
808 let result = resolve_single_require(ctx, file, &req);
809
810 assert_eq!(result.len(), 1);
811 assert!(matches!(
812 result[0].info.imported_name,
813 ImportedName::Namespace
814 ));
815 assert_eq!(result[0].info.local_name, "fs");
816 assert_eq!(result[0].info.source, "fs");
817 });
818 }
819
820 #[test]
821 fn require_namespace_without_local_name() {
822 with_empty_ctx(|ctx| {
823 let req = make_require("./side-effect", vec![], None);
824 let file = Path::new("/project/src/app.js");
825 let result = resolve_single_require(ctx, file, &req);
826
827 assert_eq!(result.len(), 1);
828 assert!(matches!(
829 result[0].info.imported_name,
830 ImportedName::Namespace
831 ));
832 assert_eq!(result[0].info.local_name, "");
834 });
835 }
836
837 #[test]
838 fn require_with_destructured_names() {
839 with_empty_ctx(|ctx| {
840 let req = make_require("path", vec!["join", "resolve"], None);
841 let file = Path::new("/project/src/app.js");
842 let result = resolve_single_require(ctx, file, &req);
843
844 assert_eq!(result.len(), 2);
845 assert!(matches!(
846 result[0].info.imported_name,
847 ImportedName::Named(ref n) if n == "join"
848 ));
849 assert_eq!(result[0].info.local_name, "join");
850 assert!(matches!(
851 result[1].info.imported_name,
852 ImportedName::Named(ref n) if n == "resolve"
853 ));
854 assert_eq!(result[1].info.local_name, "resolve");
855 assert_eq!(result[0].info.source, "path");
857 assert_eq!(result[1].info.source, "path");
858 });
859 }
860
861 #[test]
862 fn require_destructured_is_not_type_only() {
863 with_empty_ctx(|ctx| {
864 let req = make_require("path", vec!["join"], None);
865 let file = Path::new("/project/src/app.js");
866 let result = resolve_single_require(ctx, file, &req);
867
868 assert_eq!(result.len(), 1);
869 assert!(!result[0].info.is_type_only);
870 });
871 }
872
873 #[test]
878 fn require_imports_flattens_multiple() {
879 with_empty_ctx(|ctx| {
880 let reqs = vec![
881 make_require("fs", vec![], Some("fs")),
882 make_require("path", vec!["join", "resolve"], None),
883 ];
884 let file = Path::new("/project/src/app.js");
885 let result = resolve_require_imports(ctx, file, &reqs);
886
887 assert_eq!(result.len(), 3);
889 });
890 }
891
892 #[test]
893 fn require_imports_empty_list() {
894 with_empty_ctx(|ctx| {
895 let file = Path::new("/project/src/app.js");
896 let result = resolve_require_imports(ctx, file, &[]);
897 assert!(result.is_empty());
898 });
899 }
900
901 #[test]
906 fn specifier_upgrades_npm_to_internal() {
907 let mut modules = vec![
911 make_resolved_module(
912 0,
913 vec![make_resolved_import(
914 "preact/hooks",
915 ResolveResult::InternalModule(FileId(5)),
916 )],
917 vec![],
918 vec![],
919 ),
920 make_resolved_module(
921 1,
922 vec![make_resolved_import(
923 "preact/hooks",
924 ResolveResult::NpmPackage("preact".into()),
925 )],
926 vec![],
927 vec![],
928 ),
929 ];
930
931 apply_specifier_upgrades(&mut modules);
932
933 assert!(matches!(
934 modules[1].resolved_imports[0].target,
935 ResolveResult::InternalModule(FileId(5))
936 ));
937 }
938
939 #[test]
940 fn specifier_upgrades_noop_when_no_internal() {
941 let mut modules = vec![
943 make_resolved_module(
944 0,
945 vec![make_resolved_import(
946 "lodash",
947 ResolveResult::NpmPackage("lodash".into()),
948 )],
949 vec![],
950 vec![],
951 ),
952 make_resolved_module(
953 1,
954 vec![make_resolved_import(
955 "lodash",
956 ResolveResult::NpmPackage("lodash".into()),
957 )],
958 vec![],
959 vec![],
960 ),
961 ];
962
963 apply_specifier_upgrades(&mut modules);
964
965 assert!(matches!(
966 modules[0].resolved_imports[0].target,
967 ResolveResult::NpmPackage(_)
968 ));
969 assert!(matches!(
970 modules[1].resolved_imports[0].target,
971 ResolveResult::NpmPackage(_)
972 ));
973 }
974
975 #[test]
976 fn specifier_upgrades_empty_modules() {
977 let mut modules: Vec<ResolvedModule> = vec![];
978 apply_specifier_upgrades(&mut modules);
979 assert!(modules.is_empty());
980 }
981
982 #[test]
983 fn specifier_upgrades_skips_relative_specifiers() {
984 let mut modules = vec![
987 make_resolved_module(
988 0,
989 vec![make_resolved_import(
990 "./utils",
991 ResolveResult::InternalModule(FileId(5)),
992 )],
993 vec![],
994 vec![],
995 ),
996 make_resolved_module(
997 1,
998 vec![make_resolved_import(
999 "./utils",
1000 ResolveResult::NpmPackage("utils".into()),
1001 )],
1002 vec![],
1003 vec![],
1004 ),
1005 ];
1006
1007 apply_specifier_upgrades(&mut modules);
1008
1009 assert!(matches!(
1011 modules[1].resolved_imports[0].target,
1012 ResolveResult::NpmPackage(_)
1013 ));
1014 }
1015
1016 #[test]
1017 fn specifier_upgrades_applies_to_dynamic_imports() {
1018 let mut modules = vec![
1019 make_resolved_module(
1020 0,
1021 vec![],
1022 vec![make_resolved_import(
1023 "preact/hooks",
1024 ResolveResult::InternalModule(FileId(5)),
1025 )],
1026 vec![],
1027 ),
1028 make_resolved_module(
1029 1,
1030 vec![],
1031 vec![make_resolved_import(
1032 "preact/hooks",
1033 ResolveResult::NpmPackage("preact".into()),
1034 )],
1035 vec![],
1036 ),
1037 ];
1038
1039 apply_specifier_upgrades(&mut modules);
1040
1041 assert!(matches!(
1042 modules[1].resolved_dynamic_imports[0].target,
1043 ResolveResult::InternalModule(FileId(5))
1044 ));
1045 }
1046
1047 #[test]
1048 fn specifier_upgrades_applies_to_re_exports() {
1049 let mut modules = vec![
1050 make_resolved_module(
1051 0,
1052 vec![],
1053 vec![],
1054 vec![make_resolved_re_export(
1055 "preact/hooks",
1056 ResolveResult::InternalModule(FileId(5)),
1057 )],
1058 ),
1059 make_resolved_module(
1060 1,
1061 vec![],
1062 vec![],
1063 vec![make_resolved_re_export(
1064 "preact/hooks",
1065 ResolveResult::NpmPackage("preact".into()),
1066 )],
1067 ),
1068 ];
1069
1070 apply_specifier_upgrades(&mut modules);
1071
1072 assert!(matches!(
1073 modules[1].re_exports[0].target,
1074 ResolveResult::InternalModule(FileId(5))
1075 ));
1076 }
1077
1078 #[test]
1079 fn specifier_upgrades_does_not_downgrade_internal() {
1080 let mut modules = vec![
1082 make_resolved_module(
1083 0,
1084 vec![make_resolved_import(
1085 "preact/hooks",
1086 ResolveResult::InternalModule(FileId(5)),
1087 )],
1088 vec![],
1089 vec![],
1090 ),
1091 make_resolved_module(
1092 1,
1093 vec![make_resolved_import(
1094 "preact/hooks",
1095 ResolveResult::InternalModule(FileId(5)),
1096 )],
1097 vec![],
1098 vec![],
1099 ),
1100 ];
1101
1102 apply_specifier_upgrades(&mut modules);
1103
1104 assert!(matches!(
1105 modules[0].resolved_imports[0].target,
1106 ResolveResult::InternalModule(FileId(5))
1107 ));
1108 assert!(matches!(
1109 modules[1].resolved_imports[0].target,
1110 ResolveResult::InternalModule(FileId(5))
1111 ));
1112 }
1113
1114 #[test]
1115 fn specifier_upgrades_first_internal_wins() {
1116 let mut modules = vec![
1119 make_resolved_module(
1120 0,
1121 vec![make_resolved_import(
1122 "shared-lib",
1123 ResolveResult::InternalModule(FileId(10)),
1124 )],
1125 vec![],
1126 vec![],
1127 ),
1128 make_resolved_module(
1129 1,
1130 vec![make_resolved_import(
1131 "shared-lib",
1132 ResolveResult::InternalModule(FileId(20)),
1133 )],
1134 vec![],
1135 vec![],
1136 ),
1137 make_resolved_module(
1138 2,
1139 vec![make_resolved_import(
1140 "shared-lib",
1141 ResolveResult::NpmPackage("shared-lib".into()),
1142 )],
1143 vec![],
1144 vec![],
1145 ),
1146 ];
1147
1148 apply_specifier_upgrades(&mut modules);
1149
1150 assert!(matches!(
1152 modules[2].resolved_imports[0].target,
1153 ResolveResult::InternalModule(FileId(10))
1154 ));
1155 }
1156
1157 #[test]
1158 fn specifier_upgrades_does_not_touch_unresolvable() {
1159 let mut modules = vec![
1162 make_resolved_module(
1163 0,
1164 vec![make_resolved_import(
1165 "my-lib",
1166 ResolveResult::InternalModule(FileId(1)),
1167 )],
1168 vec![],
1169 vec![],
1170 ),
1171 make_resolved_module(
1172 1,
1173 vec![ResolvedImport {
1174 info: make_import("my-lib", ImportedName::Default, "myLib"),
1175 target: ResolveResult::Unresolvable("my-lib".into()),
1176 }],
1177 vec![],
1178 vec![],
1179 ),
1180 ];
1181
1182 apply_specifier_upgrades(&mut modules);
1183
1184 assert!(matches!(
1186 modules[1].resolved_imports[0].target,
1187 ResolveResult::Unresolvable(_)
1188 ));
1189 }
1190
1191 #[test]
1192 fn specifier_upgrades_cross_import_and_re_export() {
1193 let mut modules = vec![
1196 make_resolved_module(
1197 0,
1198 vec![make_resolved_import(
1199 "@myorg/utils",
1200 ResolveResult::InternalModule(FileId(3)),
1201 )],
1202 vec![],
1203 vec![],
1204 ),
1205 make_resolved_module(
1206 1,
1207 vec![],
1208 vec![],
1209 vec![make_resolved_re_export(
1210 "@myorg/utils",
1211 ResolveResult::NpmPackage("@myorg/utils".into()),
1212 )],
1213 ),
1214 ];
1215
1216 apply_specifier_upgrades(&mut modules);
1217
1218 assert!(matches!(
1219 modules[1].re_exports[0].target,
1220 ResolveResult::InternalModule(FileId(3))
1221 ));
1222 }
1223
1224 #[test]
1229 fn dynamic_patterns_matches_files_in_dir() {
1230 let from_dir = Path::new("/project/src");
1231 let patterns = vec![DynamicImportPattern {
1232 prefix: "./locales/".into(),
1233 suffix: Some(".json".into()),
1234 span: dummy_span(),
1235 }];
1236 let canonical_paths = vec![
1237 PathBuf::from("/project/src/locales/en.json"),
1238 PathBuf::from("/project/src/locales/fr.json"),
1239 PathBuf::from("/project/src/utils.ts"),
1240 ];
1241 let files = vec![
1242 DiscoveredFile {
1243 id: FileId(0),
1244 path: PathBuf::from("/project/src/locales/en.json"),
1245 size_bytes: 100,
1246 },
1247 DiscoveredFile {
1248 id: FileId(1),
1249 path: PathBuf::from("/project/src/locales/fr.json"),
1250 size_bytes: 100,
1251 },
1252 DiscoveredFile {
1253 id: FileId(2),
1254 path: PathBuf::from("/project/src/utils.ts"),
1255 size_bytes: 100,
1256 },
1257 ];
1258
1259 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1260
1261 assert_eq!(result.len(), 1);
1262 assert_eq!(result[0].1.len(), 2);
1263 assert!(result[0].1.contains(&FileId(0)));
1264 assert!(result[0].1.contains(&FileId(1)));
1265 }
1266
1267 #[test]
1268 fn dynamic_patterns_no_matches_returns_empty() {
1269 let from_dir = Path::new("/project/src");
1270 let patterns = vec![DynamicImportPattern {
1271 prefix: "./locales/".into(),
1272 suffix: Some(".json".into()),
1273 span: dummy_span(),
1274 }];
1275 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1276 let files = vec![DiscoveredFile {
1277 id: FileId(0),
1278 path: PathBuf::from("/project/src/utils.ts"),
1279 size_bytes: 100,
1280 }];
1281
1282 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1283
1284 assert!(result.is_empty());
1285 }
1286
1287 #[test]
1288 fn dynamic_patterns_empty_patterns_list() {
1289 let from_dir = Path::new("/project/src");
1290 let canonical_paths = vec![PathBuf::from("/project/src/utils.ts")];
1291 let files = vec![DiscoveredFile {
1292 id: FileId(0),
1293 path: PathBuf::from("/project/src/utils.ts"),
1294 size_bytes: 100,
1295 }];
1296
1297 let result = resolve_dynamic_patterns(from_dir, &[], &canonical_paths, &files);
1298 assert!(result.is_empty());
1299 }
1300
1301 #[test]
1302 fn dynamic_patterns_glob_prefix_passthrough() {
1303 let from_dir = Path::new("/project/src");
1304 let patterns = vec![DynamicImportPattern {
1305 prefix: "./**/*.ts".into(),
1306 suffix: None,
1307 span: dummy_span(),
1308 }];
1309 let canonical_paths = vec![
1310 PathBuf::from("/project/src/utils.ts"),
1311 PathBuf::from("/project/src/deep/nested.ts"),
1312 ];
1313 let files = vec![
1314 DiscoveredFile {
1315 id: FileId(0),
1316 path: PathBuf::from("/project/src/utils.ts"),
1317 size_bytes: 100,
1318 },
1319 DiscoveredFile {
1320 id: FileId(1),
1321 path: PathBuf::from("/project/src/deep/nested.ts"),
1322 size_bytes: 100,
1323 },
1324 ];
1325
1326 let result = resolve_dynamic_patterns(from_dir, &patterns, &canonical_paths, &files);
1327
1328 assert_eq!(result.len(), 1);
1329 assert_eq!(result[0].1.len(), 2);
1330 }
1331
1332 #[test]
1337 fn static_import_unresolvable_relative_path() {
1338 with_empty_ctx(|ctx| {
1339 let imports = vec![make_import(
1340 "./nonexistent",
1341 ImportedName::Default,
1342 "missing",
1343 )];
1344 let file = Path::new("/project/src/app.ts");
1345 let result = resolve_static_imports(ctx, file, &imports);
1346
1347 assert_eq!(result.len(), 1);
1348 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1349 });
1350 }
1351
1352 #[test]
1353 fn static_import_bare_specifier_becomes_npm_package() {
1354 with_empty_ctx(|ctx| {
1355 let imports = vec![make_import("react", ImportedName::Default, "React")];
1356 let file = Path::new("/project/src/app.ts");
1357 let result = resolve_static_imports(ctx, file, &imports);
1358
1359 assert_eq!(result.len(), 1);
1360 assert!(matches!(
1361 result[0].target,
1362 ResolveResult::NpmPackage(ref pkg) if pkg == "react"
1363 ));
1364 });
1365 }
1366
1367 #[test]
1368 fn require_bare_specifier_becomes_npm_package() {
1369 with_empty_ctx(|ctx| {
1370 let req = make_require("express", vec![], Some("express"));
1371 let file = Path::new("/project/src/app.js");
1372 let result = resolve_single_require(ctx, file, &req);
1373
1374 assert_eq!(result.len(), 1);
1375 assert!(matches!(
1376 result[0].target,
1377 ResolveResult::NpmPackage(ref pkg) if pkg == "express"
1378 ));
1379 });
1380 }
1381
1382 #[test]
1383 fn dynamic_import_unresolvable() {
1384 with_empty_ctx(|ctx| {
1385 let imp = make_dynamic("./missing-module", vec![], None);
1386 let file = Path::new("/project/src/app.ts");
1387 let result = resolve_single_dynamic_import(ctx, file, &imp);
1388
1389 assert_eq!(result.len(), 1);
1390 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1391 });
1392 }
1393
1394 #[test]
1395 fn re_export_unresolvable() {
1396 with_empty_ctx(|ctx| {
1397 let re_exports = vec![make_re_export("./missing", "foo", "foo")];
1398 let file = Path::new("/project/src/index.ts");
1399 let result = resolve_re_exports(ctx, file, &re_exports);
1400
1401 assert_eq!(result.len(), 1);
1402 assert!(matches!(result[0].target, ResolveResult::Unresolvable(_)));
1403 });
1404 }
1405}