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