1mod dynamic_imports;
19pub(crate) mod fallbacks;
20mod path_info;
21mod re_exports;
22mod react_native;
23mod require_imports;
24mod specifier;
25mod static_imports;
26#[cfg(test)]
27mod tests;
28mod types;
29mod upgrades;
30
31pub use fallbacks::extract_package_name_from_node_modules_path;
32pub use path_info::{
33 extract_package_name, is_bare_specifier, is_path_alias, is_valid_package_name,
34};
35pub use react_native::{PlatformFamilyKey, has_react_native_plugin, platform_family_key};
36pub use types::{
37 OUTPUT_DIRS, ResolveResult, ResolvedImport, ResolvedModule, ResolvedProject, ResolvedReExport,
38 ResolvedReplacedModuleTarget, ResolvedSourceEdge,
39};
40
41use std::sync::Arc;
42
43use std::path::{Path, PathBuf};
44use std::sync::Mutex;
45
46use rayon::prelude::*;
47use rustc_hash::{FxHashMap, FxHashSet};
48
49use fallow_config::{AutoImportKind, AutoImportRule};
50use fallow_types::discover::{DiscoveredFile, FileId};
51use fallow_types::extract::{ImportInfo, ImportedName, ModuleInfo, SemanticFact};
52use oxc_span::Span;
53
54use dynamic_imports::{GlobMatcherCache, resolve_dynamic_imports, resolve_dynamic_patterns};
55use re_exports::resolve_re_exports;
56use react_native::{build_condition_names, build_extensions, synthesize_platform_family_edges};
57use require_imports::resolve_require_imports;
58use specifier::{build_resolve_options, create_resolver};
59use static_imports::resolve_static_imports;
60use types::{DenoImportMapEntry, PackageManifestInfo, ResolveContext};
61use upgrades::{ResolvedVitestMockOperation, apply_specifier_upgrades};
62
63pub struct ResolveAllImportsInput<'a> {
65 pub modules: &'a [ModuleInfo],
67 pub files: &'a [DiscoveredFile],
69 pub workspaces: &'a [fallow_config::WorkspaceInfo],
71 pub active_plugins: &'a [String],
73 pub path_aliases: &'a [(String, String)],
75 pub auto_imports: &'a [AutoImportRule],
77 pub scss_include_paths: &'a [PathBuf],
79 pub static_dir_mappings: &'a [(PathBuf, String)],
81 pub framework_static_dir_mappings: &'a [(PathBuf, String)],
83 pub root: &'a Path,
85 pub extra_conditions: &'a [String],
87}
88
89pub struct ResolverSession {
100 resolver: oxc_resolver::Resolver,
101 style_resolver: oxc_resolver::Resolver,
102 extensions: Vec<String>,
103 condition_names: Vec<String>,
104 package_manifests: Vec<PackageManifestInfo>,
105 has_deno_import_maps: bool,
106 canonical_ws_roots: Vec<PathBuf>,
107 root_is_canonical: bool,
108}
109
110impl ResolverSession {
111 #[must_use]
119 pub fn new(input: &ResolveAllImportsInput<'_>) -> Self {
120 let canonical_ws_roots: Vec<PathBuf> = input
121 .workspaces
122 .par_iter()
123 .map(|ws| dunce::canonicalize(&ws.root).unwrap_or_else(|_| ws.root.clone()))
124 .collect();
125 let package_manifests = build_package_manifests(input, &canonical_ws_roots);
126 let has_deno_import_maps = package_manifests
127 .iter()
128 .any(|manifest| !manifest.deno_import_map.is_empty());
129 let root_is_canonical = dunce::canonicalize(input.root).is_ok_and(|c| c == input.root);
130
131 let extensions = build_extensions(input.active_plugins);
132 let condition_names = build_condition_names(input.active_plugins, input.extra_conditions);
133 let resolver = create_resolver(input.root, input.active_plugins, input.extra_conditions);
134 let mut style_conditions = input.extra_conditions.to_vec();
135 style_conditions.push("sass".to_string());
136 style_conditions.push("style".to_string());
137 let style_resolver = resolver.clone_with_options(build_resolve_options(
140 input.root,
141 input.active_plugins,
142 &style_conditions,
143 ));
144
145 Self {
146 resolver,
147 style_resolver,
148 extensions,
149 condition_names,
150 package_manifests,
151 has_deno_import_maps,
152 canonical_ws_roots,
153 root_is_canonical,
154 }
155 }
156}
157
158#[must_use]
160pub fn resolve_all_imports(input: &ResolveAllImportsInput<'_>) -> ResolvedProject {
161 let session = ResolverSession::new(input);
162 resolve_all_imports_with_session(input, &session)
163}
164
165#[must_use]
173pub fn resolve_all_imports_with_session(
174 input: &ResolveAllImportsInput<'_>,
175 session: &ResolverSession,
176) -> ResolvedProject {
177 let root_is_canonical = session.root_is_canonical;
178 let workspace_roots = build_workspace_roots(input.workspaces, &session.canonical_ws_roots);
179 let canonical_paths = build_canonical_file_paths(input.files, root_is_canonical);
180 let path_to_id = build_path_to_id(input.files, &canonical_paths, root_is_canonical);
181 let raw_path_to_id: FxHashMap<&Path, FileId> = input
182 .files
183 .iter()
184 .map(|f| (f.path.as_path(), f.id))
185 .collect();
186
187 let file_paths: Vec<&Path> = input.files.iter().map(|f| f.path.as_path()).collect();
188
189 let canonical_fallback = if root_is_canonical {
190 Some(types::CanonicalFallback::new(input.files))
191 } else {
192 None
193 };
194
195 let tsconfig_warned: Mutex<FxHashSet<String>> = Mutex::new(FxHashSet::default());
196 let tsconfig_cache = types::TsconfigCache::default();
197 let canonicalize_cache = types::CanonicalizeCache::default();
198 let glob_matcher_cache = GlobMatcherCache::default();
199
200 let ctx = ResolveContext {
201 resolver: &session.resolver,
202 style_resolver: &session.style_resolver,
203 extensions: &session.extensions,
204 path_to_id: &path_to_id,
205 raw_path_to_id: &raw_path_to_id,
206 workspace_roots: &workspace_roots,
207 package_manifests: &session.package_manifests,
208 has_deno_import_maps: session.has_deno_import_maps,
209 condition_names: &session.condition_names,
210 path_aliases: input.path_aliases,
211 scss_include_paths: input.scss_include_paths,
212 static_dir_mappings: input.static_dir_mappings,
213 framework_static_dir_mappings: input.framework_static_dir_mappings,
214 root: input.root,
215 canonical_fallback: canonical_fallback.as_ref(),
216 tsconfig_warned: &tsconfig_warned,
217 tsconfig_cache: &tsconfig_cache,
218 canonicalize_cache: &canonicalize_cache,
219 };
220
221 let resolved_outputs: Vec<ResolvedModuleOutput> = input
222 .modules
223 .par_iter()
224 .filter_map(|module| {
225 resolve_module_imports(
226 module,
227 &ctx,
228 &glob_matcher_cache,
229 &file_paths,
230 &canonical_paths,
231 input.files,
232 )
233 })
234 .collect();
235 let mut resolved = Vec::with_capacity(resolved_outputs.len());
236 let mut vitest_mock_operations = Vec::new();
237 for output in resolved_outputs {
238 resolved.push(output.module);
239 vitest_mock_operations.extend(output.vitest_mock_operations);
240 }
241
242 apply_specifier_upgrades(&mut resolved, &mut vitest_mock_operations);
243
244 synthesize_platform_family_edges(&mut resolved, input.files, input.active_plugins);
245
246 synthesize_auto_import_edges(
247 &mut resolved,
248 input.modules,
249 input.auto_imports,
250 &path_to_id,
251 &raw_path_to_id,
252 );
253
254 let replaced_module_targets = final_replaced_module_targets(vitest_mock_operations);
255
256 ResolvedProject {
257 modules: resolved,
258 replaced_module_targets,
259 }
260}
261
262fn build_workspace_roots<'a>(
263 workspaces: &'a [fallow_config::WorkspaceInfo],
264 canonical_ws_roots: &'a [PathBuf],
265) -> FxHashMap<&'a str, &'a Path> {
266 workspaces
267 .iter()
268 .zip(canonical_ws_roots.iter())
269 .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
270 .collect()
271}
272
273fn build_canonical_file_paths(files: &[DiscoveredFile], root_is_canonical: bool) -> Vec<PathBuf> {
274 if root_is_canonical {
275 return Vec::new();
276 }
277
278 files
279 .par_iter()
280 .map(|f| dunce::canonicalize(&f.path).unwrap_or_else(|_| f.path.clone()))
281 .collect()
282}
283
284fn build_package_manifests(
291 input: &ResolveAllImportsInput<'_>,
292 canonical_ws_roots: &[PathBuf],
293) -> Vec<PackageManifestInfo> {
294 let root_canonical =
295 dunce::canonicalize(input.root).unwrap_or_else(|_| input.root.to_path_buf());
296 let root_probe = fallow_config::probe_dir_manifest(input.root);
297 let root_import_map = merge_deno_import_maps(&[], input.root, root_probe.deno_import_map);
298 let mut package_manifests = Vec::new();
299 if let Ok(Some((_name, package_json, _deps))) = root_probe.manifest {
300 package_manifests.push(PackageManifestInfo {
301 root: input.root.to_path_buf(),
302 canonical_root: root_canonical,
303 name: package_json.name.clone(),
304 package_json,
305 deno_import_map: root_import_map.clone(),
306 });
307 }
308 for (ws, canonical_root) in input.workspaces.iter().zip(canonical_ws_roots.iter()) {
309 let ws_probe = fallow_config::probe_dir_manifest(&ws.root);
310 if let Ok(Some((_name, package_json, _deps))) = ws_probe.manifest {
311 package_manifests.push(PackageManifestInfo {
312 root: ws.root.clone(),
313 canonical_root: canonical_root.clone(),
314 name: package_json.name.clone().or_else(|| Some(ws.name.clone())),
315 package_json,
316 deno_import_map: merge_deno_import_maps(
317 &root_import_map,
318 &ws.root,
319 ws_probe.deno_import_map,
320 ),
321 });
322 }
323 }
324 package_manifests
325}
326
327fn merge_deno_import_maps(
333 inherited: &[DenoImportMapEntry],
334 package_root: &Path,
335 local: Option<(PathBuf, Vec<(String, String)>)>,
336) -> Vec<DenoImportMapEntry> {
337 let mut entries: FxHashMap<String, DenoImportMapEntry> = inherited
338 .iter()
339 .cloned()
340 .map(|entry| (entry.key.clone(), entry))
341 .collect();
342
343 if let Some((config_path, local_entries)) = local {
344 let declaring_dir = config_path.parent().unwrap_or(package_root).to_path_buf();
345 for (key, target) in local_entries {
346 entries.insert(
347 key.clone(),
348 DenoImportMapEntry {
349 key,
350 target,
351 declaring_dir: declaring_dir.clone(),
352 },
353 );
354 }
355 }
356
357 let mut entries: Vec<_> = entries.into_values().collect();
358 entries.sort_by(|a, b| a.key.cmp(&b.key));
359 entries
360}
361
362fn build_path_to_id<'a>(
365 files: &'a [DiscoveredFile],
366 canonical_paths: &'a [PathBuf],
367 root_is_canonical: bool,
368) -> FxHashMap<&'a Path, FileId> {
369 if root_is_canonical {
370 files.iter().map(|f| (f.path.as_path(), f.id)).collect()
371 } else {
372 canonical_paths
373 .iter()
374 .enumerate()
375 .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
376 .collect()
377 }
378}
379
380fn resolve_module_imports(
381 module: &ModuleInfo,
382 ctx: &ResolveContext<'_>,
383 glob_matcher_cache: &GlobMatcherCache,
384 file_paths: &[&Path],
385 canonical_paths: &[PathBuf],
386 files: &[DiscoveredFile],
387) -> Option<ResolvedModuleOutput> {
388 let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
389 tracing::warn!(
390 file_id = module.file_id.0,
391 "Skipping module with unknown file_id during resolution"
392 );
393 return None;
394 };
395
396 let mut all_imports = resolve_static_imports(ctx, file_path, &module.imports);
397 all_imports.extend(resolve_require_imports(
398 ctx,
399 file_path,
400 &module.require_calls,
401 ));
402
403 let from_dir = if canonical_paths.is_empty() {
404 file_path.parent().unwrap_or(file_path)
405 } else {
406 canonical_paths
407 .get(module.file_id.0 as usize)
408 .and_then(|p| p.parent())
409 .unwrap_or(file_path)
410 };
411
412 let vitest_mock_operations =
413 resolve_vitest_mock_operations(module.file_id, &module.semantic_facts, ctx, file_path);
414 let module = build_resolved_module(ResolvedModuleBuildInput {
415 module,
416 ctx,
417 glob_matcher_cache,
418 file_path,
419 from_dir,
420 canonical_paths,
421 files,
422 all_imports,
423 });
424
425 Some(ResolvedModuleOutput {
426 module,
427 vitest_mock_operations,
428 })
429}
430
431struct ResolvedModuleOutput {
432 module: ResolvedModule,
433 vitest_mock_operations: Vec<ResolvedVitestMockOperation>,
434}
435
436fn resolve_vitest_mock_operations(
437 source_file: FileId,
438 semantic_facts: &[SemanticFact],
439 ctx: &ResolveContext<'_>,
440 file_path: &Path,
441) -> Vec<ResolvedVitestMockOperation> {
442 let mut operations: Vec<_> = semantic_facts
443 .iter()
444 .filter_map(|fact| {
445 let SemanticFact::VitestModuleMockOperation(operation) = fact else {
446 return None;
447 };
448 Some(ResolvedVitestMockOperation {
449 source_file,
450 source_specifier: operation.source.clone(),
451 call_start: operation.call_start,
452 action: operation.action,
453 target: specifier::resolve_specifier(ctx, file_path, &operation.source, false),
454 })
455 })
456 .collect();
457 operations.sort_unstable_by(|left, right| {
458 left.call_start
459 .cmp(&right.call_start)
460 .then_with(|| left.source_specifier.cmp(&right.source_specifier))
461 });
462 operations
463}
464
465fn final_replaced_module_targets(
466 mut operations: Vec<ResolvedVitestMockOperation>,
467) -> Vec<ResolvedReplacedModuleTarget> {
468 operations.sort_unstable_by(|left, right| {
469 left.source_file
470 .0
471 .cmp(&right.source_file.0)
472 .then_with(|| left.call_start.cmp(&right.call_start))
473 .then_with(|| left.source_specifier.cmp(&right.source_specifier))
474 });
475
476 let mut final_operations = FxHashMap::default();
477 for operation in operations {
478 let Some(target_file) = operation.target.internal_file_id() else {
479 continue;
480 };
481 final_operations.insert((operation.source_file, target_file), operation.action);
482 }
483
484 let mut targets: Vec<_> = final_operations
485 .into_iter()
486 .filter_map(|((source_file, target_file), action)| {
487 action
488 .replaces_original()
489 .then_some(ResolvedReplacedModuleTarget {
490 source_file,
491 target_file,
492 })
493 })
494 .collect();
495 targets.sort_unstable_by_key(|target| (target.source_file.0, target.target_file.0));
496 targets
497}
498
499struct ResolvedModuleBuildInput<'a> {
500 module: &'a ModuleInfo,
501 ctx: &'a ResolveContext<'a>,
502 glob_matcher_cache: &'a GlobMatcherCache,
503 file_path: &'a Path,
504 from_dir: &'a Path,
505 canonical_paths: &'a [PathBuf],
506 files: &'a [DiscoveredFile],
507 all_imports: Vec<types::ResolvedImport>,
508}
509
510fn build_resolved_module(input: ResolvedModuleBuildInput<'_>) -> ResolvedModule {
511 ResolvedModule {
512 file_id: input.module.file_id,
513 path: input.file_path.to_path_buf(),
514 exports: Arc::clone(&input.module.exports),
515 re_exports: resolve_re_exports(input.ctx, input.file_path, &input.module.re_exports),
516 resolved_imports: input.all_imports,
517 resolved_dynamic_imports: resolve_dynamic_imports(
518 input.ctx,
519 input.file_path,
520 &input.module.dynamic_imports,
521 ),
522 resolved_dynamic_patterns: resolve_dynamic_patterns(
523 input.glob_matcher_cache,
524 input.from_dir,
525 &input.module.dynamic_import_patterns,
526 input.canonical_paths,
527 input.files,
528 ),
529 member_accesses: Arc::clone(&input.module.member_accesses),
530 semantic_facts: Arc::clone(&input.module.semantic_facts),
531 whole_object_uses: Arc::clone(&input.module.whole_object_uses),
532 has_cjs_exports: input.module.has_cjs_exports,
533 has_angular_component_template_url: input.module.has_angular_component_template_url,
534 unused_import_bindings: input
535 .module
536 .unused_import_bindings
537 .iter()
538 .cloned()
539 .collect(),
540 type_referenced_import_bindings: input.module.type_referenced_import_bindings.clone(),
541 value_referenced_import_bindings: input.module.value_referenced_import_bindings.clone(),
542 namespace_object_aliases: input.module.namespace_object_aliases.clone(),
543 exported_factory_returns: Arc::clone(&input.module.exported_factory_returns),
544 exported_factory_return_object_shapes: Arc::clone(
545 &input.module.exported_factory_return_object_shapes,
546 ),
547 type_member_types: Arc::clone(&input.module.type_member_types),
548 }
549}
550
551fn synthesize_auto_import_edges(
559 resolved: &mut [ResolvedModule],
560 modules: &[ModuleInfo],
561 auto_imports: &[AutoImportRule],
562 path_to_id: &FxHashMap<&Path, FileId>,
563 raw_path_to_id: &FxHashMap<&Path, FileId>,
564) {
565 if auto_imports.is_empty() {
566 return;
567 }
568
569 let mut table: FxHashMap<&str, Vec<(FileId, AutoImportKind)>> = FxHashMap::default();
570 for rule in auto_imports {
571 let source = rule.source.as_path();
572 let Some(file_id) = raw_path_to_id
573 .get(source)
574 .or_else(|| path_to_id.get(source))
575 .copied()
576 else {
577 continue;
578 };
579 table
580 .entry(rule.name.as_str())
581 .or_default()
582 .push((file_id, rule.kind));
583 }
584 if table.is_empty() {
585 return;
586 }
587
588 let candidates: FxHashMap<FileId, &[String]> = modules
589 .iter()
590 .filter(|module| !module.auto_import_candidates.is_empty())
591 .map(|module| (module.file_id, module.auto_import_candidates.as_slice()))
592 .collect();
593 if candidates.is_empty() {
594 return;
595 }
596
597 for module in resolved.iter_mut() {
598 let Some(names) = candidates.get(&module.file_id) else {
599 continue;
600 };
601 for name in *names {
602 if is_auto_import_builtin(name) {
603 continue;
604 }
605 let Some(targets) = table.get(name.as_str()) else {
606 continue;
607 };
608 for (target_id, kind) in targets {
609 if *target_id == module.file_id {
610 continue;
611 }
612 module.resolved_imports.push(ResolvedImport {
613 info: synthetic_auto_import_info(name, *kind),
614 target: ResolveResult::SyntheticAutoImport(*target_id),
615 });
616 }
617 }
618 }
619}
620
621fn is_auto_import_builtin(name: &str) -> bool {
622 is_js_auto_import_builtin(name)
623 || is_vue_auto_import_builtin(name)
624 || is_nuxt_auto_import_builtin(name)
625}
626
627fn is_js_auto_import_builtin(name: &str) -> bool {
628 matches!(
629 name,
630 "AbortController"
631 | "AbortSignal"
632 | "Array"
633 | "ArrayBuffer"
634 | "BigInt"
635 | "Blob"
636 | "Boolean"
637 | "Buffer"
638 | "CSS"
639 | "DOMParser"
640 | "Date"
641 | "Document"
642 | "Error"
643 | "Event"
644 | "EventTarget"
645 | "File"
646 | "FormData"
647 | "Intl"
648 | "JSON"
649 | "Map"
650 | "Math"
651 | "Number"
652 | "Object"
653 | "Promise"
654 | "Reflect"
655 | "RegExp"
656 | "Response"
657 | "Set"
658 | "String"
659 | "Symbol"
660 | "URL"
661 | "URLSearchParams"
662 | "WeakMap"
663 | "WeakSet"
664 | "Window"
665 | "alert"
666 | "clearInterval"
667 | "clearTimeout"
668 | "console"
669 | "document"
670 | "fetch"
671 | "global"
672 | "globalThis"
673 | "localStorage"
674 | "navigator"
675 | "process"
676 | "requestAnimationFrame"
677 | "sessionStorage"
678 | "setInterval"
679 | "setTimeout"
680 | "window"
681 )
682}
683
684fn is_vue_auto_import_builtin(name: &str) -> bool {
685 matches!(name, |"computed"| "customRef"
686 | "defineAsyncComponent"
687 | "defineComponent"
688 | "effectScope"
689 | "getCurrentInstance"
690 | "h"
691 | "inject"
692 | "isProxy"
693 | "isReactive"
694 | "isReadonly"
695 | "isRef"
696 | "markRaw"
697 | "nextTick"
698 | "onActivated"
699 | "onBeforeMount"
700 | "onBeforeUnmount"
701 | "onBeforeUpdate"
702 | "onDeactivated"
703 | "onErrorCaptured"
704 | "onMounted"
705 | "onRenderTracked"
706 | "onRenderTriggered"
707 | "onScopeDispose"
708 | "onServerPrefetch"
709 | "onUnmounted"
710 | "onUpdated"
711 | "provide"
712 | "reactive"
713 | "readonly"
714 | "ref"
715 | "resolveComponent"
716 | "shallowReactive"
717 | "shallowReadonly"
718 | "shallowRef"
719 | "toRaw"
720 | "toRef"
721 | "toRefs"
722 | "triggerRef"
723 | "unref"
724 | "watch"
725 | "watchEffect"
726 | "watchPostEffect"
727 | "watchSyncEffect")
728}
729
730fn is_nuxt_auto_import_builtin(name: &str) -> bool {
731 matches!(name, |"useAsyncData"| "useCookie"
732 | "useError"
733 | "useFetch"
734 | "useHead"
735 | "useLazyAsyncData"
736 | "useLazyFetch"
737 | "useNuxtApp"
738 | "useRequestEvent"
739 | "useRequestHeaders"
740 | "useRoute"
741 | "useRouter"
742 | "useRuntimeConfig"
743 | "useSeoMeta"
744 | "useState")
745}
746
747fn synthetic_auto_import_info(name: &str, kind: AutoImportKind) -> ImportInfo {
750 let imported_name = match kind {
751 AutoImportKind::Named => ImportedName::Named(name.to_string()),
752 AutoImportKind::Default | AutoImportKind::DefaultComponent => ImportedName::Default,
753 };
754 ImportInfo {
755 source: format!("<auto-import:{name}>"),
756 imported_name,
757 local_name: name.to_string(),
758 is_type_only: false,
759 is_type_only_star: false,
760 from_style: false,
761 span: Span::default(),
762 source_span: Span::default(),
763 }
764}