use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use fallow_types::discover::FileId;
use rustc_hash::FxHashMap;
use super::ModuleGraph;
impl ModuleGraph {
#[must_use]
pub fn package_entry_resolver<'a>(
&'a self,
package_root: &'a Path,
) -> PackageEntryResolver<'a> {
PackageEntryResolver {
graph: self,
package_root,
canonical_modules: OnceLock::new(),
}
}
}
pub struct PackageEntryResolver<'a> {
graph: &'a ModuleGraph,
package_root: &'a Path,
canonical_modules: OnceLock<FxHashMap<PathBuf, FileId>>,
}
impl PackageEntryResolver<'_> {
#[must_use]
pub fn file_id(
&self,
entry_path: &Path,
lookup: impl Fn(&Path) -> Option<FileId>,
) -> Option<FileId> {
lookup(entry_path).or_else(|| {
let canonical = dunce::canonicalize(entry_path).ok()?;
lookup(&canonical).or_else(|| {
self.canonical_modules
.get_or_init(|| {
canonical_module_index(
self.graph
.modules
.iter()
.map(|module| (module.path.as_path(), module.file_id)),
self.package_root,
)
})
.get(&canonical)
.copied()
})
})
}
}
fn canonical_module_index<'a>(
candidates: impl Iterator<Item = (&'a Path, FileId)>,
package_root: &Path,
) -> FxHashMap<PathBuf, FileId> {
let mut canonical_dirs = FxHashMap::default();
let mut index = FxHashMap::default();
for (path, file_id) in candidates.filter(|(path, _)| path.starts_with(package_root)) {
if let Some(canonical) = canonical_module_path(path, &mut canonical_dirs) {
index.entry(canonical).or_insert(file_id);
}
}
index
}
fn canonical_module_path(
path: &Path,
canonical_dirs: &mut FxHashMap<PathBuf, Option<PathBuf>>,
) -> Option<PathBuf> {
let metadata = std::fs::symlink_metadata(path).ok()?;
let (Some(parent), Some(name)) = (path.parent(), path.file_name()) else {
return dunce::canonicalize(path).ok();
};
if metadata.file_type().is_symlink() || parent.as_os_str().is_empty() {
return dunce::canonicalize(path).ok();
}
canonical_dirs
.entry(parent.to_path_buf())
.or_insert_with(|| dunce::canonicalize(parent).ok())
.as_ref()
.map(|dir| dir.join(name))
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use crate::resolve::ResolvedModule;
use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource};
fn graph_of(paths: &[PathBuf]) -> ModuleGraph {
let files: Vec<DiscoveredFile> = paths
.iter()
.zip(0..)
.map(|(path, id)| DiscoveredFile {
id: FileId(id),
path: path.clone(),
size_bytes: 1,
})
.collect();
let resolved: Vec<ResolvedModule> = files
.iter()
.map(|file| ResolvedModule {
file_id: file.id,
path: file.path.clone(),
..Default::default()
})
.collect();
let entry_points = vec![EntryPoint {
path: paths[0].clone(),
source: EntryPointSource::PackageJsonMain,
}];
ModuleGraph::build(&resolved, &entry_points, &files)
}
#[cfg_attr(miri, ignore)]
#[test]
fn resolver_builds_the_canonical_index_once_per_package() {
let dir = tempfile::tempdir().unwrap();
let real_dir = dir.path().join("real");
std::fs::create_dir(&real_dir).unwrap();
let real_file = real_dir.join("mod.ts");
std::fs::write(&real_file, "").unwrap();
let link_dir = dir.path().join("link");
std::os::unix::fs::symlink(&real_dir, &link_dir).unwrap();
let bin = dir.path().join("bin");
std::fs::write(&bin, "").unwrap();
let graph = graph_of(&[link_dir.join("mod.ts")]);
let by_path: FxHashMap<PathBuf, FileId> = graph
.modules
.iter()
.map(|module| (module.path.clone(), module.file_id))
.collect();
let lookup = |path: &Path| by_path.get(path).copied();
let resolver = graph.package_entry_resolver(dir.path());
assert_eq!(resolver.file_id(&bin, lookup), None);
assert_eq!(resolver.file_id(&real_file, lookup), Some(FileId(0)));
std::fs::remove_file(&link_dir).unwrap();
assert_eq!(resolver.file_id(&real_file, lookup), Some(FileId(0)));
let fresh = graph.package_entry_resolver(dir.path());
assert_eq!(fresh.file_id(&real_file, lookup), None);
}
#[cfg_attr(miri, ignore)]
#[test]
fn scoped_canonical_index_matches_module_reached_through_symlink() {
let dir = tempfile::tempdir().unwrap();
let real_dir = dir.path().join("real");
std::fs::create_dir(&real_dir).unwrap();
let real_file = real_dir.join("mod.ts");
std::fs::write(&real_file, "export const x = 1;\n").unwrap();
let link_dir = dir.path().join("link");
std::os::unix::fs::symlink(&real_dir, &link_dir).unwrap();
let module_raw_path = link_dir.join("mod.ts");
let canonical_entry = dunce::canonicalize(&real_file).unwrap();
let package_root = dir.path();
let candidates = [(module_raw_path.as_path(), FileId(7))];
let index = canonical_module_index(candidates.iter().copied(), package_root);
assert_eq!(index.get(&canonical_entry), Some(&FileId(7)));
let outside_root = dir.path().join("other-package");
let index = canonical_module_index(candidates.iter().copied(), &outside_root);
assert!(index.is_empty());
let unrelated = dunce::canonicalize(dir.path()).unwrap().join("nope.ts");
let index = canonical_module_index(candidates.iter().copied(), package_root);
assert_eq!(index.get(&unrelated), None);
}
#[cfg_attr(miri, ignore)]
#[test]
fn canonical_index_resolves_symlinked_module_files() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target.ts");
std::fs::write(&target, "export const x = 1;\n").unwrap();
let link = dir.path().join("link.ts");
std::os::unix::fs::symlink(&target, &link).unwrap();
let missing = dir.path().join("missing.ts");
let candidates = [
(link.as_path(), FileId(1)),
(target.as_path(), FileId(2)),
(missing.as_path(), FileId(3)),
];
let index = canonical_module_index(candidates.iter().copied(), dir.path());
assert_eq!(index.len(), 1);
assert_eq!(
index.get(&dunce::canonicalize(&target).unwrap()),
Some(&FileId(1))
);
}
#[cfg_attr(miri, ignore)]
#[test]
fn canonical_module_path_matches_full_canonicalization() {
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("a/b");
std::fs::create_dir_all(&nested).unwrap();
let file = nested.join("mod.ts");
std::fs::write(&file, "").unwrap();
let link_dir = dir.path().join("via");
std::os::unix::fs::symlink(&nested, &link_dir).unwrap();
let mut canonical_dirs = FxHashMap::default();
for path in [
file,
link_dir.join("mod.ts"),
dir.path().join("a/b/../b/mod.ts"),
] {
assert_eq!(
canonical_module_path(&path, &mut canonical_dirs),
dunce::canonicalize(&path).ok(),
"{}",
path.display()
);
}
assert_eq!(
canonical_module_path(&dir.path().join("a/none.ts"), &mut canonical_dirs),
None
);
}
}