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 has_angular_component_template_url: bool,
69 pub unused_import_bindings: FxHashSet<String>,
71 pub type_referenced_import_bindings: Vec<String>,
73 pub value_referenced_import_bindings: Vec<String>,
75 pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
78}
79
80impl Default for ResolvedModule {
81 fn default() -> Self {
82 Self {
83 file_id: FileId(0),
84 path: PathBuf::new(),
85 exports: vec![],
86 re_exports: vec![],
87 resolved_imports: vec![],
88 resolved_dynamic_imports: vec![],
89 resolved_dynamic_patterns: vec![],
90 member_accesses: vec![],
91 whole_object_uses: vec![],
92 has_cjs_exports: false,
93 has_angular_component_template_url: false,
94 unused_import_bindings: FxHashSet::default(),
95 type_referenced_import_bindings: vec![],
96 value_referenced_import_bindings: vec![],
97 namespace_object_aliases: vec![],
98 }
99 }
100}
101
102impl ResolvedModule {
103 pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
109 self.resolved_imports
110 .iter()
111 .chain(self.resolved_dynamic_imports.iter())
112 }
113}
114
115pub(super) struct ResolveContext<'a> {
120 pub resolver: &'a Resolver,
122 pub style_resolver: &'a Resolver,
126 pub extensions: &'a [String],
128 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
130 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
132 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
134 pub path_aliases: &'a [(String, String)],
136 pub scss_include_paths: &'a [PathBuf],
140 pub root: &'a Path,
142 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
146 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
151}
152
153pub(super) struct CanonicalFallback<'a> {
155 files: &'a [fallow_types::discover::DiscoveredFile],
156 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
157}
158
159impl<'a> CanonicalFallback<'a> {
160 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
161 Self {
162 files,
163 map: std::sync::OnceLock::new(),
164 }
165 }
166
167 pub fn get(&self, canonical: &Path) -> Option<FileId> {
169 let map = self.map.get_or_init(|| {
170 tracing::debug!(
171 "intra-project symlinks detected, building canonical path index ({} files)",
172 self.files.len()
173 );
174 self.files
175 .iter()
176 .filter_map(|f| {
177 dunce::canonicalize(&f.path)
178 .ok()
179 .map(|canonical| (canonical, f.id))
180 })
181 .collect()
182 });
183 map.get(canonical).copied()
184 }
185}
186
187#[cfg(all(test, not(miri)))]
188mod tests {
189 use super::*;
190 use fallow_types::discover::DiscoveredFile;
191
192 #[test]
193 fn canonical_fallback_returns_none_for_empty_files() {
194 let files: Vec<DiscoveredFile> = vec![];
195 let fallback = CanonicalFallback::new(&files);
196 assert!(fallback.get(Path::new("/nonexistent")).is_none());
197 }
198
199 #[test]
200 fn canonical_fallback_finds_existing_file() {
201 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
202 let _ = std::fs::create_dir_all(&temp);
203 let test_file = temp.join("test.ts");
204 std::fs::write(&test_file, "").unwrap();
205
206 let files = vec![DiscoveredFile {
207 id: FileId(42),
208 path: test_file.clone(),
209 size_bytes: 0,
210 }];
211 let fallback = CanonicalFallback::new(&files);
212
213 let canonical = dunce::canonicalize(&test_file).unwrap();
214 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
215
216 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
218
219 let _ = std::fs::remove_dir_all(&temp);
220 }
221
222 #[test]
223 fn canonical_fallback_returns_none_for_missing_path() {
224 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
225 let _ = std::fs::create_dir_all(&temp);
226 let test_file = temp.join("exists.ts");
227 std::fs::write(&test_file, "").unwrap();
228
229 let files = vec![DiscoveredFile {
230 id: FileId(1),
231 path: test_file,
232 size_bytes: 0,
233 }];
234 let fallback = CanonicalFallback::new(&files);
235 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
236
237 let _ = std::fs::remove_dir_all(&temp);
238 }
239}
240
241pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
246
247pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
249
250pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];