1use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5
6use dashmap::DashMap;
7use oxc_resolver::Resolver;
8use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
9use serde_json::Value;
10
11use fallow_types::discover::FileId;
12
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
15pub enum ResolveResult {
16 InternalModule(FileId),
18 CommonJsInternalModule(FileId),
20 SyntheticAutoImport(FileId),
22 InternalPackageModule {
25 file_id: FileId,
27 package_name: String,
29 },
30 CommonJsInternalPackageModule {
32 file_id: FileId,
34 package_name: String,
36 },
37 ExternalFile(PathBuf),
39 NpmPackage(String),
41 CommonJsNpmPackage(String),
43 Unresolvable(String),
45}
46
47impl ResolveResult {
48 #[must_use]
50 pub const fn internal_file_id(&self) -> Option<FileId> {
51 match self {
52 Self::InternalModule(file_id)
53 | Self::CommonJsInternalModule(file_id)
54 | Self::SyntheticAutoImport(file_id)
55 | Self::InternalPackageModule { file_id, .. }
56 | Self::CommonJsInternalPackageModule { file_id, .. } => Some(*file_id),
57 Self::ExternalFile(_)
58 | Self::NpmPackage(_)
59 | Self::CommonJsNpmPackage(_)
60 | Self::Unresolvable(_) => None,
61 }
62 }
63
64 #[must_use]
66 pub const fn is_synthetic_auto_import(&self) -> bool {
67 matches!(self, Self::SyntheticAutoImport(_))
68 }
69
70 #[must_use]
72 pub const fn is_commonjs_require(&self) -> bool {
73 matches!(
74 self,
75 Self::CommonJsInternalModule(_)
76 | Self::CommonJsInternalPackageModule { .. }
77 | Self::CommonJsNpmPackage(_)
78 )
79 }
80
81 #[must_use]
83 pub const fn is_bare_package(&self) -> bool {
84 matches!(self, Self::NpmPackage(_) | Self::CommonJsNpmPackage(_))
85 }
86
87 #[must_use]
89 pub fn into_commonjs_require(self) -> Self {
90 match self {
91 Self::InternalModule(file_id) => Self::CommonJsInternalModule(file_id),
92 Self::InternalPackageModule {
93 file_id,
94 package_name,
95 } => Self::CommonJsInternalPackageModule {
96 file_id,
97 package_name,
98 },
99 Self::NpmPackage(package_name) => Self::CommonJsNpmPackage(package_name),
100 other => other,
101 }
102 }
103
104 #[must_use]
106 pub fn into_es_module(self) -> Self {
107 match self {
108 Self::CommonJsInternalModule(file_id) => Self::InternalModule(file_id),
109 Self::CommonJsInternalPackageModule {
110 file_id,
111 package_name,
112 } => Self::InternalPackageModule {
113 file_id,
114 package_name,
115 },
116 Self::CommonJsNpmPackage(package_name) => Self::NpmPackage(package_name),
117 other => other,
118 }
119 }
120
121 #[must_use]
123 pub fn package_usage_name(&self) -> Option<&str> {
124 match self {
125 Self::InternalPackageModule { package_name, .. }
126 | Self::CommonJsInternalPackageModule { package_name, .. }
127 | Self::NpmPackage(package_name)
128 | Self::CommonJsNpmPackage(package_name) => Some(package_name),
129 Self::InternalModule(_)
130 | Self::CommonJsInternalModule(_)
131 | Self::SyntheticAutoImport(_)
132 | Self::ExternalFile(_)
133 | Self::Unresolvable(_) => None,
134 }
135 }
136}
137
138#[derive(Debug, Default)]
140pub struct ResolvedProject {
141 pub modules: Vec<ResolvedModule>,
143 pub replaced_module_targets: Vec<ResolvedReplacedModuleTarget>,
145 pub work: super::ResolveWork,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct ResolvedReplacedModuleTarget {
153 pub source_file: FileId,
155 pub target_file: FileId,
157}
158
159#[derive(Debug, Clone)]
161pub struct ResolvedImport {
162 pub info: fallow_types::extract::ImportInfo,
164 pub target: ResolveResult,
166}
167
168#[derive(Debug, Clone)]
170pub struct ResolvedReExport {
171 pub info: fallow_types::extract::ReExportInfo,
173 pub target: ResolveResult,
175}
176
177pub enum ResolvedSourceEdge<'a> {
179 Import(&'a ResolvedImport),
181 ReExport(&'a ResolvedReExport),
183}
184
185impl<'a> ResolvedSourceEdge<'a> {
186 #[must_use]
188 pub fn source_specifier(&self) -> &'a str {
189 match self {
190 Self::Import(import) => &import.info.source,
191 Self::ReExport(re_export) => &re_export.info.source,
192 }
193 }
194
195 #[must_use]
197 pub const fn target(&self) -> &'a ResolveResult {
198 match self {
199 Self::Import(import) => &import.target,
200 Self::ReExport(re_export) => &re_export.target,
201 }
202 }
203
204 #[must_use]
206 pub const fn is_type_only(&self) -> bool {
207 match self {
208 Self::Import(import) => import.info.is_type_only,
209 Self::ReExport(re_export) => re_export.info.is_type_only,
210 }
211 }
212
213 #[must_use]
215 pub const fn span(&self) -> oxc_span::Span {
216 match self {
217 Self::Import(import) => import.info.span,
218 Self::ReExport(re_export) => re_export.info.span,
219 }
220 }
221
222 #[must_use]
224 pub const fn source_span(&self) -> oxc_span::Span {
225 match self {
226 Self::Import(import) => import.info.source_span,
227 Self::ReExport(re_export) => re_export.info.source_span,
228 }
229 }
230
231 #[must_use]
236 pub const fn statement_span(&self) -> oxc_span::Span {
237 match self {
238 Self::Import(import) => import.info.span,
239 Self::ReExport(re_export) => re_export.info.statement_span,
240 }
241 }
242}
243
244#[derive(Debug)]
250pub struct ResolvedModule {
251 pub file_id: FileId,
253 pub path: PathBuf,
255 pub exports: Arc<[fallow_types::extract::ExportInfo]>,
257 pub re_exports: Vec<ResolvedReExport>,
259 pub resolved_imports: Vec<ResolvedImport>,
261 pub resolved_dynamic_imports: Vec<ResolvedImport>,
263 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
265 pub member_accesses: Arc<[fallow_types::extract::MemberAccess]>,
267 pub semantic_facts: Arc<[fallow_types::extract::SemanticFact]>,
269 pub whole_object_uses: Arc<[String]>,
271 pub has_cjs_exports: bool,
273 pub has_angular_component_template_url: bool,
277 pub unused_import_bindings: FxHashSet<String>,
279 pub type_referenced_import_bindings: Vec<String>,
281 pub value_referenced_import_bindings: Vec<String>,
283 pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
286 pub exported_factory_returns: Arc<[fallow_types::extract::FactoryReturnExport]>,
289 pub exported_factory_return_object_shapes:
293 Arc<[fallow_types::extract::FactoryReturnObjectShapeExport]>,
294 pub type_member_types: Arc<[fallow_types::extract::TypeMemberTypeEntry]>,
298}
299
300impl Default for ResolvedModule {
301 fn default() -> Self {
302 Self {
303 file_id: FileId(0),
304 path: PathBuf::new(),
305 exports: Arc::default(),
306 re_exports: vec![],
307 resolved_imports: vec![],
308 resolved_dynamic_imports: vec![],
309 resolved_dynamic_patterns: vec![],
310 member_accesses: Arc::default(),
311 semantic_facts: Arc::default(),
312 whole_object_uses: Arc::default(),
313 has_cjs_exports: false,
314 has_angular_component_template_url: false,
315 unused_import_bindings: FxHashSet::default(),
316 type_referenced_import_bindings: vec![],
317 value_referenced_import_bindings: vec![],
318 namespace_object_aliases: vec![],
319 exported_factory_returns: Arc::default(),
320 exported_factory_return_object_shapes: Arc::default(),
321 type_member_types: Arc::default(),
322 }
323 }
324}
325
326impl ResolvedModule {
327 pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
333 self.resolved_imports
334 .iter()
335 .chain(self.resolved_dynamic_imports.iter())
336 }
337
338 pub fn all_resolved_source_edges(&self) -> impl Iterator<Item = ResolvedSourceEdge<'_>> {
344 self.resolved_imports
345 .iter()
346 .map(ResolvedSourceEdge::Import)
347 .chain(
348 self.resolved_dynamic_imports
349 .iter()
350 .map(ResolvedSourceEdge::Import),
351 )
352 .chain(self.re_exports.iter().map(ResolvedSourceEdge::ReExport))
353 }
354}
355
356pub(super) struct ResolveContext<'a> {
361 pub resolver: &'a Resolver,
363 pub style_resolver: &'a Resolver,
367 pub extensions: &'a [String],
369 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
371 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
373 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
375 pub package_manifests: &'a [PackageManifestInfo],
377 pub has_deno_import_maps: bool,
380 pub condition_names: &'a [String],
382 pub path_aliases: &'a [(String, String)],
384 pub scss_include_paths: &'a [PathBuf],
388 pub static_dir_mappings: &'a [(PathBuf, String)],
391 pub framework_static_dir_mappings: &'a [(PathBuf, String)],
395 pub root: &'a Path,
397 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
401 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
406 pub tsconfig_cache: &'a TsconfigCache,
411 pub canonicalize_cache: &'a CanonicalizeCache,
418}
419
420#[derive(Default)]
422pub(super) struct CanonicalizeCache {
423 map: DashMap<PathBuf, Option<PathBuf>, FxBuildHasher>,
424}
425
426impl CanonicalizeCache {
427 pub fn get(&self, path: &Path) -> Option<PathBuf> {
431 if let Some(value) = self.map.get(path) {
432 return value.clone();
433 }
434 let value = dunce::canonicalize(path).ok();
435 self.map.insert(path.to_path_buf(), value.clone());
436 value
437 }
438
439 pub fn distinct_paths(&self) -> usize {
443 self.map.len()
444 }
445}
446
447#[derive(Default)]
449pub(super) struct TsconfigCache {
450 json: DashMap<PathBuf, Option<Arc<Value>>, FxBuildHasher>,
451 chains: DashMap<PathBuf, Arc<[PathBuf]>, FxBuildHasher>,
452}
453
454impl TsconfigCache {
455 pub fn json(
462 &self,
463 path: &Path,
464 load: impl FnOnce(&Path) -> Option<Value>,
465 ) -> Option<Arc<Value>> {
466 if let Some(value) = self.json.get(path) {
467 return value.clone();
468 }
469
470 let value = load(path).map(Arc::new);
471 self.json.insert(path.to_path_buf(), value.clone());
472 value
473 }
474
475 pub fn chain(&self, from_file: &Path) -> Option<Arc<[PathBuf]>> {
477 self.chains.get(from_file).map(|entry| Arc::clone(&entry))
478 }
479
480 pub fn store_chain(&self, from_file: &Path, chain: Arc<[PathBuf]>) {
482 self.chains.insert(from_file.to_path_buf(), chain);
483 }
484}
485
486#[derive(Debug, Clone)]
488pub(super) struct PackageManifestInfo {
489 pub root: PathBuf,
491 pub canonical_root: PathBuf,
493 pub name: Option<String>,
495 pub package_json: fallow_config::PackageJson,
497 pub deno_import_map: Vec<DenoImportMapEntry>,
499}
500
501#[derive(Debug, Clone)]
503pub(super) struct DenoImportMapEntry {
504 pub key: String,
505 pub target: String,
506 pub declaring_dir: PathBuf,
507}
508
509pub(super) struct CanonicalFallback<'a> {
511 files: &'a [fallow_types::discover::DiscoveredFile],
512 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
513}
514
515impl<'a> CanonicalFallback<'a> {
516 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
517 Self {
518 files,
519 map: std::sync::OnceLock::new(),
520 }
521 }
522
523 pub fn get(&self, canonical: &Path) -> Option<FileId> {
525 let map = self.map.get_or_init(|| {
526 super::work::note_canonicalize(self.files.len() as u64);
527 tracing::debug!(
528 "intra-project symlinks detected, building canonical path index ({} files)",
529 self.files.len()
530 );
531 self.files
532 .iter()
533 .filter_map(|f| {
534 dunce::canonicalize(&f.path)
535 .ok()
536 .map(|canonical| (canonical, f.id))
537 })
538 .collect()
539 });
540 map.get(canonical).copied()
541 }
542}
543
544#[cfg(all(test, not(miri)))]
545mod tests {
546 use super::*;
547 use fallow_types::discover::DiscoveredFile;
548
549 #[test]
550 fn canonical_fallback_returns_none_for_empty_files() {
551 let files: Vec<DiscoveredFile> = vec![];
552 let fallback = CanonicalFallback::new(&files);
553 assert!(fallback.get(Path::new("/nonexistent")).is_none());
554 }
555
556 #[test]
557 fn canonical_fallback_finds_existing_file() {
558 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
559 let _ = std::fs::create_dir_all(&temp);
560 let test_file = temp.join("test.ts");
561 std::fs::write(&test_file, "").unwrap();
562
563 let files = vec![DiscoveredFile {
564 id: FileId(42),
565 path: test_file.clone(),
566 size_bytes: 0,
567 }];
568 let fallback = CanonicalFallback::new(&files);
569
570 let canonical = dunce::canonicalize(&test_file).unwrap();
571 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
572
573 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
574
575 let _ = std::fs::remove_dir_all(&temp);
576 }
577
578 #[test]
579 fn canonical_fallback_returns_none_for_missing_path() {
580 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
581 let _ = std::fs::create_dir_all(&temp);
582 let test_file = temp.join("exists.ts");
583 std::fs::write(&test_file, "").unwrap();
584
585 let files = vec![DiscoveredFile {
586 id: FileId(1),
587 path: test_file,
588 size_bytes: 0,
589 }];
590 let fallback = CanonicalFallback::new(&files);
591 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
592
593 let _ = std::fs::remove_dir_all(&temp);
594 }
595
596 #[test]
597 fn tsconfig_cache_loads_once_and_shares_the_parsed_document() {
598 let cache = TsconfigCache::default();
599 let path = Path::new("/project/tsconfig.json");
600 let loads = std::sync::atomic::AtomicUsize::new(0);
601 let load = |_: &Path| {
602 loads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
603 Some(serde_json::json!({ "compilerOptions": {} }))
604 };
605
606 let first = cache.json(path, load).unwrap();
607 let second = cache.json(path, load).unwrap();
608
609 assert_eq!(loads.load(std::sync::atomic::Ordering::SeqCst), 1);
610 assert!(
611 Arc::ptr_eq(&first, &second),
612 "repeat reads must share one allocation rather than deep-copy"
613 );
614 }
615
616 #[test]
618 fn tsconfig_cache_caches_a_failed_load() {
619 let cache = TsconfigCache::default();
620 let path = Path::new("/project/missing.json");
621 let loads = std::sync::atomic::AtomicUsize::new(0);
622 let load = |_: &Path| {
623 loads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
624 None
625 };
626
627 assert!(cache.json(path, load).is_none());
628 assert!(cache.json(path, load).is_none());
629 assert_eq!(loads.load(std::sync::atomic::Ordering::SeqCst), 1);
630 }
631
632 #[test]
633 fn tsconfig_cache_round_trips_a_chain() {
634 let cache = TsconfigCache::default();
635 let from_file = Path::new("/project/src/index.ts");
636 assert!(cache.chain(from_file).is_none());
637
638 let chain: Arc<[PathBuf]> = vec![PathBuf::from("/project/tsconfig.json")].into();
639 cache.store_chain(from_file, Arc::clone(&chain));
640
641 assert!(Arc::ptr_eq(&cache.chain(from_file).unwrap(), &chain));
642 }
643
644 #[test]
646 fn tsconfig_cache_is_consistent_under_concurrent_access() {
647 const THREADS: usize = 8;
648 const PATHS: usize = 32;
649
650 let cache = TsconfigCache::default();
651 std::thread::scope(|scope| {
652 for _ in 0..THREADS {
653 scope.spawn(|| {
654 for index in 0..PATHS {
655 let path = PathBuf::from(format!("/project/{index}/tsconfig.json"));
656 let json = cache
657 .json(&path, |_| Some(serde_json::json!({ "index": index })))
658 .unwrap();
659 assert_eq!(json["index"], index);
660 }
661 });
662 }
663 });
664 }
665
666 #[test]
667 fn canonicalize_cache_returns_the_same_result_on_repeat_lookups() {
668 let temp = tempfile::tempdir().expect("create temp dir");
669 let file = temp.path().join("file.ts");
670 std::fs::write(&file, "").unwrap();
671
672 let cache = CanonicalizeCache::default();
673 let expected = dunce::canonicalize(&file).ok();
674 assert_eq!(cache.get(&file), expected);
675 assert_eq!(cache.get(&file), expected);
676 assert!(cache.get(&temp.path().join("missing.ts")).is_none());
677 }
678
679 #[test]
680 fn commonjs_provenance_wraps_internal_and_bare_package_targets() {
681 assert!(matches!(
682 ResolveResult::InternalModule(FileId(4)).into_commonjs_require(),
683 ResolveResult::CommonJsInternalModule(FileId(4))
684 ));
685 assert!(matches!(
686 ResolveResult::InternalPackageModule {
687 file_id: FileId(5),
688 package_name: "pkg".to_string(),
689 }
690 .into_commonjs_require(),
691 ResolveResult::CommonJsInternalPackageModule {
692 file_id: FileId(5),
693 package_name,
694 } if package_name == "pkg"
695 ));
696 assert!(matches!(
697 ResolveResult::NpmPackage("pkg".to_string()).into_commonjs_require(),
698 ResolveResult::CommonJsNpmPackage(package_name) if package_name == "pkg"
699 ));
700 }
701}
702
703pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
708
709pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
711
712pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];