1pub(crate) mod fallbacks;
9mod path_info;
10mod react_native;
11mod specifier;
12mod types;
13
14pub use path_info::{extract_package_name, is_bare_specifier, is_path_alias};
15pub use types::{ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport};
16
17use std::path::{Path, PathBuf};
18
19use rayon::prelude::*;
20use rustc_hash::FxHashMap;
21
22use fallow_types::discover::{DiscoveredFile, FileId};
23use fallow_types::extract::{ImportInfo, ModuleInfo};
24
25use fallbacks::make_glob_from_pattern;
26use specifier::{create_resolver, resolve_specifier};
27use types::ResolveContext;
28
29pub fn resolve_all_imports(
31 modules: &[ModuleInfo],
32 files: &[DiscoveredFile],
33 workspaces: &[fallow_config::WorkspaceInfo],
34 active_plugins: &[String],
35 path_aliases: &[(String, String)],
36 root: &Path,
37) -> Vec<ResolvedModule> {
38 let canonical_ws_roots: Vec<PathBuf> = workspaces
43 .par_iter()
44 .map(|ws| ws.root.canonicalize().unwrap_or_else(|_| ws.root.clone()))
45 .collect();
46 let workspace_roots: FxHashMap<&str, &Path> = workspaces
47 .iter()
48 .zip(canonical_ws_roots.iter())
49 .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
50 .collect();
51
52 let canonical_paths: Vec<PathBuf> = files
55 .par_iter()
56 .map(|f| f.path.canonicalize().unwrap_or_else(|_| f.path.clone()))
57 .collect();
58
59 let path_to_id: FxHashMap<&Path, FileId> = canonical_paths
61 .iter()
62 .enumerate()
63 .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
64 .collect();
65
66 let raw_path_to_id: FxHashMap<&Path, FileId> =
68 files.iter().map(|f| (f.path.as_path(), f.id)).collect();
69
70 let file_paths: Vec<&Path> = files.iter().map(|f| f.path.as_path()).collect();
72
73 let resolver = create_resolver(active_plugins);
75
76 let ctx = ResolveContext {
78 resolver: &resolver,
79 path_to_id: &path_to_id,
80 raw_path_to_id: &raw_path_to_id,
81 workspace_roots: &workspace_roots,
82 path_aliases,
83 root,
84 };
85
86 let mut resolved: Vec<ResolvedModule> = modules
91 .par_iter()
92 .filter_map(|module| {
93 let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
94 tracing::warn!(
95 file_id = module.file_id.0,
96 "Skipping module with unknown file_id during resolution"
97 );
98 return None;
99 };
100
101 let resolved_imports: Vec<ResolvedImport> = module
102 .imports
103 .iter()
104 .map(|imp| ResolvedImport {
105 info: imp.clone(),
106 target: resolve_specifier(&ctx, file_path, &imp.source),
107 })
108 .collect();
109
110 let resolved_dynamic_imports: Vec<ResolvedImport> = module
111 .dynamic_imports
112 .iter()
113 .flat_map(|imp| {
114 let target = resolve_specifier(&ctx, file_path, &imp.source);
115 if !imp.destructured_names.is_empty() {
116 imp.destructured_names
118 .iter()
119 .map(|name| ResolvedImport {
120 info: ImportInfo {
121 source: imp.source.clone(),
122 imported_name: fallow_types::extract::ImportedName::Named(
123 name.clone(),
124 ),
125 local_name: name.clone(),
126 is_type_only: false,
127 span: imp.span,
128 },
129 target: target.clone(),
130 })
131 .collect()
132 } else if imp.local_name.is_some() {
133 vec![ResolvedImport {
135 info: ImportInfo {
136 source: imp.source.clone(),
137 imported_name: fallow_types::extract::ImportedName::Namespace,
138 local_name: imp.local_name.clone().unwrap_or_default(),
139 is_type_only: false,
140 span: imp.span,
141 },
142 target,
143 }]
144 } else {
145 vec![ResolvedImport {
147 info: ImportInfo {
148 source: imp.source.clone(),
149 imported_name: fallow_types::extract::ImportedName::SideEffect,
150 local_name: String::new(),
151 is_type_only: false,
152 span: imp.span,
153 },
154 target,
155 }]
156 }
157 })
158 .collect();
159
160 let re_exports: Vec<ResolvedReExport> = module
161 .re_exports
162 .iter()
163 .map(|re| ResolvedReExport {
164 info: re.clone(),
165 target: resolve_specifier(&ctx, file_path, &re.source),
166 })
167 .collect();
168
169 let require_imports: Vec<ResolvedImport> = module
172 .require_calls
173 .iter()
174 .flat_map(|req| {
175 let target = resolve_specifier(&ctx, file_path, &req.source);
176 if req.destructured_names.is_empty() {
177 vec![ResolvedImport {
178 info: ImportInfo {
179 source: req.source.clone(),
180 imported_name: fallow_types::extract::ImportedName::Namespace,
181 local_name: req.local_name.clone().unwrap_or_default(),
182 is_type_only: false,
183 span: req.span,
184 },
185 target,
186 }]
187 } else {
188 req.destructured_names
189 .iter()
190 .map(|name| ResolvedImport {
191 info: ImportInfo {
192 source: req.source.clone(),
193 imported_name: fallow_types::extract::ImportedName::Named(
194 name.clone(),
195 ),
196 local_name: name.clone(),
197 is_type_only: false,
198 span: req.span,
199 },
200 target: target.clone(),
201 })
202 .collect()
203 }
204 })
205 .collect();
206
207 let mut all_imports = resolved_imports;
208 all_imports.extend(require_imports);
209
210 let from_dir = canonical_paths
213 .get(module.file_id.0 as usize)
214 .and_then(|p| p.parent())
215 .unwrap_or(file_path);
216 let resolved_dynamic_patterns: Vec<(
217 fallow_types::extract::DynamicImportPattern,
218 Vec<FileId>,
219 )> = module
220 .dynamic_import_patterns
221 .iter()
222 .filter_map(|pattern| {
223 let glob_str = make_glob_from_pattern(pattern);
224 let matcher = globset::Glob::new(&glob_str)
225 .ok()
226 .map(|g| g.compile_matcher())?;
227 let matched: Vec<FileId> = canonical_paths
228 .iter()
229 .enumerate()
230 .filter(|(_idx, canonical)| {
231 canonical.strip_prefix(from_dir).is_ok_and(|relative| {
232 let rel_str = format!("./{}", relative.to_string_lossy());
233 matcher.is_match(&rel_str)
234 })
235 })
236 .map(|(idx, _)| files[idx].id)
237 .collect();
238 if matched.is_empty() {
239 None
240 } else {
241 Some((pattern.clone(), matched))
242 }
243 })
244 .collect();
245
246 Some(ResolvedModule {
247 file_id: module.file_id,
248 path: file_path.to_path_buf(),
249 exports: module.exports.clone(),
250 re_exports,
251 resolved_imports: all_imports,
252 resolved_dynamic_imports,
253 resolved_dynamic_patterns,
254 member_accesses: module.member_accesses.clone(),
255 whole_object_uses: module.whole_object_uses.clone(),
256 has_cjs_exports: module.has_cjs_exports,
257 unused_import_bindings: module.unused_import_bindings.clone(),
258 })
259 })
260 .collect();
261
262 let mut specifier_upgrades: FxHashMap<String, FileId> = FxHashMap::default();
279 for module in &resolved {
280 for imp in module
281 .resolved_imports
282 .iter()
283 .chain(module.resolved_dynamic_imports.iter())
284 {
285 if is_bare_specifier(&imp.info.source)
286 && let ResolveResult::InternalModule(file_id) = &imp.target
287 {
288 specifier_upgrades
289 .entry(imp.info.source.clone())
290 .or_insert(*file_id);
291 }
292 }
293 for re in &module.re_exports {
294 if is_bare_specifier(&re.info.source)
295 && let ResolveResult::InternalModule(file_id) = &re.target
296 {
297 specifier_upgrades
298 .entry(re.info.source.clone())
299 .or_insert(*file_id);
300 }
301 }
302 }
303
304 if specifier_upgrades.is_empty() {
305 return resolved;
306 }
307
308 for module in &mut resolved {
310 for imp in module
311 .resolved_imports
312 .iter_mut()
313 .chain(module.resolved_dynamic_imports.iter_mut())
314 {
315 if matches!(imp.target, ResolveResult::NpmPackage(_))
316 && let Some(&file_id) = specifier_upgrades.get(&imp.info.source)
317 {
318 imp.target = ResolveResult::InternalModule(file_id);
319 }
320 }
321 for re in &mut module.re_exports {
322 if matches!(re.target, ResolveResult::NpmPackage(_))
323 && let Some(&file_id) = specifier_upgrades.get(&re.info.source)
324 {
325 re.target = ResolveResult::InternalModule(file_id);
326 }
327 }
328 }
329
330 resolved
331}