use std::path::{Path, PathBuf};
use std::sync::Mutex;
use oxc_resolver::Resolver;
use rustc_hash::{FxHashMap, FxHashSet};
use fallow_types::discover::FileId;
#[derive(Debug, Clone)]
pub enum ResolveResult {
InternalModule(FileId),
ExternalFile(PathBuf),
NpmPackage(String),
Unresolvable(String),
}
#[derive(Debug, Clone)]
pub struct ResolvedImport {
pub info: fallow_types::extract::ImportInfo,
pub target: ResolveResult,
}
#[derive(Debug, Clone)]
pub struct ResolvedReExport {
pub info: fallow_types::extract::ReExportInfo,
pub target: ResolveResult,
}
#[derive(Debug)]
pub struct ResolvedModule {
pub file_id: FileId,
pub path: PathBuf,
pub exports: Vec<fallow_types::extract::ExportInfo>,
pub re_exports: Vec<ResolvedReExport>,
pub resolved_imports: Vec<ResolvedImport>,
pub resolved_dynamic_imports: Vec<ResolvedImport>,
pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
pub whole_object_uses: Vec<String>,
pub has_cjs_exports: bool,
pub unused_import_bindings: FxHashSet<String>,
}
impl Default for ResolvedModule {
fn default() -> Self {
Self {
file_id: FileId(0),
path: PathBuf::new(),
exports: vec![],
re_exports: vec![],
resolved_imports: vec![],
resolved_dynamic_imports: vec![],
resolved_dynamic_patterns: vec![],
member_accesses: vec![],
whole_object_uses: vec![],
has_cjs_exports: false,
unused_import_bindings: FxHashSet::default(),
}
}
}
pub(super) struct ResolveContext<'a> {
pub resolver: &'a Resolver,
pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
pub path_aliases: &'a [(String, String)],
pub scss_include_paths: &'a [PathBuf],
pub root: &'a Path,
pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
}
pub(super) struct CanonicalFallback<'a> {
files: &'a [fallow_types::discover::DiscoveredFile],
map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
}
impl<'a> CanonicalFallback<'a> {
pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
Self {
files,
map: std::sync::OnceLock::new(),
}
}
pub fn get(&self, canonical: &Path) -> Option<FileId> {
let map = self.map.get_or_init(|| {
tracing::debug!(
"intra-project symlinks detected — building canonical path index ({} files)",
self.files.len()
);
self.files
.iter()
.filter_map(|f| {
dunce::canonicalize(&f.path)
.ok()
.map(|canonical| (canonical, f.id))
})
.collect()
});
map.get(canonical).copied()
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use fallow_types::discover::DiscoveredFile;
#[test]
fn canonical_fallback_returns_none_for_empty_files() {
let files: Vec<DiscoveredFile> = vec![];
let fallback = CanonicalFallback::new(&files);
assert!(fallback.get(Path::new("/nonexistent")).is_none());
}
#[test]
fn canonical_fallback_finds_existing_file() {
let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
let _ = std::fs::create_dir_all(&temp);
let test_file = temp.join("test.ts");
std::fs::write(&test_file, "").unwrap();
let files = vec![DiscoveredFile {
id: FileId(42),
path: test_file.clone(),
size_bytes: 0,
}];
let fallback = CanonicalFallback::new(&files);
let canonical = dunce::canonicalize(&test_file).unwrap();
assert_eq!(fallback.get(&canonical), Some(FileId(42)));
assert_eq!(fallback.get(&canonical), Some(FileId(42)));
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn canonical_fallback_returns_none_for_missing_path() {
let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
let _ = std::fs::create_dir_all(&temp);
let test_file = temp.join("exists.ts");
std::fs::write(&test_file, "").unwrap();
let files = vec![DiscoveredFile {
id: FileId(1),
path: test_file,
size_bytes: 0,
}];
let fallback = CanonicalFallback::new(&files);
assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
let _ = std::fs::remove_dir_all(&temp);
}
}
pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];