use std::{borrow::Cow, path::Path};
use compact_str::CompactString;
use crate::{
Alias, AliasValue, CachedPath, ResolveError, ResolverImpl, TsConfig,
context::ResolveContext as Ctx,
path::{PathUtil, SLASH_START},
};
pub type CompiledAlias = Vec<CompiledAliasEntry>;
#[derive(Clone)]
pub struct CompiledAliasEntry {
key: CompactString,
match_kind: AliasMatchKind,
specifiers: Vec<AliasValue>,
match_first_byte: Option<u8>,
}
#[derive(Clone)]
pub enum AliasMatchKind {
Exact,
Prefix,
Wildcard { prefix: CompactString, suffix: CompactString },
}
pub fn compile_alias(aliases: &Alias) -> CompiledAlias {
aliases
.iter()
.map(|(key, specifiers)| {
let (key, match_kind) = key.strip_suffix('$').map_or_else(
|| {
if let Some((prefix, suffix)) = key.split_once('*') {
(
CompactString::new(key),
AliasMatchKind::Wildcard {
prefix: CompactString::new(prefix),
suffix: CompactString::new(suffix),
},
)
} else {
(CompactString::new(key), AliasMatchKind::Prefix)
}
},
|stripped_key| (CompactString::new(stripped_key), AliasMatchKind::Exact),
);
let match_first_byte = match &match_kind {
AliasMatchKind::Exact | AliasMatchKind::Prefix => key.as_bytes().first().copied(),
AliasMatchKind::Wildcard { prefix, .. } => prefix.as_bytes().first().copied(),
};
CompiledAliasEntry { key, match_kind, specifiers: specifiers.clone(), match_first_byte }
})
.collect()
}
impl CompiledAliasEntry {
pub(crate) fn key_matches(&self, specifier: &[u8]) -> bool {
if let Some(required) = self.match_first_byte
&& Some(required) != specifier.first().copied()
{
return false;
}
match &self.match_kind {
AliasMatchKind::Exact => self.key.as_bytes() == specifier,
AliasMatchKind::Wildcard { .. } => true,
AliasMatchKind::Prefix => specifier
.strip_prefix(self.key.as_bytes())
.is_some_and(|tail| tail.is_empty() || matches!(tail.first(), Some(b'/' | b'\\'))),
}
}
}
impl ResolverImpl {
pub(super) fn load_alias_by_options(
&self,
cached_path: &CachedPath,
specifier: &str,
aliases: &Alias,
tsconfig: Option<&TsConfig>,
ctx: &mut Ctx,
) -> Result<Option<CachedPath>, ResolveError> {
let compiled_aliases = compile_alias(aliases);
self.load_alias(cached_path, specifier, &compiled_aliases, tsconfig, ctx)
}
pub(super) fn load_alias(
&self,
cached_path: &CachedPath,
specifier: &str,
aliases: &CompiledAlias,
tsconfig: Option<&TsConfig>,
ctx: &mut Ctx,
) -> Result<Option<CachedPath>, ResolveError> {
for alias in aliases {
if !alias.key_matches(specifier.as_bytes()) {
continue;
}
let alias_key = alias.key.as_str();
let mut should_stop = false;
for r in &alias.specifiers {
match r {
AliasValue::Path(alias_value) => {
if let Some(path) = self.load_alias_value(
cached_path,
alias_key,
&alias.match_kind,
alias_value,
specifier,
tsconfig,
ctx,
&mut should_stop,
)? {
return Ok(Some(path));
}
}
AliasValue::Ignore => {
let cached_path = cached_path.normalize_with(alias_key, &self.cache);
return Err(ResolveError::Ignored(cached_path.to_path_buf()));
}
}
}
if should_stop {
return Err(ResolveError::MatchedAliasNotFound(
specifier.to_string(),
alias_key.to_string(),
));
}
}
Ok(None)
}
fn load_alias_value(
&self,
cached_path: &CachedPath,
alias_key: &str,
alias_match_kind: &AliasMatchKind,
alias_value: &str,
request: &str,
tsconfig: Option<&TsConfig>,
ctx: &mut Ctx,
should_stop: &mut bool,
) -> Result<Option<CachedPath>, ResolveError> {
if request != alias_value
&& !request.strip_prefix(alias_value).is_some_and(|prefix| prefix.starts_with('/'))
{
let new_specifier = match alias_match_kind {
AliasMatchKind::Wildcard { prefix, suffix } => {
let Some(alias_key) = request
.strip_prefix(prefix.as_str())
.and_then(|specifier| specifier.strip_suffix(suffix.as_str()))
else {
return Ok(None);
};
if alias_value.contains('*') {
Cow::Owned(alias_value.replacen('*', alias_key, 1))
} else {
Cow::Borrowed(alias_value)
}
}
AliasMatchKind::Exact | AliasMatchKind::Prefix => {
let tail = &request[alias_key.len()..];
if tail.is_empty() {
Cow::Borrowed(alias_value)
} else {
let alias_path = Path::new(alias_value).normalize();
let cached_alias_path = self.cache.value(&alias_path);
if self.is_file(&cached_alias_path, ctx) {
return Ok(None);
}
let tail = tail.trim_start_matches(SLASH_START);
if tail.is_empty() {
Cow::Borrowed(alias_value)
} else {
let normalized = alias_path.normalize_with(tail);
Cow::Owned(normalized.to_string_lossy().to_string())
}
}
}
};
*should_stop = true;
ctx.with_fully_specified(false);
return match self.require(cached_path, new_specifier.as_ref(), tsconfig, ctx) {
Err(ResolveError::NotFound(_) | ResolveError::MatchedAliasNotFound(_, _)) => {
Ok(None)
}
Ok(path) => Ok(Some(path)),
Err(err) => Err(err),
};
}
Ok(None)
}
}