1use std::path::{Path, PathBuf};
4use std::sync::Mutex;
5
6use oxc_resolver::Resolver;
7use rustc_hash::{FxHashMap, FxHashSet};
8
9use fallow_types::discover::FileId;
10
11#[derive(Debug, Clone)]
13pub enum ResolveResult {
14 InternalModule(FileId),
16 ExternalFile(PathBuf),
18 NpmPackage(String),
20 Unresolvable(String),
22}
23
24#[derive(Debug, Clone)]
26pub struct ResolvedImport {
27 pub info: fallow_types::extract::ImportInfo,
29 pub target: ResolveResult,
31}
32
33#[derive(Debug, Clone)]
35pub struct ResolvedReExport {
36 pub info: fallow_types::extract::ReExportInfo,
38 pub target: ResolveResult,
40}
41
42#[derive(Debug)]
44pub struct ResolvedModule {
45 pub file_id: FileId,
47 pub path: PathBuf,
49 pub exports: Vec<fallow_types::extract::ExportInfo>,
51 pub re_exports: Vec<ResolvedReExport>,
53 pub resolved_imports: Vec<ResolvedImport>,
55 pub resolved_dynamic_imports: Vec<ResolvedImport>,
57 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
59 pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
61 pub whole_object_uses: Vec<String>,
63 pub has_cjs_exports: bool,
65 pub unused_import_bindings: FxHashSet<String>,
67 pub type_referenced_import_bindings: Vec<String>,
69 pub value_referenced_import_bindings: Vec<String>,
71}
72
73impl Default for ResolvedModule {
74 fn default() -> Self {
75 Self {
76 file_id: FileId(0),
77 path: PathBuf::new(),
78 exports: vec![],
79 re_exports: vec![],
80 resolved_imports: vec![],
81 resolved_dynamic_imports: vec![],
82 resolved_dynamic_patterns: vec![],
83 member_accesses: vec![],
84 whole_object_uses: vec![],
85 has_cjs_exports: false,
86 unused_import_bindings: FxHashSet::default(),
87 type_referenced_import_bindings: vec![],
88 value_referenced_import_bindings: vec![],
89 }
90 }
91}
92
93pub(super) struct ResolveContext<'a> {
98 pub resolver: &'a Resolver,
100 pub style_resolver: &'a Resolver,
104 pub extensions: &'a [String],
106 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
108 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
110 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
112 pub path_aliases: &'a [(String, String)],
114 pub scss_include_paths: &'a [PathBuf],
118 pub root: &'a Path,
120 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
124 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
129}
130
131pub(super) struct CanonicalFallback<'a> {
133 files: &'a [fallow_types::discover::DiscoveredFile],
134 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
135}
136
137impl<'a> CanonicalFallback<'a> {
138 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
139 Self {
140 files,
141 map: std::sync::OnceLock::new(),
142 }
143 }
144
145 pub fn get(&self, canonical: &Path) -> Option<FileId> {
147 let map = self.map.get_or_init(|| {
148 tracing::debug!(
149 "intra-project symlinks detected, building canonical path index ({} files)",
150 self.files.len()
151 );
152 self.files
153 .iter()
154 .filter_map(|f| {
155 dunce::canonicalize(&f.path)
156 .ok()
157 .map(|canonical| (canonical, f.id))
158 })
159 .collect()
160 });
161 map.get(canonical).copied()
162 }
163}
164
165#[cfg(all(test, not(miri)))]
166mod tests {
167 use super::*;
168 use fallow_types::discover::DiscoveredFile;
169
170 #[test]
171 fn canonical_fallback_returns_none_for_empty_files() {
172 let files: Vec<DiscoveredFile> = vec![];
173 let fallback = CanonicalFallback::new(&files);
174 assert!(fallback.get(Path::new("/nonexistent")).is_none());
175 }
176
177 #[test]
178 fn canonical_fallback_finds_existing_file() {
179 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
180 let _ = std::fs::create_dir_all(&temp);
181 let test_file = temp.join("test.ts");
182 std::fs::write(&test_file, "").unwrap();
183
184 let files = vec![DiscoveredFile {
185 id: FileId(42),
186 path: test_file.clone(),
187 size_bytes: 0,
188 }];
189 let fallback = CanonicalFallback::new(&files);
190
191 let canonical = dunce::canonicalize(&test_file).unwrap();
192 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
193
194 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
196
197 let _ = std::fs::remove_dir_all(&temp);
198 }
199
200 #[test]
201 fn canonical_fallback_returns_none_for_missing_path() {
202 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
203 let _ = std::fs::create_dir_all(&temp);
204 let test_file = temp.join("exists.ts");
205 std::fs::write(&test_file, "").unwrap();
206
207 let files = vec![DiscoveredFile {
208 id: FileId(1),
209 path: test_file,
210 size_bytes: 0,
211 }];
212 let fallback = CanonicalFallback::new(&files);
213 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
214
215 let _ = std::fs::remove_dir_all(&temp);
216 }
217}
218
219pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
224
225pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
227
228pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];