1use std::path::{Path, PathBuf};
4use std::sync::{Arc, 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(re_export) => re_export.info.source_span,
224 }
225 }
226
227 #[must_use]
232 pub const fn statement_span(&self) -> oxc_span::Span {
233 match self {
234 Self::Import(import) => import.info.span,
235 Self::ReExport(re_export) => re_export.info.statement_span,
236 }
237 }
238}
239
240#[derive(Debug)]
246pub struct ResolvedModule {
247 pub file_id: FileId,
249 pub path: PathBuf,
251 pub exports: Arc<[fallow_types::extract::ExportInfo]>,
253 pub re_exports: Vec<ResolvedReExport>,
255 pub resolved_imports: Vec<ResolvedImport>,
257 pub resolved_dynamic_imports: Vec<ResolvedImport>,
259 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
261 pub member_accesses: Arc<[fallow_types::extract::MemberAccess]>,
263 pub semantic_facts: Arc<[fallow_types::extract::SemanticFact]>,
265 pub whole_object_uses: Arc<[String]>,
267 pub has_cjs_exports: bool,
269 pub has_angular_component_template_url: bool,
273 pub unused_import_bindings: FxHashSet<String>,
275 pub type_referenced_import_bindings: Vec<String>,
277 pub value_referenced_import_bindings: Vec<String>,
279 pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
282 pub exported_factory_returns: Arc<[fallow_types::extract::FactoryReturnExport]>,
285 pub exported_factory_return_object_shapes:
289 Arc<[fallow_types::extract::FactoryReturnObjectShapeExport]>,
290 pub type_member_types: Arc<[fallow_types::extract::TypeMemberTypeEntry]>,
294}
295
296impl Default for ResolvedModule {
297 fn default() -> Self {
298 Self {
299 file_id: FileId(0),
300 path: PathBuf::new(),
301 exports: Arc::default(),
302 re_exports: vec![],
303 resolved_imports: vec![],
304 resolved_dynamic_imports: vec![],
305 resolved_dynamic_patterns: vec![],
306 member_accesses: Arc::default(),
307 semantic_facts: Arc::default(),
308 whole_object_uses: Arc::default(),
309 has_cjs_exports: false,
310 has_angular_component_template_url: false,
311 unused_import_bindings: FxHashSet::default(),
312 type_referenced_import_bindings: vec![],
313 value_referenced_import_bindings: vec![],
314 namespace_object_aliases: vec![],
315 exported_factory_returns: Arc::default(),
316 exported_factory_return_object_shapes: Arc::default(),
317 type_member_types: Arc::default(),
318 }
319 }
320}
321
322impl ResolvedModule {
323 pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
329 self.resolved_imports
330 .iter()
331 .chain(self.resolved_dynamic_imports.iter())
332 }
333
334 pub fn all_resolved_source_edges(&self) -> impl Iterator<Item = ResolvedSourceEdge<'_>> {
340 self.resolved_imports
341 .iter()
342 .map(ResolvedSourceEdge::Import)
343 .chain(
344 self.resolved_dynamic_imports
345 .iter()
346 .map(ResolvedSourceEdge::Import),
347 )
348 .chain(self.re_exports.iter().map(ResolvedSourceEdge::ReExport))
349 }
350}
351
352pub(super) struct ResolveContext<'a> {
357 pub resolver: &'a Resolver,
359 pub style_resolver: &'a Resolver,
363 pub extensions: &'a [String],
365 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
367 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
369 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
371 pub package_manifests: &'a [PackageManifestInfo],
373 pub has_deno_import_maps: bool,
376 pub condition_names: &'a [String],
378 pub path_aliases: &'a [(String, String)],
380 pub scss_include_paths: &'a [PathBuf],
384 pub static_dir_mappings: &'a [(PathBuf, String)],
387 pub root: &'a Path,
389 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
393 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
398 pub tsconfig_cache: &'a TsconfigCache,
403 pub canonicalize_cache: &'a CanonicalizeCache,
410}
411
412#[derive(Default)]
414pub(super) struct CanonicalizeCache {
415 map: Mutex<FxHashMap<PathBuf, Option<PathBuf>>>,
416}
417
418impl CanonicalizeCache {
419 pub fn get(&self, path: &Path) -> Option<PathBuf> {
423 if let Ok(cache) = self.map.lock()
424 && let Some(value) = cache.get(path)
425 {
426 return value.clone();
427 }
428 let value = dunce::canonicalize(path).ok();
429 if let Ok(mut cache) = self.map.lock() {
430 cache.insert(path.to_path_buf(), value.clone());
431 }
432 value
433 }
434}
435
436#[derive(Default)]
438pub(super) struct TsconfigCache {
439 json: Mutex<FxHashMap<PathBuf, Option<Value>>>,
440 chains: Mutex<FxHashMap<PathBuf, Vec<PathBuf>>>,
441}
442
443impl TsconfigCache {
444 pub fn json(&self, path: &Path, load: impl FnOnce(&Path) -> Option<Value>) -> Option<Value> {
446 if let Ok(cache) = self.json.lock()
447 && let Some(value) = cache.get(path)
448 {
449 return value.clone();
450 }
451
452 let value = load(path);
453 if let Ok(mut cache) = self.json.lock() {
454 cache.insert(path.to_path_buf(), value.clone());
455 }
456 value
457 }
458
459 pub fn chain(&self, from_file: &Path) -> Option<Vec<PathBuf>> {
461 self.chains
462 .lock()
463 .ok()
464 .and_then(|cache| cache.get(from_file).cloned())
465 }
466
467 pub fn store_chain(&self, from_file: &Path, chain: Vec<PathBuf>) {
469 if let Ok(mut cache) = self.chains.lock() {
470 cache.insert(from_file.to_path_buf(), chain);
471 }
472 }
473}
474
475#[derive(Debug, Clone)]
477pub(super) struct PackageManifestInfo {
478 pub root: PathBuf,
480 pub canonical_root: PathBuf,
482 pub name: Option<String>,
484 pub package_json: fallow_config::PackageJson,
486 pub deno_import_map: Vec<DenoImportMapEntry>,
488}
489
490#[derive(Debug, Clone)]
492pub(super) struct DenoImportMapEntry {
493 pub key: String,
494 pub target: String,
495 pub declaring_dir: PathBuf,
496}
497
498pub(super) struct CanonicalFallback<'a> {
500 files: &'a [fallow_types::discover::DiscoveredFile],
501 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
502}
503
504impl<'a> CanonicalFallback<'a> {
505 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
506 Self {
507 files,
508 map: std::sync::OnceLock::new(),
509 }
510 }
511
512 pub fn get(&self, canonical: &Path) -> Option<FileId> {
514 let map = self.map.get_or_init(|| {
515 tracing::debug!(
516 "intra-project symlinks detected, building canonical path index ({} files)",
517 self.files.len()
518 );
519 self.files
520 .iter()
521 .filter_map(|f| {
522 dunce::canonicalize(&f.path)
523 .ok()
524 .map(|canonical| (canonical, f.id))
525 })
526 .collect()
527 });
528 map.get(canonical).copied()
529 }
530}
531
532#[cfg(all(test, not(miri)))]
533mod tests {
534 use super::*;
535 use fallow_types::discover::DiscoveredFile;
536
537 #[test]
538 fn canonical_fallback_returns_none_for_empty_files() {
539 let files: Vec<DiscoveredFile> = vec![];
540 let fallback = CanonicalFallback::new(&files);
541 assert!(fallback.get(Path::new("/nonexistent")).is_none());
542 }
543
544 #[test]
545 fn canonical_fallback_finds_existing_file() {
546 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
547 let _ = std::fs::create_dir_all(&temp);
548 let test_file = temp.join("test.ts");
549 std::fs::write(&test_file, "").unwrap();
550
551 let files = vec![DiscoveredFile {
552 id: FileId(42),
553 path: test_file.clone(),
554 size_bytes: 0,
555 }];
556 let fallback = CanonicalFallback::new(&files);
557
558 let canonical = dunce::canonicalize(&test_file).unwrap();
559 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
560
561 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
562
563 let _ = std::fs::remove_dir_all(&temp);
564 }
565
566 #[test]
567 fn canonical_fallback_returns_none_for_missing_path() {
568 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
569 let _ = std::fs::create_dir_all(&temp);
570 let test_file = temp.join("exists.ts");
571 std::fs::write(&test_file, "").unwrap();
572
573 let files = vec![DiscoveredFile {
574 id: FileId(1),
575 path: test_file,
576 size_bytes: 0,
577 }];
578 let fallback = CanonicalFallback::new(&files);
579 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
580
581 let _ = std::fs::remove_dir_all(&temp);
582 }
583
584 #[test]
585 fn commonjs_provenance_wraps_internal_and_bare_package_targets() {
586 assert!(matches!(
587 ResolveResult::InternalModule(FileId(4)).into_commonjs_require(),
588 ResolveResult::CommonJsInternalModule(FileId(4))
589 ));
590 assert!(matches!(
591 ResolveResult::InternalPackageModule {
592 file_id: FileId(5),
593 package_name: "pkg".to_string(),
594 }
595 .into_commonjs_require(),
596 ResolveResult::CommonJsInternalPackageModule {
597 file_id: FileId(5),
598 package_name,
599 } if package_name == "pkg"
600 ));
601 assert!(matches!(
602 ResolveResult::NpmPackage("pkg".to_string()).into_commonjs_require(),
603 ResolveResult::CommonJsNpmPackage(package_name) if package_name == "pkg"
604 ));
605 }
606}
607
608pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
613
614pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
616
617pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];