use rustc_hash::FxHashMap;
use rustc_hash::FxHashSet;
use std::path::Path;
const TS_EXTENSIONS: &[&str] = &[
".d.ts", ".d.tsx", ".d.mts", ".d.cts", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs",
".cjs",
];
fn strip_ts_extension(path: &str) -> &str {
for ext in TS_EXTENSIONS {
if let Some(stripped) = path.strip_suffix(ext) {
return stripped;
}
}
path
}
fn relative_specifier(from_dir: &Path, to_path: &Path) -> Option<String> {
if let Ok(rel) = to_path.strip_prefix(from_dir) {
let rel_str = rel.to_string_lossy();
let without_ext = strip_ts_extension(&rel_str);
return Some(format!("./{without_ext}"));
}
let mut up_count = 0;
let mut ancestor = from_dir;
loop {
match ancestor.parent() {
Some(parent) => {
up_count += 1;
if let Ok(rel) = to_path.strip_prefix(parent) {
let rel_str = rel.to_string_lossy();
let without_ext = strip_ts_extension(&rel_str);
let prefix = "../".repeat(up_count);
return Some(format!("{prefix}{without_ext}"));
}
ancestor = parent;
}
None => return None,
}
}
}
pub fn build_module_resolution_maps(
file_names: &[String],
) -> (FxHashMap<(usize, String), usize>, FxHashSet<String>) {
let mut resolved_module_paths: FxHashMap<(usize, String), usize> = FxHashMap::default();
let mut resolved_modules: FxHashSet<String> = FxHashSet::default();
let mut stem_to_idx: FxHashMap<String, usize> = FxHashMap::default();
for (idx, name) in file_names.iter().enumerate() {
let without_ext = strip_ts_extension(name);
stem_to_idx.insert(without_ext.to_string(), idx);
}
for (src_idx, src_name) in file_names.iter().enumerate() {
let src_path = Path::new(src_name);
let Some(src_dir) = src_path.parent() else {
continue;
};
for (tgt_idx, tgt_name) in file_names.iter().enumerate() {
if src_idx == tgt_idx {
continue;
}
let tgt_path = Path::new(tgt_name);
if let Some(specifier) = relative_specifier(src_dir, tgt_path) {
resolved_module_paths.insert((src_idx, specifier.clone()), tgt_idx);
resolved_modules.insert(specifier.clone());
if let Some(bare) = specifier.strip_prefix("./") {
resolved_module_paths.insert((src_idx, bare.to_string()), tgt_idx);
resolved_modules.insert(bare.to_string());
}
}
let tgt_stem = strip_ts_extension(tgt_name);
if let Some(dir_path) = tgt_stem.strip_suffix("/index") {
let dir_as_path = Path::new(dir_path);
if let Some(dir_specifier) = relative_specifier(src_dir, dir_as_path) {
resolved_module_paths.insert((src_idx, dir_specifier.clone()), tgt_idx);
resolved_modules.insert(dir_specifier.clone());
if let Some(bare) = dir_specifier.strip_prefix("./") {
resolved_module_paths.insert((src_idx, bare.to_string()), tgt_idx);
resolved_modules.insert(bare.to_string());
}
}
}
}
}
(resolved_module_paths, resolved_modules)
}
pub fn module_specifier_candidates(specifier: &str) -> Vec<String> {
let mut candidates = Vec::with_capacity(5);
let mut push_unique = |value: String| {
if !candidates.contains(&value) {
candidates.push(value);
}
};
push_unique(specifier.to_string());
let trimmed = specifier.trim().trim_matches('"').trim_matches('\'');
if trimmed != specifier {
push_unique(trimmed.to_string());
}
if !trimmed.is_empty() {
push_unique(format!("\"{trimmed}\""));
push_unique(format!("'{trimmed}'"));
if trimmed.contains('\\') {
push_unique(trimmed.replace('\\', "/"));
}
}
candidates
}
#[cfg(test)]
#[path = "../tests/module_resolution.rs"]
mod tests;