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