1use std::path::{Path, PathBuf};
4use std::sync::Mutex;
5
6use oxc_resolver::Resolver;
7use rustc_hash::{FxHashMap, FxHashSet};
8use serde_json::Value;
9
10use fallow_types::discover::FileId;
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub enum ResolveResult {
15 InternalModule(FileId),
17 SyntheticAutoImport(FileId),
19 InternalPackageModule {
22 file_id: FileId,
24 package_name: String,
26 },
27 ExternalFile(PathBuf),
29 NpmPackage(String),
31 Unresolvable(String),
33}
34
35impl ResolveResult {
36 #[must_use]
38 pub const fn internal_file_id(&self) -> Option<FileId> {
39 match self {
40 Self::InternalModule(file_id)
41 | Self::SyntheticAutoImport(file_id)
42 | Self::InternalPackageModule { file_id, .. } => Some(*file_id),
43 Self::ExternalFile(_) | Self::NpmPackage(_) | Self::Unresolvable(_) => None,
44 }
45 }
46
47 #[must_use]
49 pub const fn is_synthetic_auto_import(&self) -> bool {
50 matches!(self, Self::SyntheticAutoImport(_))
51 }
52
53 #[must_use]
55 pub fn package_usage_name(&self) -> Option<&str> {
56 match self {
57 Self::InternalPackageModule { package_name, .. } | Self::NpmPackage(package_name) => {
58 Some(package_name)
59 }
60 Self::InternalModule(_)
61 | Self::SyntheticAutoImport(_)
62 | Self::ExternalFile(_)
63 | Self::Unresolvable(_) => None,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct ResolvedImport {
71 pub info: fallow_types::extract::ImportInfo,
73 pub target: ResolveResult,
75}
76
77#[derive(Debug, Clone)]
79pub struct ResolvedReExport {
80 pub info: fallow_types::extract::ReExportInfo,
82 pub target: ResolveResult,
84}
85
86pub enum ResolvedSourceEdge<'a> {
88 Import(&'a ResolvedImport),
90 ReExport(&'a ResolvedReExport),
92}
93
94impl<'a> ResolvedSourceEdge<'a> {
95 #[must_use]
97 pub fn source_specifier(&self) -> &'a str {
98 match self {
99 Self::Import(import) => &import.info.source,
100 Self::ReExport(re_export) => &re_export.info.source,
101 }
102 }
103
104 #[must_use]
106 pub const fn target(&self) -> &'a ResolveResult {
107 match self {
108 Self::Import(import) => &import.target,
109 Self::ReExport(re_export) => &re_export.target,
110 }
111 }
112
113 #[must_use]
115 pub const fn is_type_only(&self) -> bool {
116 match self {
117 Self::Import(import) => import.info.is_type_only,
118 Self::ReExport(re_export) => re_export.info.is_type_only,
119 }
120 }
121
122 #[must_use]
124 pub const fn span(&self) -> oxc_span::Span {
125 match self {
126 Self::Import(import) => import.info.span,
127 Self::ReExport(re_export) => re_export.info.span,
128 }
129 }
130
131 #[must_use]
133 pub const fn source_span(&self) -> oxc_span::Span {
134 match self {
135 Self::Import(import) => import.info.source_span,
136 Self::ReExport(_) => oxc_span::Span::new(0, 0),
137 }
138 }
139}
140
141#[derive(Debug)]
143pub struct ResolvedModule {
144 pub file_id: FileId,
146 pub path: PathBuf,
148 pub exports: Vec<fallow_types::extract::ExportInfo>,
150 pub re_exports: Vec<ResolvedReExport>,
152 pub resolved_imports: Vec<ResolvedImport>,
154 pub resolved_dynamic_imports: Vec<ResolvedImport>,
156 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
158 pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
160 pub semantic_facts: Box<[fallow_types::extract::SemanticFact]>,
162 pub whole_object_uses: Box<[String]>,
164 pub has_cjs_exports: bool,
166 pub has_angular_component_template_url: bool,
170 pub unused_import_bindings: FxHashSet<String>,
172 pub type_referenced_import_bindings: Vec<String>,
174 pub value_referenced_import_bindings: Vec<String>,
176 pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
179 pub exported_factory_returns: Box<[fallow_types::extract::FactoryReturnExport]>,
182 pub exported_factory_return_object_shapes:
186 Box<[fallow_types::extract::FactoryReturnObjectShapeExport]>,
187 pub type_member_types: Box<[fallow_types::extract::TypeMemberTypeEntry]>,
191}
192
193impl Default for ResolvedModule {
194 fn default() -> Self {
195 Self {
196 file_id: FileId(0),
197 path: PathBuf::new(),
198 exports: vec![],
199 re_exports: vec![],
200 resolved_imports: vec![],
201 resolved_dynamic_imports: vec![],
202 resolved_dynamic_patterns: vec![],
203 member_accesses: vec![],
204 semantic_facts: Box::default(),
205 whole_object_uses: Box::default(),
206 has_cjs_exports: false,
207 has_angular_component_template_url: false,
208 unused_import_bindings: FxHashSet::default(),
209 type_referenced_import_bindings: vec![],
210 value_referenced_import_bindings: vec![],
211 namespace_object_aliases: vec![],
212 exported_factory_returns: Box::default(),
213 exported_factory_return_object_shapes: Box::default(),
214 type_member_types: Box::default(),
215 }
216 }
217}
218
219impl ResolvedModule {
220 pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
226 self.resolved_imports
227 .iter()
228 .chain(self.resolved_dynamic_imports.iter())
229 }
230
231 pub fn all_resolved_source_edges(&self) -> impl Iterator<Item = ResolvedSourceEdge<'_>> {
237 self.resolved_imports
238 .iter()
239 .map(ResolvedSourceEdge::Import)
240 .chain(
241 self.resolved_dynamic_imports
242 .iter()
243 .map(ResolvedSourceEdge::Import),
244 )
245 .chain(self.re_exports.iter().map(ResolvedSourceEdge::ReExport))
246 }
247}
248
249pub(super) struct ResolveContext<'a> {
254 pub resolver: &'a Resolver,
256 pub style_resolver: &'a Resolver,
260 pub extensions: &'a [String],
262 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
264 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
266 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
268 pub package_manifests: &'a [PackageManifestInfo],
270 pub condition_names: &'a [String],
272 pub path_aliases: &'a [(String, String)],
274 pub scss_include_paths: &'a [PathBuf],
278 pub static_dir_mappings: &'a [(PathBuf, String)],
281 pub root: &'a Path,
283 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
287 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
292 pub tsconfig_cache: &'a TsconfigCache,
297 pub canonicalize_cache: &'a CanonicalizeCache,
304}
305
306#[derive(Default)]
308pub(super) struct CanonicalizeCache {
309 map: Mutex<FxHashMap<PathBuf, Option<PathBuf>>>,
310}
311
312impl CanonicalizeCache {
313 pub fn get(&self, path: &Path) -> Option<PathBuf> {
317 if let Ok(cache) = self.map.lock()
318 && let Some(value) = cache.get(path)
319 {
320 return value.clone();
321 }
322 let value = dunce::canonicalize(path).ok();
323 if let Ok(mut cache) = self.map.lock() {
324 cache.insert(path.to_path_buf(), value.clone());
325 }
326 value
327 }
328}
329
330#[derive(Default)]
332pub(super) struct TsconfigCache {
333 json: Mutex<FxHashMap<PathBuf, Option<Value>>>,
334 chains: Mutex<FxHashMap<PathBuf, Vec<PathBuf>>>,
335}
336
337impl TsconfigCache {
338 pub fn json(&self, path: &Path, load: impl FnOnce(&Path) -> Option<Value>) -> Option<Value> {
340 if let Ok(cache) = self.json.lock()
341 && let Some(value) = cache.get(path)
342 {
343 return value.clone();
344 }
345
346 let value = load(path);
347 if let Ok(mut cache) = self.json.lock() {
348 cache.insert(path.to_path_buf(), value.clone());
349 }
350 value
351 }
352
353 pub fn chain(&self, from_file: &Path) -> Option<Vec<PathBuf>> {
355 self.chains
356 .lock()
357 .ok()
358 .and_then(|cache| cache.get(from_file).cloned())
359 }
360
361 pub fn store_chain(&self, from_file: &Path, chain: Vec<PathBuf>) {
363 if let Ok(mut cache) = self.chains.lock() {
364 cache.insert(from_file.to_path_buf(), chain);
365 }
366 }
367}
368
369#[derive(Debug, Clone)]
371pub(super) struct PackageManifestInfo {
372 pub root: PathBuf,
374 pub canonical_root: PathBuf,
376 pub name: Option<String>,
378 pub package_json: fallow_config::PackageJson,
380}
381
382pub(super) struct CanonicalFallback<'a> {
384 files: &'a [fallow_types::discover::DiscoveredFile],
385 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
386}
387
388impl<'a> CanonicalFallback<'a> {
389 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
390 Self {
391 files,
392 map: std::sync::OnceLock::new(),
393 }
394 }
395
396 pub fn get(&self, canonical: &Path) -> Option<FileId> {
398 let map = self.map.get_or_init(|| {
399 tracing::debug!(
400 "intra-project symlinks detected, building canonical path index ({} files)",
401 self.files.len()
402 );
403 self.files
404 .iter()
405 .filter_map(|f| {
406 dunce::canonicalize(&f.path)
407 .ok()
408 .map(|canonical| (canonical, f.id))
409 })
410 .collect()
411 });
412 map.get(canonical).copied()
413 }
414}
415
416#[cfg(all(test, not(miri)))]
417mod tests {
418 use super::*;
419 use fallow_types::discover::DiscoveredFile;
420
421 #[test]
422 fn canonical_fallback_returns_none_for_empty_files() {
423 let files: Vec<DiscoveredFile> = vec![];
424 let fallback = CanonicalFallback::new(&files);
425 assert!(fallback.get(Path::new("/nonexistent")).is_none());
426 }
427
428 #[test]
429 fn canonical_fallback_finds_existing_file() {
430 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
431 let _ = std::fs::create_dir_all(&temp);
432 let test_file = temp.join("test.ts");
433 std::fs::write(&test_file, "").unwrap();
434
435 let files = vec![DiscoveredFile {
436 id: FileId(42),
437 path: test_file.clone(),
438 size_bytes: 0,
439 }];
440 let fallback = CanonicalFallback::new(&files);
441
442 let canonical = dunce::canonicalize(&test_file).unwrap();
443 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
444
445 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
446
447 let _ = std::fs::remove_dir_all(&temp);
448 }
449
450 #[test]
451 fn canonical_fallback_returns_none_for_missing_path() {
452 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
453 let _ = std::fs::create_dir_all(&temp);
454 let test_file = temp.join("exists.ts");
455 std::fs::write(&test_file, "").unwrap();
456
457 let files = vec![DiscoveredFile {
458 id: FileId(1),
459 path: test_file,
460 size_bytes: 0,
461 }];
462 let fallback = CanonicalFallback::new(&files);
463 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
464
465 let _ = std::fs::remove_dir_all(&temp);
466 }
467}
468
469pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
474
475pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
477
478pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];