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
74pub enum ResolvedSourceEdge<'a> {
76 Import(&'a ResolvedImport),
78 ReExport(&'a ResolvedReExport),
80}
81
82impl<'a> ResolvedSourceEdge<'a> {
83 #[must_use]
85 pub fn source_specifier(&self) -> &'a str {
86 match self {
87 Self::Import(import) => &import.info.source,
88 Self::ReExport(re_export) => &re_export.info.source,
89 }
90 }
91
92 #[must_use]
94 pub const fn target(&self) -> &'a ResolveResult {
95 match self {
96 Self::Import(import) => &import.target,
97 Self::ReExport(re_export) => &re_export.target,
98 }
99 }
100
101 #[must_use]
103 pub const fn is_type_only(&self) -> bool {
104 match self {
105 Self::Import(import) => import.info.is_type_only,
106 Self::ReExport(re_export) => re_export.info.is_type_only,
107 }
108 }
109
110 #[must_use]
112 pub const fn span(&self) -> oxc_span::Span {
113 match self {
114 Self::Import(import) => import.info.span,
115 Self::ReExport(re_export) => re_export.info.span,
116 }
117 }
118
119 #[must_use]
121 pub const fn source_span(&self) -> oxc_span::Span {
122 match self {
123 Self::Import(import) => import.info.source_span,
124 Self::ReExport(_) => oxc_span::Span::new(0, 0),
125 }
126 }
127}
128
129#[derive(Debug)]
131pub struct ResolvedModule {
132 pub file_id: FileId,
134 pub path: PathBuf,
136 pub exports: Vec<fallow_types::extract::ExportInfo>,
138 pub re_exports: Vec<ResolvedReExport>,
140 pub resolved_imports: Vec<ResolvedImport>,
142 pub resolved_dynamic_imports: Vec<ResolvedImport>,
144 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
146 pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
148 pub whole_object_uses: Vec<String>,
150 pub has_cjs_exports: bool,
152 pub has_angular_component_template_url: bool,
156 pub unused_import_bindings: FxHashSet<String>,
158 pub type_referenced_import_bindings: Vec<String>,
160 pub value_referenced_import_bindings: Vec<String>,
162 pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
165}
166
167impl Default for ResolvedModule {
168 fn default() -> Self {
169 Self {
170 file_id: FileId(0),
171 path: PathBuf::new(),
172 exports: vec![],
173 re_exports: vec![],
174 resolved_imports: vec![],
175 resolved_dynamic_imports: vec![],
176 resolved_dynamic_patterns: vec![],
177 member_accesses: vec![],
178 whole_object_uses: vec![],
179 has_cjs_exports: false,
180 has_angular_component_template_url: false,
181 unused_import_bindings: FxHashSet::default(),
182 type_referenced_import_bindings: vec![],
183 value_referenced_import_bindings: vec![],
184 namespace_object_aliases: vec![],
185 }
186 }
187}
188
189impl ResolvedModule {
190 pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
196 self.resolved_imports
197 .iter()
198 .chain(self.resolved_dynamic_imports.iter())
199 }
200
201 pub fn all_resolved_source_edges(&self) -> impl Iterator<Item = ResolvedSourceEdge<'_>> {
207 self.resolved_imports
208 .iter()
209 .map(ResolvedSourceEdge::Import)
210 .chain(
211 self.resolved_dynamic_imports
212 .iter()
213 .map(ResolvedSourceEdge::Import),
214 )
215 .chain(self.re_exports.iter().map(ResolvedSourceEdge::ReExport))
216 }
217}
218
219pub(super) struct ResolveContext<'a> {
224 pub resolver: &'a Resolver,
226 pub style_resolver: &'a Resolver,
230 pub extensions: &'a [String],
232 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
234 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
236 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
238 pub package_manifests: &'a [PackageManifestInfo],
240 pub condition_names: &'a [String],
242 pub path_aliases: &'a [(String, String)],
244 pub scss_include_paths: &'a [PathBuf],
248 pub static_dir_mappings: &'a [(PathBuf, String)],
251 pub root: &'a Path,
253 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
257 pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
262}
263
264#[derive(Debug, Clone)]
266pub(super) struct PackageManifestInfo {
267 pub root: PathBuf,
269 pub canonical_root: PathBuf,
271 pub name: Option<String>,
273 pub package_json: fallow_config::PackageJson,
275}
276
277pub(super) struct CanonicalFallback<'a> {
279 files: &'a [fallow_types::discover::DiscoveredFile],
280 map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
281}
282
283impl<'a> CanonicalFallback<'a> {
284 pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
285 Self {
286 files,
287 map: std::sync::OnceLock::new(),
288 }
289 }
290
291 pub fn get(&self, canonical: &Path) -> Option<FileId> {
293 let map = self.map.get_or_init(|| {
294 tracing::debug!(
295 "intra-project symlinks detected, building canonical path index ({} files)",
296 self.files.len()
297 );
298 self.files
299 .iter()
300 .filter_map(|f| {
301 dunce::canonicalize(&f.path)
302 .ok()
303 .map(|canonical| (canonical, f.id))
304 })
305 .collect()
306 });
307 map.get(canonical).copied()
308 }
309}
310
311#[cfg(all(test, not(miri)))]
312mod tests {
313 use super::*;
314 use fallow_types::discover::DiscoveredFile;
315
316 #[test]
317 fn canonical_fallback_returns_none_for_empty_files() {
318 let files: Vec<DiscoveredFile> = vec![];
319 let fallback = CanonicalFallback::new(&files);
320 assert!(fallback.get(Path::new("/nonexistent")).is_none());
321 }
322
323 #[test]
324 fn canonical_fallback_finds_existing_file() {
325 let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
326 let _ = std::fs::create_dir_all(&temp);
327 let test_file = temp.join("test.ts");
328 std::fs::write(&test_file, "").unwrap();
329
330 let files = vec![DiscoveredFile {
331 id: FileId(42),
332 path: test_file.clone(),
333 size_bytes: 0,
334 }];
335 let fallback = CanonicalFallback::new(&files);
336
337 let canonical = dunce::canonicalize(&test_file).unwrap();
338 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
339
340 assert_eq!(fallback.get(&canonical), Some(FileId(42)));
342
343 let _ = std::fs::remove_dir_all(&temp);
344 }
345
346 #[test]
347 fn canonical_fallback_returns_none_for_missing_path() {
348 let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
349 let _ = std::fs::create_dir_all(&temp);
350 let test_file = temp.join("exists.ts");
351 std::fs::write(&test_file, "").unwrap();
352
353 let files = vec![DiscoveredFile {
354 id: FileId(1),
355 path: test_file,
356 size_bytes: 0,
357 }];
358 let fallback = CanonicalFallback::new(&files);
359 assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
360
361 let _ = std::fs::remove_dir_all(&temp);
362 }
363}
364
365pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
370
371pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
373
374pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];