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 InternalPackageModule {
19 file_id: FileId,
21 package_name: String,
23 },
24 ExternalFile(PathBuf),
26 NpmPackage(String),
28 Unresolvable(String),
30}
31
32impl ResolveResult {
33 #[must_use]
35 pub const fn internal_file_id(&self) -> Option<FileId> {
36 match self {
37 Self::InternalModule(file_id) | Self::InternalPackageModule { file_id, .. } => {
38 Some(*file_id)
39 }
40 Self::ExternalFile(_) | Self::NpmPackage(_) | Self::Unresolvable(_) => None,
41 }
42 }
43
44 #[must_use]
46 pub fn package_usage_name(&self) -> Option<&str> {
47 match self {
48 Self::InternalPackageModule { package_name, .. } | Self::NpmPackage(package_name) => {
49 Some(package_name)
50 }
51 Self::InternalModule(_) | Self::ExternalFile(_) | Self::Unresolvable(_) => None,
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct ResolvedImport {
59 pub info: fallow_types::extract::ImportInfo,
61 pub target: ResolveResult,
63}
64
65#[derive(Debug, Clone)]
67pub struct ResolvedReExport {
68 pub info: fallow_types::extract::ReExportInfo,
70 pub target: ResolveResult,
72}
73
74#[derive(Debug)]
76pub struct ResolvedModule {
77 pub file_id: FileId,
79 pub path: PathBuf,
81 pub exports: Vec<fallow_types::extract::ExportInfo>,
83 pub re_exports: Vec<ResolvedReExport>,
85 pub resolved_imports: Vec<ResolvedImport>,
87 pub resolved_dynamic_imports: Vec<ResolvedImport>,
89 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
91 pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
93 pub whole_object_uses: Vec<String>,
95 pub has_cjs_exports: bool,
97 pub has_angular_component_template_url: bool,
101 pub unused_import_bindings: FxHashSet<String>,
103 pub type_referenced_import_bindings: Vec<String>,
105 pub value_referenced_import_bindings: Vec<String>,
107 pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
110}
111
112impl Default for ResolvedModule {
113 fn default() -> Self {
114 Self {
115 file_id: FileId(0),
116 path: PathBuf::new(),
117 exports: vec![],
118 re_exports: vec![],
119 resolved_imports: vec![],
120 resolved_dynamic_imports: vec![],
121 resolved_dynamic_patterns: vec![],
122 member_accesses: vec![],
123 whole_object_uses: vec![],
124 has_cjs_exports: false,
125 has_angular_component_template_url: false,
126 unused_import_bindings: FxHashSet::default(),
127 type_referenced_import_bindings: vec![],
128 value_referenced_import_bindings: vec![],
129 namespace_object_aliases: vec![],
130 }
131 }
132}
133
134impl ResolvedModule {
135 pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
141 self.resolved_imports
142 .iter()
143 .chain(self.resolved_dynamic_imports.iter())
144 }
145}
146
147pub(super) struct ResolveContext<'a> {
152 pub resolver: &'a Resolver,
154 pub style_resolver: &'a Resolver,
158 pub extensions: &'a [String],
160 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
162 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
164 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
166 pub package_manifests: &'a [PackageManifestInfo],
168 pub condition_names: &'a [String],
170 pub path_aliases: &'a [(String, String)],
172 pub scss_include_paths: &'a [PathBuf],
176 pub root: &'a Path,
178 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
182 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
187}
188
189#[derive(Debug, Clone)]
191pub(super) struct PackageManifestInfo {
192 pub root: PathBuf,
194 pub canonical_root: PathBuf,
196 pub name: Option<String>,
198 pub package_json: fallow_config::PackageJson,
200}
201
202pub(super) struct CanonicalFallback<'a> {
204 files: &'a [fallow_types::discover::DiscoveredFile],
205 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
206}
207
208impl<'a> CanonicalFallback<'a> {
209 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
210 Self {
211 files,
212 map: std::sync::OnceLock::new(),
213 }
214 }
215
216 pub fn get(&self, canonical: &Path) -> Option<FileId> {
218 let map = self.map.get_or_init(|| {
219 tracing::debug!(
220 "intra-project symlinks detected, building canonical path index ({} files)",
221 self.files.len()
222 );
223 self.files
224 .iter()
225 .filter_map(|f| {
226 dunce::canonicalize(&f.path)
227 .ok()
228 .map(|canonical| (canonical, f.id))
229 })
230 .collect()
231 });
232 map.get(canonical).copied()
233 }
234}
235
236#[cfg(all(test, not(miri)))]
237mod tests {
238 use super::*;
239 use fallow_types::discover::DiscoveredFile;
240
241 #[test]
242 fn canonical_fallback_returns_none_for_empty_files() {
243 let files: Vec<DiscoveredFile> = vec![];
244 let fallback = CanonicalFallback::new(&files);
245 assert!(fallback.get(Path::new("/nonexistent")).is_none());
246 }
247
248 #[test]
249 fn canonical_fallback_finds_existing_file() {
250 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
251 let _ = std::fs::create_dir_all(&temp);
252 let test_file = temp.join("test.ts");
253 std::fs::write(&test_file, "").unwrap();
254
255 let files = vec![DiscoveredFile {
256 id: FileId(42),
257 path: test_file.clone(),
258 size_bytes: 0,
259 }];
260 let fallback = CanonicalFallback::new(&files);
261
262 let canonical = dunce::canonicalize(&test_file).unwrap();
263 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
264
265 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
267
268 let _ = std::fs::remove_dir_all(&temp);
269 }
270
271 #[test]
272 fn canonical_fallback_returns_none_for_missing_path() {
273 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
274 let _ = std::fs::create_dir_all(&temp);
275 let test_file = temp.join("exists.ts");
276 std::fs::write(&test_file, "").unwrap();
277
278 let files = vec![DiscoveredFile {
279 id: FileId(1),
280 path: test_file,
281 size_bytes: 0,
282 }];
283 let fallback = CanonicalFallback::new(&files);
284 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
285
286 let _ = std::fs::remove_dir_all(&temp);
287 }
288}
289
290pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
295
296pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
298
299pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];