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