use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TsconfigPathsHook {
pub base_url: PathBuf,
pub paths: BTreeMap<String, Vec<String>>,
}
impl TsconfigPathsHook {
pub fn new(base_url: impl Into<PathBuf>, paths: BTreeMap<String, Vec<String>>) -> Self {
Self {
base_url: base_url.into(),
paths,
}
}
pub fn rewrite_module_specifier(
&self,
source_file: impl AsRef<Path>,
module_specifier: &str,
) -> Option<String> {
if module_specifier.starts_with('.') || module_specifier.starts_with('/') {
return None;
}
let target = self.resolve_alias(module_specifier)?;
let source_dir = source_file
.as_ref()
.parent()
.unwrap_or_else(|| Path::new(""));
let relative = make_relative_posix(source_dir, &target);
Some(if relative.starts_with('.') {
relative
} else {
format!("./{relative}")
})
}
pub fn resolve_alias(&self, module_specifier: &str) -> Option<PathBuf> {
self.paths.iter().find_map(|(pattern, replacements)| {
match_pattern(pattern, module_specifier).and_then(|matched| {
replacements.first().map(|replacement| {
let replaced = replacement.replace('*', &matched);
self.base_url.join(replaced)
})
})
})
}
}
pub fn tsconfig_paths_before_hook_factory(
base_url: impl Into<PathBuf>,
paths: BTreeMap<String, Vec<String>>,
) -> TsconfigPathsHook {
TsconfigPathsHook::new(base_url, paths)
}
fn match_pattern(pattern: &str, text: &str) -> Option<String> {
if pattern == text {
return Some(String::new());
}
let (prefix, suffix) = pattern.split_once('*')?;
text.strip_prefix(prefix)
.and_then(|value| value.strip_suffix(suffix))
.map(ToString::to_string)
}
fn make_relative_posix(from: &Path, to: &Path) -> String {
let from_components: Vec<_> = from.components().collect();
let to_components: Vec<_> = to.components().collect();
let common = from_components
.iter()
.zip(&to_components)
.take_while(|(a, b)| a == b)
.count();
let mut parts = Vec::new();
for _ in common..from_components.len() {
parts.push("..".to_string());
}
parts.extend(
to_components[common..]
.iter()
.map(|component| component.as_os_str().to_string_lossy().replace('\\', "/")),
);
if parts.is_empty() {
".".to_string()
} else {
parts.join("/")
}
}