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 CommonJsInternalModule(FileId),
19 SyntheticAutoImport(FileId),
21 InternalPackageModule {
24 file_id: FileId,
26 package_name: String,
28 },
29 CommonJsInternalPackageModule {
31 file_id: FileId,
33 package_name: String,
35 },
36 ExternalFile(PathBuf),
38 NpmPackage(String),
40 CommonJsNpmPackage(String),
42 Unresolvable(String),
44}
45
46impl ResolveResult {
47 #[must_use]
49 pub const fn internal_file_id(&self) -> Option<FileId> {
50 match self {
51 Self::InternalModule(file_id)
52 | Self::CommonJsInternalModule(file_id)
53 | Self::SyntheticAutoImport(file_id)
54 | Self::InternalPackageModule { file_id, .. }
55 | Self::CommonJsInternalPackageModule { file_id, .. } => Some(*file_id),
56 Self::ExternalFile(_)
57 | Self::NpmPackage(_)
58 | Self::CommonJsNpmPackage(_)
59 | Self::Unresolvable(_) => None,
60 }
61 }
62
63 #[must_use]
65 pub const fn is_synthetic_auto_import(&self) -> bool {
66 matches!(self, Self::SyntheticAutoImport(_))
67 }
68
69 #[must_use]
71 pub const fn is_commonjs_require(&self) -> bool {
72 matches!(
73 self,
74 Self::CommonJsInternalModule(_)
75 | Self::CommonJsInternalPackageModule { .. }
76 | Self::CommonJsNpmPackage(_)
77 )
78 }
79
80 #[must_use]
82 pub const fn is_bare_package(&self) -> bool {
83 matches!(self, Self::NpmPackage(_) | Self::CommonJsNpmPackage(_))
84 }
85
86 #[must_use]
88 pub fn into_commonjs_require(self) -> Self {
89 match self {
90 Self::InternalModule(file_id) => Self::CommonJsInternalModule(file_id),
91 Self::InternalPackageModule {
92 file_id,
93 package_name,
94 } => Self::CommonJsInternalPackageModule {
95 file_id,
96 package_name,
97 },
98 Self::NpmPackage(package_name) => Self::CommonJsNpmPackage(package_name),
99 other => other,
100 }
101 }
102
103 #[must_use]
105 pub fn into_es_module(self) -> Self {
106 match self {
107 Self::CommonJsInternalModule(file_id) => Self::InternalModule(file_id),
108 Self::CommonJsInternalPackageModule {
109 file_id,
110 package_name,
111 } => Self::InternalPackageModule {
112 file_id,
113 package_name,
114 },
115 Self::CommonJsNpmPackage(package_name) => Self::NpmPackage(package_name),
116 other => other,
117 }
118 }
119
120 #[must_use]
122 pub fn package_usage_name(&self) -> Option<&str> {
123 match self {
124 Self::InternalPackageModule { package_name, .. }
125 | Self::CommonJsInternalPackageModule { package_name, .. }
126 | Self::NpmPackage(package_name)
127 | Self::CommonJsNpmPackage(package_name) => Some(package_name),
128 Self::InternalModule(_)
129 | Self::CommonJsInternalModule(_)
130 | Self::SyntheticAutoImport(_)
131 | Self::ExternalFile(_)
132 | Self::Unresolvable(_) => None,
133 }
134 }
135}
136
137#[derive(Debug, Default)]
139pub struct ResolvedProject {
140 pub modules: Vec<ResolvedModule>,
142 pub replaced_module_targets: Vec<ResolvedReplacedModuleTarget>,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct ResolvedReplacedModuleTarget {
149 pub source_file: FileId,
151 pub target_file: FileId,
153}
154
155#[derive(Debug, Clone)]
157pub struct ResolvedImport {
158 pub info: fallow_types::extract::ImportInfo,
160 pub target: ResolveResult,
162}
163
164#[derive(Debug, Clone)]
166pub struct ResolvedReExport {
167 pub info: fallow_types::extract::ReExportInfo,
169 pub target: ResolveResult,
171}
172
173pub enum ResolvedSourceEdge<'a> {
175 Import(&'a ResolvedImport),
177 ReExport(&'a ResolvedReExport),
179}
180
181impl<'a> ResolvedSourceEdge<'a> {
182 #[must_use]
184 pub fn source_specifier(&self) -> &'a str {
185 match self {
186 Self::Import(import) => &import.info.source,
187 Self::ReExport(re_export) => &re_export.info.source,
188 }
189 }
190
191 #[must_use]
193 pub const fn target(&self) -> &'a ResolveResult {
194 match self {
195 Self::Import(import) => &import.target,
196 Self::ReExport(re_export) => &re_export.target,
197 }
198 }
199
200 #[must_use]
202 pub const fn is_type_only(&self) -> bool {
203 match self {
204 Self::Import(import) => import.info.is_type_only,
205 Self::ReExport(re_export) => re_export.info.is_type_only,
206 }
207 }
208
209 #[must_use]
211 pub const fn span(&self) -> oxc_span::Span {
212 match self {
213 Self::Import(import) => import.info.span,
214 Self::ReExport(re_export) => re_export.info.span,
215 }
216 }
217
218 #[must_use]
220 pub const fn source_span(&self) -> oxc_span::Span {
221 match self {
222 Self::Import(import) => import.info.source_span,
223 Self::ReExport(_) => oxc_span::Span::new(0, 0),
224 }
225 }
226}
227
228#[derive(Debug)]
230pub struct ResolvedModule {
231 pub file_id: FileId,
233 pub path: PathBuf,
235 pub exports: Vec<fallow_types::extract::ExportInfo>,
237 pub re_exports: Vec<ResolvedReExport>,
239 pub resolved_imports: Vec<ResolvedImport>,
241 pub resolved_dynamic_imports: Vec<ResolvedImport>,
243 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
245 pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
247 pub semantic_facts: Box<[fallow_types::extract::SemanticFact]>,
249 pub whole_object_uses: Box<[String]>,
251 pub has_cjs_exports: bool,
253 pub has_angular_component_template_url: bool,
257 pub unused_import_bindings: FxHashSet<String>,
259 pub type_referenced_import_bindings: Vec<String>,
261 pub value_referenced_import_bindings: Vec<String>,
263 pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
266 pub exported_factory_returns: Box<[fallow_types::extract::FactoryReturnExport]>,
269 pub exported_factory_return_object_shapes:
273 Box<[fallow_types::extract::FactoryReturnObjectShapeExport]>,
274 pub type_member_types: Box<[fallow_types::extract::TypeMemberTypeEntry]>,
278}
279
280impl Default for ResolvedModule {
281 fn default() -> Self {
282 Self {
283 file_id: FileId(0),
284 path: PathBuf::new(),
285 exports: vec![],
286 re_exports: vec![],
287 resolved_imports: vec![],
288 resolved_dynamic_imports: vec![],
289 resolved_dynamic_patterns: vec![],
290 member_accesses: vec![],
291 semantic_facts: Box::default(),
292 whole_object_uses: Box::default(),
293 has_cjs_exports: false,
294 has_angular_component_template_url: false,
295 unused_import_bindings: FxHashSet::default(),
296 type_referenced_import_bindings: vec![],
297 value_referenced_import_bindings: vec![],
298 namespace_object_aliases: vec![],
299 exported_factory_returns: Box::default(),
300 exported_factory_return_object_shapes: Box::default(),
301 type_member_types: Box::default(),
302 }
303 }
304}
305
306impl ResolvedModule {
307 pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
313 self.resolved_imports
314 .iter()
315 .chain(self.resolved_dynamic_imports.iter())
316 }
317
318 pub fn all_resolved_source_edges(&self) -> impl Iterator<Item = ResolvedSourceEdge<'_>> {
324 self.resolved_imports
325 .iter()
326 .map(ResolvedSourceEdge::Import)
327 .chain(
328 self.resolved_dynamic_imports
329 .iter()
330 .map(ResolvedSourceEdge::Import),
331 )
332 .chain(self.re_exports.iter().map(ResolvedSourceEdge::ReExport))
333 }
334}
335
336pub(super) struct ResolveContext<'a> {
341 pub resolver: &'a Resolver,
343 pub style_resolver: &'a Resolver,
347 pub extensions: &'a [String],
349 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
351 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
353 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
355 pub package_manifests: &'a [PackageManifestInfo],
357 pub has_deno_import_maps: bool,
360 pub condition_names: &'a [String],
362 pub path_aliases: &'a [(String, String)],
364 pub scss_include_paths: &'a [PathBuf],
368 pub static_dir_mappings: &'a [(PathBuf, String)],
371 pub root: &'a Path,
373 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
377 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
382 pub tsconfig_cache: &'a TsconfigCache,
387 pub canonicalize_cache: &'a CanonicalizeCache,
394}
395
396#[derive(Default)]
398pub(super) struct CanonicalizeCache {
399 map: Mutex<FxHashMap<PathBuf, Option<PathBuf>>>,
400}
401
402impl CanonicalizeCache {
403 pub fn get(&self, path: &Path) -> Option<PathBuf> {
407 if let Ok(cache) = self.map.lock()
408 && let Some(value) = cache.get(path)
409 {
410 return value.clone();
411 }
412 let value = dunce::canonicalize(path).ok();
413 if let Ok(mut cache) = self.map.lock() {
414 cache.insert(path.to_path_buf(), value.clone());
415 }
416 value
417 }
418}
419
420#[derive(Default)]
422pub(super) struct TsconfigCache {
423 json: Mutex<FxHashMap<PathBuf, Option<Value>>>,
424 chains: Mutex<FxHashMap<PathBuf, Vec<PathBuf>>>,
425}
426
427impl TsconfigCache {
428 pub fn json(&self, path: &Path, load: impl FnOnce(&Path) -> Option<Value>) -> Option<Value> {
430 if let Ok(cache) = self.json.lock()
431 && let Some(value) = cache.get(path)
432 {
433 return value.clone();
434 }
435
436 let value = load(path);
437 if let Ok(mut cache) = self.json.lock() {
438 cache.insert(path.to_path_buf(), value.clone());
439 }
440 value
441 }
442
443 pub fn chain(&self, from_file: &Path) -> Option<Vec<PathBuf>> {
445 self.chains
446 .lock()
447 .ok()
448 .and_then(|cache| cache.get(from_file).cloned())
449 }
450
451 pub fn store_chain(&self, from_file: &Path, chain: Vec<PathBuf>) {
453 if let Ok(mut cache) = self.chains.lock() {
454 cache.insert(from_file.to_path_buf(), chain);
455 }
456 }
457}
458
459#[derive(Debug, Clone)]
461pub(super) struct PackageManifestInfo {
462 pub root: PathBuf,
464 pub canonical_root: PathBuf,
466 pub name: Option<String>,
468 pub package_json: fallow_config::PackageJson,
470 pub deno_import_map: Vec<DenoImportMapEntry>,
472}
473
474#[derive(Debug, Clone)]
476pub(super) struct DenoImportMapEntry {
477 pub key: String,
478 pub target: String,
479 pub declaring_dir: PathBuf,
480}
481
482pub(super) struct CanonicalFallback<'a> {
484 files: &'a [fallow_types::discover::DiscoveredFile],
485 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
486}
487
488impl<'a> CanonicalFallback<'a> {
489 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
490 Self {
491 files,
492 map: std::sync::OnceLock::new(),
493 }
494 }
495
496 pub fn get(&self, canonical: &Path) -> Option<FileId> {
498 let map = self.map.get_or_init(|| {
499 tracing::debug!(
500 "intra-project symlinks detected, building canonical path index ({} files)",
501 self.files.len()
502 );
503 self.files
504 .iter()
505 .filter_map(|f| {
506 dunce::canonicalize(&f.path)
507 .ok()
508 .map(|canonical| (canonical, f.id))
509 })
510 .collect()
511 });
512 map.get(canonical).copied()
513 }
514}
515
516#[cfg(all(test, not(miri)))]
517mod tests {
518 use super::*;
519 use fallow_types::discover::DiscoveredFile;
520
521 #[test]
522 fn canonical_fallback_returns_none_for_empty_files() {
523 let files: Vec<DiscoveredFile> = vec![];
524 let fallback = CanonicalFallback::new(&files);
525 assert!(fallback.get(Path::new("/nonexistent")).is_none());
526 }
527
528 #[test]
529 fn canonical_fallback_finds_existing_file() {
530 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
531 let _ = std::fs::create_dir_all(&temp);
532 let test_file = temp.join("test.ts");
533 std::fs::write(&test_file, "").unwrap();
534
535 let files = vec![DiscoveredFile {
536 id: FileId(42),
537 path: test_file.clone(),
538 size_bytes: 0,
539 }];
540 let fallback = CanonicalFallback::new(&files);
541
542 let canonical = dunce::canonicalize(&test_file).unwrap();
543 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
544
545 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
546
547 let _ = std::fs::remove_dir_all(&temp);
548 }
549
550 #[test]
551 fn canonical_fallback_returns_none_for_missing_path() {
552 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
553 let _ = std::fs::create_dir_all(&temp);
554 let test_file = temp.join("exists.ts");
555 std::fs::write(&test_file, "").unwrap();
556
557 let files = vec![DiscoveredFile {
558 id: FileId(1),
559 path: test_file,
560 size_bytes: 0,
561 }];
562 let fallback = CanonicalFallback::new(&files);
563 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
564
565 let _ = std::fs::remove_dir_all(&temp);
566 }
567
568 #[test]
569 fn commonjs_provenance_wraps_internal_and_bare_package_targets() {
570 assert!(matches!(
571 ResolveResult::InternalModule(FileId(4)).into_commonjs_require(),
572 ResolveResult::CommonJsInternalModule(FileId(4))
573 ));
574 assert!(matches!(
575 ResolveResult::InternalPackageModule {
576 file_id: FileId(5),
577 package_name: "pkg".to_string(),
578 }
579 .into_commonjs_require(),
580 ResolveResult::CommonJsInternalPackageModule {
581 file_id: FileId(5),
582 package_name,
583 } if package_name == "pkg"
584 ));
585 assert!(matches!(
586 ResolveResult::NpmPackage("pkg".to_string()).into_commonjs_require(),
587 ResolveResult::CommonJsNpmPackage(package_name) if package_name == "pkg"
588 ));
589 }
590}
591
592pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
597
598pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
600
601pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];