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(
239 module,
240 ctx,
241 file_path,
242 from_dir,
243 canonical_paths,
244 files,
245 all_imports,
246 ))
247}
248
249fn build_resolved_module(
250 module: &ModuleInfo,
251 ctx: &ResolveContext<'_>,
252 file_path: &Path,
253 from_dir: &Path,
254 canonical_paths: &[PathBuf],
255 files: &[DiscoveredFile],
256 all_imports: Vec<types::ResolvedImport>,
257) -> ResolvedModule {
258 ResolvedModule {
259 file_id: module.file_id,
260 path: file_path.to_path_buf(),
261 exports: module.exports.clone(),
262 re_exports: resolve_re_exports(ctx, file_path, &module.re_exports),
263 resolved_imports: all_imports,
264 resolved_dynamic_imports: resolve_dynamic_imports(ctx, file_path, &module.dynamic_imports),
265 resolved_dynamic_patterns: resolve_dynamic_patterns(
266 from_dir,
267 &module.dynamic_import_patterns,
268 canonical_paths,
269 files,
270 ),
271 member_accesses: module.member_accesses.clone(),
272 whole_object_uses: module.whole_object_uses.clone(),
273 has_cjs_exports: module.has_cjs_exports,
274 has_angular_component_template_url: module.has_angular_component_template_url,
275 unused_import_bindings: module.unused_import_bindings.iter().cloned().collect(),
276 type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
277 value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
278 namespace_object_aliases: module.namespace_object_aliases.clone(),
279 }
280}
281
282fn synthesize_auto_import_edges(
290 resolved: &mut [ResolvedModule],
291 modules: &[ModuleInfo],
292 auto_imports: &[AutoImportRule],
293 path_to_id: &FxHashMap<&Path, FileId>,
294 raw_path_to_id: &FxHashMap<&Path, FileId>,
295) {
296 if auto_imports.is_empty() {
297 return;
298 }
299
300 let mut table: FxHashMap<&str, Vec<(FileId, AutoImportKind)>> = FxHashMap::default();
301 for rule in auto_imports {
302 let source = rule.source.as_path();
303 let Some(file_id) = raw_path_to_id
304 .get(source)
305 .or_else(|| path_to_id.get(source))
306 .copied()
307 else {
308 continue;
309 };
310 table
311 .entry(rule.name.as_str())
312 .or_default()
313 .push((file_id, rule.kind));
314 }
315 if table.is_empty() {
316 return;
317 }
318
319 let candidates: FxHashMap<FileId, &[String]> = modules
320 .iter()
321 .filter(|module| !module.auto_import_candidates.is_empty())
322 .map(|module| (module.file_id, module.auto_import_candidates.as_slice()))
323 .collect();
324 if candidates.is_empty() {
325 return;
326 }
327
328 for module in resolved.iter_mut() {
329 let Some(names) = candidates.get(&module.file_id) else {
330 continue;
331 };
332 for name in *names {
333 if is_auto_import_builtin(name) {
334 continue;
335 }
336 let Some(targets) = table.get(name.as_str()) else {
337 continue;
338 };
339 for (target_id, kind) in targets {
340 if *target_id == module.file_id {
341 continue;
342 }
343 module.resolved_imports.push(ResolvedImport {
344 info: synthetic_auto_import_info(name, *kind),
345 target: ResolveResult::InternalModule(*target_id),
346 });
347 }
348 }
349 }
350}
351
352fn is_auto_import_builtin(name: &str) -> bool {
353 is_js_auto_import_builtin(name)
354 || is_vue_auto_import_builtin(name)
355 || is_nuxt_auto_import_builtin(name)
356}
357
358fn is_js_auto_import_builtin(name: &str) -> bool {
359 matches!(
360 name,
361 "AbortController"
362 | "AbortSignal"
363 | "Array"
364 | "ArrayBuffer"
365 | "BigInt"
366 | "Blob"
367 | "Boolean"
368 | "Buffer"
369 | "CSS"
370 | "DOMParser"
371 | "Date"
372 | "Document"
373 | "Error"
374 | "Event"
375 | "EventTarget"
376 | "File"
377 | "FormData"
378 | "Intl"
379 | "JSON"
380 | "Map"
381 | "Math"
382 | "Number"
383 | "Object"
384 | "Promise"
385 | "Reflect"
386 | "RegExp"
387 | "Response"
388 | "Set"
389 | "String"
390 | "Symbol"
391 | "URL"
392 | "URLSearchParams"
393 | "WeakMap"
394 | "WeakSet"
395 | "Window"
396 | "alert"
397 | "clearInterval"
398 | "clearTimeout"
399 | "console"
400 | "document"
401 | "fetch"
402 | "global"
403 | "globalThis"
404 | "localStorage"
405 | "navigator"
406 | "process"
407 | "requestAnimationFrame"
408 | "sessionStorage"
409 | "setInterval"
410 | "setTimeout"
411 | "window"
412 )
413}
414
415fn is_vue_auto_import_builtin(name: &str) -> bool {
416 matches!(name, |"computed"| "customRef"
417 | "defineAsyncComponent"
418 | "defineComponent"
419 | "effectScope"
420 | "getCurrentInstance"
421 | "h"
422 | "inject"
423 | "isProxy"
424 | "isReactive"
425 | "isReadonly"
426 | "isRef"
427 | "markRaw"
428 | "nextTick"
429 | "onActivated"
430 | "onBeforeMount"
431 | "onBeforeUnmount"
432 | "onBeforeUpdate"
433 | "onDeactivated"
434 | "onErrorCaptured"
435 | "onMounted"
436 | "onRenderTracked"
437 | "onRenderTriggered"
438 | "onScopeDispose"
439 | "onServerPrefetch"
440 | "onUnmounted"
441 | "onUpdated"
442 | "provide"
443 | "reactive"
444 | "readonly"
445 | "ref"
446 | "resolveComponent"
447 | "shallowReactive"
448 | "shallowReadonly"
449 | "shallowRef"
450 | "toRaw"
451 | "toRef"
452 | "toRefs"
453 | "triggerRef"
454 | "unref"
455 | "watch"
456 | "watchEffect"
457 | "watchPostEffect"
458 | "watchSyncEffect")
459}
460
461fn is_nuxt_auto_import_builtin(name: &str) -> bool {
462 matches!(name, |"useAsyncData"| "useCookie"
463 | "useError"
464 | "useFetch"
465 | "useHead"
466 | "useLazyAsyncData"
467 | "useLazyFetch"
468 | "useNuxtApp"
469 | "useRequestEvent"
470 | "useRequestHeaders"
471 | "useRoute"
472 | "useRouter"
473 | "useRuntimeConfig"
474 | "useSeoMeta"
475 | "useState")
476}
477
478fn synthetic_auto_import_info(name: &str, kind: AutoImportKind) -> ImportInfo {
481 let imported_name = match kind {
482 AutoImportKind::Named => ImportedName::Named(name.to_string()),
483 AutoImportKind::Default | AutoImportKind::DefaultComponent => ImportedName::Default,
484 };
485 ImportInfo {
486 source: format!("<auto-import:{name}>"),
487 imported_name,
488 local_name: name.to_string(),
489 is_type_only: false,
490 from_style: false,
491 span: Span::default(),
492 source_span: Span::default(),
493 }
494}