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 ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport, ResolvedSourceEdge,
37};
38
39use std::path::{Path, PathBuf};
40use std::sync::Mutex;
41
42use rayon::prelude::*;
43use rustc_hash::{FxHashMap, FxHashSet};
44
45use fallow_config::{AutoImportKind, AutoImportRule};
46use fallow_types::discover::{DiscoveredFile, FileId};
47use fallow_types::extract::{ImportInfo, ImportedName, ModuleInfo};
48use oxc_span::Span;
49
50use dynamic_imports::{resolve_dynamic_imports, resolve_dynamic_patterns};
51use re_exports::resolve_re_exports;
52use react_native::{build_condition_names, build_extensions};
53use require_imports::resolve_require_imports;
54use specifier::create_resolver;
55use static_imports::resolve_static_imports;
56use types::{PackageManifestInfo, ResolveContext};
57use upgrades::apply_specifier_upgrades;
58
59pub struct ResolveAllImportsInput<'a> {
61 pub modules: &'a [ModuleInfo],
63 pub files: &'a [DiscoveredFile],
65 pub workspaces: &'a [fallow_config::WorkspaceInfo],
67 pub active_plugins: &'a [String],
69 pub path_aliases: &'a [(String, String)],
71 pub auto_imports: &'a [AutoImportRule],
73 pub scss_include_paths: &'a [PathBuf],
75 pub static_dir_mappings: &'a [(PathBuf, String)],
77 pub root: &'a Path,
79 pub extra_conditions: &'a [String],
81}
82
83pub struct ResolverSession {
94 resolver: oxc_resolver::Resolver,
95 style_resolver: oxc_resolver::Resolver,
96 extensions: Vec<String>,
97 condition_names: Vec<String>,
98 package_manifests: Vec<PackageManifestInfo>,
99 canonical_ws_roots: Vec<PathBuf>,
100 root_is_canonical: bool,
101}
102
103impl ResolverSession {
104 #[must_use]
112 pub fn new(input: &ResolveAllImportsInput<'_>) -> Self {
113 let canonical_ws_roots: Vec<PathBuf> = input
114 .workspaces
115 .par_iter()
116 .map(|ws| dunce::canonicalize(&ws.root).unwrap_or_else(|_| ws.root.clone()))
117 .collect();
118 let package_manifests = build_package_manifests(input, &canonical_ws_roots);
119 let root_is_canonical = dunce::canonicalize(input.root).is_ok_and(|c| c == input.root);
120
121 let extensions = build_extensions(input.active_plugins);
122 let condition_names = build_condition_names(input.active_plugins, input.extra_conditions);
123 let resolver = create_resolver(input.active_plugins, input.extra_conditions);
124 let mut style_conditions = input.extra_conditions.to_vec();
125 style_conditions.push("sass".to_string());
126 style_conditions.push("style".to_string());
127 let style_resolver = create_resolver(input.active_plugins, &style_conditions);
128
129 Self {
130 resolver,
131 style_resolver,
132 extensions,
133 condition_names,
134 package_manifests,
135 canonical_ws_roots,
136 root_is_canonical,
137 }
138 }
139}
140
141#[must_use]
143pub fn resolve_all_imports(input: &ResolveAllImportsInput<'_>) -> Vec<ResolvedModule> {
144 let session = ResolverSession::new(input);
145 resolve_all_imports_with_session(input, &session)
146}
147
148#[must_use]
156pub fn resolve_all_imports_with_session(
157 input: &ResolveAllImportsInput<'_>,
158 session: &ResolverSession,
159) -> Vec<ResolvedModule> {
160 let root_is_canonical = session.root_is_canonical;
161 let workspace_roots = build_workspace_roots(input.workspaces, &session.canonical_ws_roots);
162 let canonical_paths = build_canonical_file_paths(input.files, root_is_canonical);
163 let path_to_id = build_path_to_id(input.files, &canonical_paths, root_is_canonical);
164 let raw_path_to_id: FxHashMap<&Path, FileId> = input
165 .files
166 .iter()
167 .map(|f| (f.path.as_path(), f.id))
168 .collect();
169
170 let file_paths: Vec<&Path> = input.files.iter().map(|f| f.path.as_path()).collect();
171
172 let canonical_fallback = if root_is_canonical {
173 Some(types::CanonicalFallback::new(input.files))
174 } else {
175 None
176 };
177
178 let tsconfig_warned: Mutex<FxHashSet<String>> = Mutex::new(FxHashSet::default());
179 let tsconfig_cache = types::TsconfigCache::default();
180 let canonicalize_cache = types::CanonicalizeCache::default();
181
182 let ctx = ResolveContext {
183 resolver: &session.resolver,
184 style_resolver: &session.style_resolver,
185 extensions: &session.extensions,
186 path_to_id: &path_to_id,
187 raw_path_to_id: &raw_path_to_id,
188 workspace_roots: &workspace_roots,
189 package_manifests: &session.package_manifests,
190 condition_names: &session.condition_names,
191 path_aliases: input.path_aliases,
192 scss_include_paths: input.scss_include_paths,
193 static_dir_mappings: input.static_dir_mappings,
194 root: input.root,
195 canonical_fallback: canonical_fallback.as_ref(),
196 tsconfig_warned: &tsconfig_warned,
197 tsconfig_cache: &tsconfig_cache,
198 canonicalize_cache: &canonicalize_cache,
199 };
200
201 let mut resolved: Vec<ResolvedModule> = input
202 .modules
203 .par_iter()
204 .filter_map(|module| {
205 resolve_module_imports(module, &ctx, &file_paths, &canonical_paths, input.files)
206 })
207 .collect();
208
209 apply_specifier_upgrades(&mut resolved);
210
211 synthesize_auto_import_edges(
212 &mut resolved,
213 input.modules,
214 input.auto_imports,
215 &path_to_id,
216 &raw_path_to_id,
217 );
218
219 resolved
220}
221
222fn build_workspace_roots<'a>(
223 workspaces: &'a [fallow_config::WorkspaceInfo],
224 canonical_ws_roots: &'a [PathBuf],
225) -> FxHashMap<&'a str, &'a Path> {
226 workspaces
227 .iter()
228 .zip(canonical_ws_roots.iter())
229 .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
230 .collect()
231}
232
233fn build_canonical_file_paths(files: &[DiscoveredFile], root_is_canonical: bool) -> Vec<PathBuf> {
234 if root_is_canonical {
235 return Vec::new();
236 }
237
238 files
239 .par_iter()
240 .map(|f| dunce::canonicalize(&f.path).unwrap_or_else(|_| f.path.clone()))
241 .collect()
242}
243
244fn build_package_manifests(
247 input: &ResolveAllImportsInput<'_>,
248 canonical_ws_roots: &[PathBuf],
249) -> Vec<PackageManifestInfo> {
250 let root_canonical =
251 dunce::canonicalize(input.root).unwrap_or_else(|_| input.root.to_path_buf());
252 let mut package_manifests = Vec::new();
253 if let Ok(package_json) = fallow_config::PackageJson::load(&input.root.join("package.json")) {
254 package_manifests.push(PackageManifestInfo {
255 root: input.root.to_path_buf(),
256 canonical_root: root_canonical,
257 name: package_json.name.clone(),
258 package_json,
259 });
260 }
261 for (ws, canonical_root) in input.workspaces.iter().zip(canonical_ws_roots.iter()) {
262 if let Ok(package_json) = fallow_config::PackageJson::load(&ws.root.join("package.json")) {
263 package_manifests.push(PackageManifestInfo {
264 root: ws.root.clone(),
265 canonical_root: canonical_root.clone(),
266 name: package_json.name.clone().or_else(|| Some(ws.name.clone())),
267 package_json,
268 });
269 }
270 }
271 package_manifests
272}
273
274fn build_path_to_id<'a>(
277 files: &'a [DiscoveredFile],
278 canonical_paths: &'a [PathBuf],
279 root_is_canonical: bool,
280) -> FxHashMap<&'a Path, FileId> {
281 if root_is_canonical {
282 files.iter().map(|f| (f.path.as_path(), f.id)).collect()
283 } else {
284 canonical_paths
285 .iter()
286 .enumerate()
287 .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
288 .collect()
289 }
290}
291
292fn resolve_module_imports(
293 module: &ModuleInfo,
294 ctx: &ResolveContext<'_>,
295 file_paths: &[&Path],
296 canonical_paths: &[PathBuf],
297 files: &[DiscoveredFile],
298) -> Option<ResolvedModule> {
299 let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
300 tracing::warn!(
301 file_id = module.file_id.0,
302 "Skipping module with unknown file_id during resolution"
303 );
304 return None;
305 };
306
307 let mut all_imports = resolve_static_imports(ctx, file_path, &module.imports);
308 all_imports.extend(resolve_require_imports(
309 ctx,
310 file_path,
311 &module.require_calls,
312 ));
313
314 let from_dir = if canonical_paths.is_empty() {
315 file_path.parent().unwrap_or(file_path)
316 } else {
317 canonical_paths
318 .get(module.file_id.0 as usize)
319 .and_then(|p| p.parent())
320 .unwrap_or(file_path)
321 };
322
323 Some(build_resolved_module(ResolvedModuleBuildInput {
324 module,
325 ctx,
326 file_path,
327 from_dir,
328 canonical_paths,
329 files,
330 all_imports,
331 }))
332}
333
334struct ResolvedModuleBuildInput<'a> {
335 module: &'a ModuleInfo,
336 ctx: &'a ResolveContext<'a>,
337 file_path: &'a Path,
338 from_dir: &'a Path,
339 canonical_paths: &'a [PathBuf],
340 files: &'a [DiscoveredFile],
341 all_imports: Vec<types::ResolvedImport>,
342}
343
344fn build_resolved_module(input: ResolvedModuleBuildInput<'_>) -> ResolvedModule {
345 ResolvedModule {
346 file_id: input.module.file_id,
347 path: input.file_path.to_path_buf(),
348 exports: input.module.exports.clone(),
349 re_exports: resolve_re_exports(input.ctx, input.file_path, &input.module.re_exports),
350 resolved_imports: input.all_imports,
351 resolved_dynamic_imports: resolve_dynamic_imports(
352 input.ctx,
353 input.file_path,
354 &input.module.dynamic_imports,
355 ),
356 resolved_dynamic_patterns: resolve_dynamic_patterns(
357 input.from_dir,
358 &input.module.dynamic_import_patterns,
359 input.canonical_paths,
360 input.files,
361 ),
362 member_accesses: input.module.member_accesses.clone(),
363 semantic_facts: input.module.semantic_facts.clone(),
364 whole_object_uses: input.module.whole_object_uses.clone(),
365 has_cjs_exports: input.module.has_cjs_exports,
366 has_angular_component_template_url: input.module.has_angular_component_template_url,
367 unused_import_bindings: input
368 .module
369 .unused_import_bindings
370 .iter()
371 .cloned()
372 .collect(),
373 type_referenced_import_bindings: input.module.type_referenced_import_bindings.clone(),
374 value_referenced_import_bindings: input.module.value_referenced_import_bindings.clone(),
375 namespace_object_aliases: input.module.namespace_object_aliases.clone(),
376 exported_factory_returns: input.module.exported_factory_returns.clone(),
377 }
378}
379
380fn synthesize_auto_import_edges(
388 resolved: &mut [ResolvedModule],
389 modules: &[ModuleInfo],
390 auto_imports: &[AutoImportRule],
391 path_to_id: &FxHashMap<&Path, FileId>,
392 raw_path_to_id: &FxHashMap<&Path, FileId>,
393) {
394 if auto_imports.is_empty() {
395 return;
396 }
397
398 let mut table: FxHashMap<&str, Vec<(FileId, AutoImportKind)>> = FxHashMap::default();
399 for rule in auto_imports {
400 let source = rule.source.as_path();
401 let Some(file_id) = raw_path_to_id
402 .get(source)
403 .or_else(|| path_to_id.get(source))
404 .copied()
405 else {
406 continue;
407 };
408 table
409 .entry(rule.name.as_str())
410 .or_default()
411 .push((file_id, rule.kind));
412 }
413 if table.is_empty() {
414 return;
415 }
416
417 let candidates: FxHashMap<FileId, &[String]> = modules
418 .iter()
419 .filter(|module| !module.auto_import_candidates.is_empty())
420 .map(|module| (module.file_id, module.auto_import_candidates.as_slice()))
421 .collect();
422 if candidates.is_empty() {
423 return;
424 }
425
426 for module in resolved.iter_mut() {
427 let Some(names) = candidates.get(&module.file_id) else {
428 continue;
429 };
430 for name in *names {
431 if is_auto_import_builtin(name) {
432 continue;
433 }
434 let Some(targets) = table.get(name.as_str()) else {
435 continue;
436 };
437 for (target_id, kind) in targets {
438 if *target_id == module.file_id {
439 continue;
440 }
441 module.resolved_imports.push(ResolvedImport {
442 info: synthetic_auto_import_info(name, *kind),
443 target: ResolveResult::SyntheticAutoImport(*target_id),
444 });
445 }
446 }
447 }
448}
449
450fn is_auto_import_builtin(name: &str) -> bool {
451 is_js_auto_import_builtin(name)
452 || is_vue_auto_import_builtin(name)
453 || is_nuxt_auto_import_builtin(name)
454}
455
456fn is_js_auto_import_builtin(name: &str) -> bool {
457 matches!(
458 name,
459 "AbortController"
460 | "AbortSignal"
461 | "Array"
462 | "ArrayBuffer"
463 | "BigInt"
464 | "Blob"
465 | "Boolean"
466 | "Buffer"
467 | "CSS"
468 | "DOMParser"
469 | "Date"
470 | "Document"
471 | "Error"
472 | "Event"
473 | "EventTarget"
474 | "File"
475 | "FormData"
476 | "Intl"
477 | "JSON"
478 | "Map"
479 | "Math"
480 | "Number"
481 | "Object"
482 | "Promise"
483 | "Reflect"
484 | "RegExp"
485 | "Response"
486 | "Set"
487 | "String"
488 | "Symbol"
489 | "URL"
490 | "URLSearchParams"
491 | "WeakMap"
492 | "WeakSet"
493 | "Window"
494 | "alert"
495 | "clearInterval"
496 | "clearTimeout"
497 | "console"
498 | "document"
499 | "fetch"
500 | "global"
501 | "globalThis"
502 | "localStorage"
503 | "navigator"
504 | "process"
505 | "requestAnimationFrame"
506 | "sessionStorage"
507 | "setInterval"
508 | "setTimeout"
509 | "window"
510 )
511}
512
513fn is_vue_auto_import_builtin(name: &str) -> bool {
514 matches!(name, |"computed"| "customRef"
515 | "defineAsyncComponent"
516 | "defineComponent"
517 | "effectScope"
518 | "getCurrentInstance"
519 | "h"
520 | "inject"
521 | "isProxy"
522 | "isReactive"
523 | "isReadonly"
524 | "isRef"
525 | "markRaw"
526 | "nextTick"
527 | "onActivated"
528 | "onBeforeMount"
529 | "onBeforeUnmount"
530 | "onBeforeUpdate"
531 | "onDeactivated"
532 | "onErrorCaptured"
533 | "onMounted"
534 | "onRenderTracked"
535 | "onRenderTriggered"
536 | "onScopeDispose"
537 | "onServerPrefetch"
538 | "onUnmounted"
539 | "onUpdated"
540 | "provide"
541 | "reactive"
542 | "readonly"
543 | "ref"
544 | "resolveComponent"
545 | "shallowReactive"
546 | "shallowReadonly"
547 | "shallowRef"
548 | "toRaw"
549 | "toRef"
550 | "toRefs"
551 | "triggerRef"
552 | "unref"
553 | "watch"
554 | "watchEffect"
555 | "watchPostEffect"
556 | "watchSyncEffect")
557}
558
559fn is_nuxt_auto_import_builtin(name: &str) -> bool {
560 matches!(name, |"useAsyncData"| "useCookie"
561 | "useError"
562 | "useFetch"
563 | "useHead"
564 | "useLazyAsyncData"
565 | "useLazyFetch"
566 | "useNuxtApp"
567 | "useRequestEvent"
568 | "useRequestHeaders"
569 | "useRoute"
570 | "useRouter"
571 | "useRuntimeConfig"
572 | "useSeoMeta"
573 | "useState")
574}
575
576fn synthetic_auto_import_info(name: &str, kind: AutoImportKind) -> ImportInfo {
579 let imported_name = match kind {
580 AutoImportKind::Named => ImportedName::Named(name.to_string()),
581 AutoImportKind::Default | AutoImportKind::DefaultComponent => ImportedName::Default,
582 };
583 ImportInfo {
584 source: format!("<auto-import:{name}>"),
585 imported_name,
586 local_name: name.to_string(),
587 is_type_only: false,
588 from_style: false,
589 span: Span::default(),
590 source_span: Span::default(),
591 }
592}