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