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_types::discover::{DiscoveredFile, FileId};
46use fallow_types::extract::ModuleInfo;
47
48use dynamic_imports::{resolve_dynamic_imports, resolve_dynamic_patterns};
49use re_exports::resolve_re_exports;
50use react_native::{build_condition_names, build_extensions};
51use require_imports::resolve_require_imports;
52use specifier::create_resolver;
53use static_imports::resolve_static_imports;
54use types::{PackageManifestInfo, ResolveContext};
55use upgrades::apply_specifier_upgrades;
56
57/// Resolve all imports across all modules in parallel.
58#[must_use]
59#[expect(
60    clippy::too_many_arguments,
61    reason = "resolver inputs come from disjoint sources (config, plugins, workspace, filesystem); \
62              bundling them into a struct would be a cross-cutting refactor outside this task"
63)]
64pub fn resolve_all_imports(
65    modules: &[ModuleInfo],
66    files: &[DiscoveredFile],
67    workspaces: &[fallow_config::WorkspaceInfo],
68    active_plugins: &[String],
69    path_aliases: &[(String, String)],
70    scss_include_paths: &[PathBuf],
71    root: &Path,
72    extra_conditions: &[String],
73) -> Vec<ResolvedModule> {
74    // Build workspace name → root index for pnpm store fallback.
75    // Canonicalize roots to match path_to_id (which uses canonical paths).
76    // Without this, macOS /var → /private/var and similar platform symlinks
77    // cause workspace roots to mismatch canonical file paths.
78    let canonical_ws_roots: Vec<PathBuf> = workspaces
79        .par_iter()
80        .map(|ws| dunce::canonicalize(&ws.root).unwrap_or_else(|_| ws.root.clone()))
81        .collect();
82    let workspace_roots: FxHashMap<&str, &Path> = workspaces
83        .iter()
84        .zip(canonical_ws_roots.iter())
85        .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
86        .collect();
87    let root_canonical = dunce::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
88    let mut package_manifests = Vec::new();
89    if let Ok(package_json) = fallow_config::PackageJson::load(&root.join("package.json")) {
90        package_manifests.push(PackageManifestInfo {
91            root: root.to_path_buf(),
92            canonical_root: root_canonical,
93            name: package_json.name.clone(),
94            package_json,
95        });
96    }
97    for (ws, canonical_root) in workspaces.iter().zip(canonical_ws_roots.iter()) {
98        if let Ok(package_json) = fallow_config::PackageJson::load(&ws.root.join("package.json")) {
99            package_manifests.push(PackageManifestInfo {
100                root: ws.root.clone(),
101                canonical_root: canonical_root.clone(),
102                name: package_json.name.clone().or_else(|| Some(ws.name.clone())),
103                package_json,
104            });
105        }
106    }
107
108    // Check if project root is already canonical (no symlinks in path).
109    // When true, raw paths == canonical paths for files under root, so we can skip
110    // the upfront bulk canonicalize() of all source files (21k+ syscalls on large projects).
111    // A lazy CanonicalFallback handles the rare intra-project symlink case.
112    let root_is_canonical = dunce::canonicalize(root).is_ok_and(|c| c == root);
113
114    // Pre-compute canonical paths ONCE for all files in parallel (avoiding repeated syscalls).
115    // Skipped when root is canonical — the lazy fallback below handles edge cases.
116    let canonical_paths: Vec<PathBuf> = if root_is_canonical {
117        Vec::new()
118    } else {
119        files
120            .par_iter()
121            .map(|f| dunce::canonicalize(&f.path).unwrap_or_else(|_| f.path.clone()))
122            .collect()
123    };
124
125    // Primary path → FileId index. When root is canonical, uses raw paths (fast).
126    // Otherwise uses pre-computed canonical paths (correct for all symlink configurations).
127    let path_to_id: FxHashMap<&Path, FileId> = if root_is_canonical {
128        files.iter().map(|f| (f.path.as_path(), f.id)).collect()
129    } else {
130        canonical_paths
131            .iter()
132            .enumerate()
133            .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
134            .collect()
135    };
136
137    // Also index by non-canonical path for fallback lookups
138    let raw_path_to_id: FxHashMap<&Path, FileId> =
139        files.iter().map(|f| (f.path.as_path(), f.id)).collect();
140
141    // FileIds are sequential 0..n, so direct array indexing is faster than FxHashMap.
142    let file_paths: Vec<&Path> = files.iter().map(|f| f.path.as_path()).collect();
143
144    // Create resolvers ONCE and share across threads (oxc_resolver::Resolver is Send + Sync).
145    let extensions = build_extensions(active_plugins);
146    let condition_names = build_condition_names(active_plugins, extra_conditions);
147    let resolver = create_resolver(active_plugins, extra_conditions);
148    let mut style_conditions = extra_conditions.to_vec();
149    style_conditions.push("style".to_string());
150    let style_resolver = create_resolver(active_plugins, &style_conditions);
151
152    // Lazy canonical fallback — only needed when root is canonical (path_to_id uses raw paths).
153    // When root is NOT canonical, path_to_id already uses canonical paths, no fallback needed.
154    let canonical_fallback = if root_is_canonical {
155        Some(types::CanonicalFallback::new(files))
156    } else {
157        None
158    };
159
160    // Dedup set for broken-tsconfig warnings. See `ResolveContext::tsconfig_warned`.
161    let tsconfig_warned: Mutex<FxHashSet<String>> = Mutex::new(FxHashSet::default());
162
163    // Shared resolution context — avoids passing 6 arguments to every resolve_specifier call
164    let ctx = ResolveContext {
165        resolver: &resolver,
166        style_resolver: &style_resolver,
167        extensions: &extensions,
168        path_to_id: &path_to_id,
169        raw_path_to_id: &raw_path_to_id,
170        workspace_roots: &workspace_roots,
171        package_manifests: &package_manifests,
172        condition_names: &condition_names,
173        path_aliases,
174        scss_include_paths,
175        root,
176        canonical_fallback: canonical_fallback.as_ref(),
177        tsconfig_warned: &tsconfig_warned,
178    };
179
180    // Resolve in parallel — shared resolver instance.
181    // Each file resolves its own imports independently (no shared bare specifier cache).
182    // oxc_resolver's internal caches (package.json, tsconfig, directory entries) are
183    // shared across threads for performance.
184    let mut resolved: Vec<ResolvedModule> = modules
185        .par_iter()
186        .filter_map(|module| {
187            let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
188                tracing::warn!(
189                    file_id = module.file_id.0,
190                    "Skipping module with unknown file_id during resolution"
191                );
192                return None;
193            };
194
195            let mut all_imports = resolve_static_imports(&ctx, file_path, &module.imports);
196            all_imports.extend(resolve_require_imports(
197                &ctx,
198                file_path,
199                &module.require_calls,
200            ));
201
202            let from_dir = if canonical_paths.is_empty() {
203                // Root is canonical — raw paths are canonical
204                file_path.parent().unwrap_or(file_path)
205            } else {
206                canonical_paths
207                    .get(module.file_id.0 as usize)
208                    .and_then(|p| p.parent())
209                    .unwrap_or(file_path)
210            };
211
212            Some(ResolvedModule {
213                file_id: module.file_id,
214                path: file_path.to_path_buf(),
215                exports: module.exports.clone(),
216                re_exports: resolve_re_exports(&ctx, file_path, &module.re_exports),
217                resolved_imports: all_imports,
218                resolved_dynamic_imports: resolve_dynamic_imports(
219                    &ctx,
220                    file_path,
221                    &module.dynamic_imports,
222                ),
223                resolved_dynamic_patterns: resolve_dynamic_patterns(
224                    from_dir,
225                    &module.dynamic_import_patterns,
226                    &canonical_paths,
227                    files,
228                ),
229                member_accesses: module.member_accesses.clone(),
230                whole_object_uses: module.whole_object_uses.clone(),
231                has_cjs_exports: module.has_cjs_exports,
232                has_angular_component_template_url: module.has_angular_component_template_url,
233                unused_import_bindings: module.unused_import_bindings.iter().cloned().collect(),
234                type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
235                value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
236                namespace_object_aliases: module.namespace_object_aliases.clone(),
237            })
238        })
239        .collect();
240
241    apply_specifier_upgrades(&mut resolved);
242
243    resolved
244}