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
83#[must_use]
85pub fn resolve_all_imports(input: &ResolveAllImportsInput<'_>) -> Vec<ResolvedModule> {
86 let canonical_ws_roots: Vec<PathBuf> = input
87 .workspaces
88 .par_iter()
89 .map(|ws| dunce::canonicalize(&ws.root).unwrap_or_else(|_| ws.root.clone()))
90 .collect();
91 let workspace_roots: FxHashMap<&str, &Path> = input
92 .workspaces
93 .iter()
94 .zip(canonical_ws_roots.iter())
95 .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
96 .collect();
97 let root_canonical =
98 dunce::canonicalize(input.root).unwrap_or_else(|_| input.root.to_path_buf());
99 let mut package_manifests = Vec::new();
100 if let Ok(package_json) = fallow_config::PackageJson::load(&input.root.join("package.json")) {
101 package_manifests.push(PackageManifestInfo {
102 root: input.root.to_path_buf(),
103 canonical_root: root_canonical,
104 name: package_json.name.clone(),
105 package_json,
106 });
107 }
108 for (ws, canonical_root) in input.workspaces.iter().zip(canonical_ws_roots.iter()) {
109 if let Ok(package_json) = fallow_config::PackageJson::load(&ws.root.join("package.json")) {
110 package_manifests.push(PackageManifestInfo {
111 root: ws.root.clone(),
112 canonical_root: canonical_root.clone(),
113 name: package_json.name.clone().or_else(|| Some(ws.name.clone())),
114 package_json,
115 });
116 }
117 }
118
119 let root_is_canonical = dunce::canonicalize(input.root).is_ok_and(|c| c == input.root);
120
121 let canonical_paths: Vec<PathBuf> = if root_is_canonical {
122 Vec::new()
123 } else {
124 input
125 .files
126 .par_iter()
127 .map(|f| dunce::canonicalize(&f.path).unwrap_or_else(|_| f.path.clone()))
128 .collect()
129 };
130
131 let path_to_id: FxHashMap<&Path, FileId> = if root_is_canonical {
132 input
133 .files
134 .iter()
135 .map(|f| (f.path.as_path(), f.id))
136 .collect()
137 } else {
138 canonical_paths
139 .iter()
140 .enumerate()
141 .map(|(idx, canonical)| (canonical.as_path(), input.files[idx].id))
142 .collect()
143 };
144
145 let raw_path_to_id: FxHashMap<&Path, FileId> = input
146 .files
147 .iter()
148 .map(|f| (f.path.as_path(), f.id))
149 .collect();
150
151 let file_paths: Vec<&Path> = input.files.iter().map(|f| f.path.as_path()).collect();
152
153 let extensions = build_extensions(input.active_plugins);
154 let condition_names = build_condition_names(input.active_plugins, input.extra_conditions);
155 let resolver = create_resolver(input.active_plugins, input.extra_conditions);
156 let mut style_conditions = input.extra_conditions.to_vec();
157 style_conditions.push("sass".to_string());
158 style_conditions.push("style".to_string());
159 let style_resolver = create_resolver(input.active_plugins, &style_conditions);
160
161 let canonical_fallback = if root_is_canonical {
162 Some(types::CanonicalFallback::new(input.files))
163 } else {
164 None
165 };
166
167 let tsconfig_warned: Mutex<FxHashSet<String>> = Mutex::new(FxHashSet::default());
168
169 let ctx = ResolveContext {
170 resolver: &resolver,
171 style_resolver: &style_resolver,
172 extensions: &extensions,
173 path_to_id: &path_to_id,
174 raw_path_to_id: &raw_path_to_id,
175 workspace_roots: &workspace_roots,
176 package_manifests: &package_manifests,
177 condition_names: &condition_names,
178 path_aliases: input.path_aliases,
179 scss_include_paths: input.scss_include_paths,
180 static_dir_mappings: input.static_dir_mappings,
181 root: input.root,
182 canonical_fallback: canonical_fallback.as_ref(),
183 tsconfig_warned: &tsconfig_warned,
184 };
185
186 let mut resolved: Vec<ResolvedModule> = input
187 .modules
188 .par_iter()
189 .filter_map(|module| {
190 resolve_module_imports(module, &ctx, &file_paths, &canonical_paths, input.files)
191 })
192 .collect();
193
194 apply_specifier_upgrades(&mut resolved);
195
196 synthesize_auto_import_edges(
197 &mut resolved,
198 input.modules,
199 input.auto_imports,
200 &path_to_id,
201 &raw_path_to_id,
202 );
203
204 resolved
205}
206
207fn resolve_module_imports(
208 module: &ModuleInfo,
209 ctx: &ResolveContext<'_>,
210 file_paths: &[&Path],
211 canonical_paths: &[PathBuf],
212 files: &[DiscoveredFile],
213) -> Option<ResolvedModule> {
214 let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
215 tracing::warn!(
216 file_id = module.file_id.0,
217 "Skipping module with unknown file_id during resolution"
218 );
219 return None;
220 };
221
222 let mut all_imports = resolve_static_imports(ctx, file_path, &module.imports);
223 all_imports.extend(resolve_require_imports(
224 ctx,
225 file_path,
226 &module.require_calls,
227 ));
228
229 let from_dir = if canonical_paths.is_empty() {
230 file_path.parent().unwrap_or(file_path)
231 } else {
232 canonical_paths
233 .get(module.file_id.0 as usize)
234 .and_then(|p| p.parent())
235 .unwrap_or(file_path)
236 };
237
238 Some(build_resolved_module(ResolvedModuleBuildInput {
239 module,
240 ctx,
241 file_path,
242 from_dir,
243 canonical_paths,
244 files,
245 all_imports,
246 }))
247}
248
249struct ResolvedModuleBuildInput<'a> {
250 module: &'a ModuleInfo,
251 ctx: &'a ResolveContext<'a>,
252 file_path: &'a Path,
253 from_dir: &'a Path,
254 canonical_paths: &'a [PathBuf],
255 files: &'a [DiscoveredFile],
256 all_imports: Vec<types::ResolvedImport>,
257}
258
259fn build_resolved_module(input: ResolvedModuleBuildInput<'_>) -> ResolvedModule {
260 ResolvedModule {
261 file_id: input.module.file_id,
262 path: input.file_path.to_path_buf(),
263 exports: input.module.exports.clone(),
264 re_exports: resolve_re_exports(input.ctx, input.file_path, &input.module.re_exports),
265 resolved_imports: input.all_imports,
266 resolved_dynamic_imports: resolve_dynamic_imports(
267 input.ctx,
268 input.file_path,
269 &input.module.dynamic_imports,
270 ),
271 resolved_dynamic_patterns: resolve_dynamic_patterns(
272 input.from_dir,
273 &input.module.dynamic_import_patterns,
274 input.canonical_paths,
275 input.files,
276 ),
277 member_accesses: input.module.member_accesses.clone(),
278 whole_object_uses: input.module.whole_object_uses.clone(),
279 has_cjs_exports: input.module.has_cjs_exports,
280 has_angular_component_template_url: input.module.has_angular_component_template_url,
281 unused_import_bindings: input
282 .module
283 .unused_import_bindings
284 .iter()
285 .cloned()
286 .collect(),
287 type_referenced_import_bindings: input.module.type_referenced_import_bindings.clone(),
288 value_referenced_import_bindings: input.module.value_referenced_import_bindings.clone(),
289 namespace_object_aliases: input.module.namespace_object_aliases.clone(),
290 }
291}
292
293fn synthesize_auto_import_edges(
301 resolved: &mut [ResolvedModule],
302 modules: &[ModuleInfo],
303 auto_imports: &[AutoImportRule],
304 path_to_id: &FxHashMap<&Path, FileId>,
305 raw_path_to_id: &FxHashMap<&Path, FileId>,
306) {
307 if auto_imports.is_empty() {
308 return;
309 }
310
311 let mut table: FxHashMap<&str, Vec<(FileId, AutoImportKind)>> = FxHashMap::default();
312 for rule in auto_imports {
313 let source = rule.source.as_path();
314 let Some(file_id) = raw_path_to_id
315 .get(source)
316 .or_else(|| path_to_id.get(source))
317 .copied()
318 else {
319 continue;
320 };
321 table
322 .entry(rule.name.as_str())
323 .or_default()
324 .push((file_id, rule.kind));
325 }
326 if table.is_empty() {
327 return;
328 }
329
330 let candidates: FxHashMap<FileId, &[String]> = modules
331 .iter()
332 .filter(|module| !module.auto_import_candidates.is_empty())
333 .map(|module| (module.file_id, module.auto_import_candidates.as_slice()))
334 .collect();
335 if candidates.is_empty() {
336 return;
337 }
338
339 for module in resolved.iter_mut() {
340 let Some(names) = candidates.get(&module.file_id) else {
341 continue;
342 };
343 for name in *names {
344 if is_auto_import_builtin(name) {
345 continue;
346 }
347 let Some(targets) = table.get(name.as_str()) else {
348 continue;
349 };
350 for (target_id, kind) in targets {
351 if *target_id == module.file_id {
352 continue;
353 }
354 module.resolved_imports.push(ResolvedImport {
355 info: synthetic_auto_import_info(name, *kind),
356 target: ResolveResult::InternalModule(*target_id),
357 });
358 }
359 }
360 }
361}
362
363fn is_auto_import_builtin(name: &str) -> bool {
364 is_js_auto_import_builtin(name)
365 || is_vue_auto_import_builtin(name)
366 || is_nuxt_auto_import_builtin(name)
367}
368
369fn is_js_auto_import_builtin(name: &str) -> bool {
370 matches!(
371 name,
372 "AbortController"
373 | "AbortSignal"
374 | "Array"
375 | "ArrayBuffer"
376 | "BigInt"
377 | "Blob"
378 | "Boolean"
379 | "Buffer"
380 | "CSS"
381 | "DOMParser"
382 | "Date"
383 | "Document"
384 | "Error"
385 | "Event"
386 | "EventTarget"
387 | "File"
388 | "FormData"
389 | "Intl"
390 | "JSON"
391 | "Map"
392 | "Math"
393 | "Number"
394 | "Object"
395 | "Promise"
396 | "Reflect"
397 | "RegExp"
398 | "Response"
399 | "Set"
400 | "String"
401 | "Symbol"
402 | "URL"
403 | "URLSearchParams"
404 | "WeakMap"
405 | "WeakSet"
406 | "Window"
407 | "alert"
408 | "clearInterval"
409 | "clearTimeout"
410 | "console"
411 | "document"
412 | "fetch"
413 | "global"
414 | "globalThis"
415 | "localStorage"
416 | "navigator"
417 | "process"
418 | "requestAnimationFrame"
419 | "sessionStorage"
420 | "setInterval"
421 | "setTimeout"
422 | "window"
423 )
424}
425
426fn is_vue_auto_import_builtin(name: &str) -> bool {
427 matches!(name, |"computed"| "customRef"
428 | "defineAsyncComponent"
429 | "defineComponent"
430 | "effectScope"
431 | "getCurrentInstance"
432 | "h"
433 | "inject"
434 | "isProxy"
435 | "isReactive"
436 | "isReadonly"
437 | "isRef"
438 | "markRaw"
439 | "nextTick"
440 | "onActivated"
441 | "onBeforeMount"
442 | "onBeforeUnmount"
443 | "onBeforeUpdate"
444 | "onDeactivated"
445 | "onErrorCaptured"
446 | "onMounted"
447 | "onRenderTracked"
448 | "onRenderTriggered"
449 | "onScopeDispose"
450 | "onServerPrefetch"
451 | "onUnmounted"
452 | "onUpdated"
453 | "provide"
454 | "reactive"
455 | "readonly"
456 | "ref"
457 | "resolveComponent"
458 | "shallowReactive"
459 | "shallowReadonly"
460 | "shallowRef"
461 | "toRaw"
462 | "toRef"
463 | "toRefs"
464 | "triggerRef"
465 | "unref"
466 | "watch"
467 | "watchEffect"
468 | "watchPostEffect"
469 | "watchSyncEffect")
470}
471
472fn is_nuxt_auto_import_builtin(name: &str) -> bool {
473 matches!(name, |"useAsyncData"| "useCookie"
474 | "useError"
475 | "useFetch"
476 | "useHead"
477 | "useLazyAsyncData"
478 | "useLazyFetch"
479 | "useNuxtApp"
480 | "useRequestEvent"
481 | "useRequestHeaders"
482 | "useRoute"
483 | "useRouter"
484 | "useRuntimeConfig"
485 | "useSeoMeta"
486 | "useState")
487}
488
489fn synthetic_auto_import_info(name: &str, kind: AutoImportKind) -> ImportInfo {
492 let imported_name = match kind {
493 AutoImportKind::Named => ImportedName::Named(name.to_string()),
494 AutoImportKind::Default | AutoImportKind::DefaultComponent => ImportedName::Default,
495 };
496 ImportInfo {
497 source: format!("<auto-import:{name}>"),
498 imported_name,
499 local_name: name.to_string(),
500 is_type_only: false,
501 from_style: false,
502 span: Span::default(),
503 source_span: Span::default(),
504 }
505}