Skip to main content

fallow_graph/resolve/
mod.rs

1//! Import specifier resolution using `oxc_resolver`.
2//!
3//! Orchestrates the resolution pipeline: for every extracted module, resolves all
4//! import specifiers in parallel (via rayon) to an [`ResolveResult`], internal file,
5//! npm package, external file, or unresolvable. The entry point is [`resolve_all_imports`].
6//!
7//! Resolution is split into submodules by import kind:
8//! - `static_imports`: ES `import` declarations
9//! - `dynamic_imports`: `import()` expressions and glob-based dynamic patterns
10//! - `require_imports`: CommonJS `require()` calls
11//! - `re_exports`: `export { x } from './y'` re-export sources
12//! - `upgrades`: post-resolution pass fixing non-deterministic bare specifier results
13//!
14//! Handles tsconfig path aliases (auto-discovered per file), pnpm virtual store paths,
15//! React Native platform extensions, and package.json `exports` subpath resolution with
16//! output-to-source directory fallback.
17
18mod 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
59/// Inputs used to resolve imports for a complete extracted project.
60pub struct ResolveAllImportsInput<'a> {
61    /// Extracted modules whose imports should be resolved.
62    pub modules: &'a [ModuleInfo],
63    /// Discovered source files indexed by [`FileId`].
64    pub files: &'a [DiscoveredFile],
65    /// Workspace package roots used for package self-resolution.
66    pub workspaces: &'a [fallow_config::WorkspaceInfo],
67    /// Active plugin names that affect extensions and resolver conditions.
68    pub active_plugins: &'a [String],
69    /// Configured TypeScript path alias pairs.
70    pub path_aliases: &'a [(String, String)],
71    /// Auto-import rules that synthesize implicit graph edges.
72    pub auto_imports: &'a [AutoImportRule],
73    /// Additional Sass and SCSS include directories.
74    pub scss_include_paths: &'a [PathBuf],
75    /// Static directory mappings for framework-specific asset resolution.
76    pub static_dir_mappings: &'a [(PathBuf, String)],
77    /// Project root used for package manifest and relative-path resolution.
78    pub root: &'a Path,
79    /// Extra resolver conditions supplied by configuration.
80    pub extra_conditions: &'a [String],
81}
82
83/// Resolve all imports across all modules in parallel.
84#[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
293/// Synthesize module-graph edges for convention auto-imports.
294///
295/// For each module, every captured `auto_import_candidates` name is matched
296/// against the active plugins' auto-import table; on a hit a synthetic
297/// [`ResolvedImport`] is added so the existing graph builder credits the edge.
298/// Name collisions across files over-credit every match, keeping each provider
299/// reachable. Resolution is recomputed from the live file index each run.
300fn 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
489/// Build a synthetic [`ImportInfo`] for a convention auto-import. Component and
490/// default kinds credit the default export; named kinds credit the named export.
491fn 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}