mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(resolver): handle path alias with # (#739)
`#` can be: * an actual path fragment `path#fragment` * esm import module specifier `#import-path` * part of a path `path/to/#/fragment` * part of path alias `#` -> `./path/alias` This is driving me crazy.
This commit is contained in:
parent
7c3e29d421
commit
4fa6aafa3e
3 changed files with 72 additions and 73 deletions
|
|
@ -43,7 +43,7 @@ use crate::{
|
|||
file_system::FileSystemOs,
|
||||
package_json::{ExportsField, ExportsKey, MatchObject},
|
||||
path::PathUtil,
|
||||
specifier::{Specifier, SpecifierKind},
|
||||
specifier::Specifier,
|
||||
};
|
||||
pub use crate::{
|
||||
error::{JSONError, ResolveError},
|
||||
|
|
@ -169,7 +169,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
let specifier = Specifier::parse(specifier).map_err(ResolveError::Specifier)?;
|
||||
ctx.with_query_fragment(specifier.query, specifier.fragment);
|
||||
let cached_path = self.cache.value(path);
|
||||
let cached_path = self.require(&cached_path, &specifier, &ctx).or_else(|err| {
|
||||
let cached_path = self.require(&cached_path, specifier.path(), &ctx).or_else(|err| {
|
||||
// enhanced-resolve: try fallback
|
||||
self.load_alias(&cached_path, Some(specifier.path()), &self.options.fallback, &ctx)
|
||||
.and_then(|value| value.ok_or(err))
|
||||
|
|
@ -192,7 +192,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
fn require(
|
||||
&self,
|
||||
cached_path: &CachedPath,
|
||||
specifier: &Specifier,
|
||||
specifier: &str,
|
||||
ctx: &ResolveContext,
|
||||
) -> Result<CachedPath, ResolveError> {
|
||||
ctx.test_for_infinite_recursion()?;
|
||||
|
|
@ -204,27 +204,26 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
|
||||
// enhanced-resolve: try alias
|
||||
if let Some(path) =
|
||||
self.load_alias(cached_path, Some(specifier.path()), &self.options.alias, ctx)?
|
||||
self.load_alias(cached_path, Some(specifier), &self.options.alias, ctx)?
|
||||
{
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let specifier_str = specifier.path();
|
||||
match specifier.kind {
|
||||
match specifier.as_bytes()[0] {
|
||||
// 1. If X is a core module,
|
||||
// a. return the core module
|
||||
// b. STOP
|
||||
// 2. If X begins with '/'
|
||||
// a. set Y to be the file system root
|
||||
SpecifierKind::Absolute => self.require_absolute(cached_path, specifier_str, ctx),
|
||||
b'/' => self.require_absolute(cached_path, specifier, ctx),
|
||||
// 3. If X begins with './' or '/' or '../'
|
||||
SpecifierKind::Relative => self.require_relative(cached_path, specifier_str, ctx),
|
||||
b'.' => self.require_relative(cached_path, specifier, ctx),
|
||||
// 4. If X begins with '#'
|
||||
SpecifierKind::Hash => self.require_hash(cached_path, specifier_str, ctx),
|
||||
b'#' => self.require_hash(cached_path, specifier, ctx),
|
||||
// (ESM) 5. Otherwise,
|
||||
// Note: specifier is now a bare specifier.
|
||||
// Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
|
||||
SpecifierKind::Bare => self.require_bare(cached_path, specifier_str, ctx),
|
||||
_ => self.require_bare(cached_path, specifier, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -319,15 +318,13 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
fn try_fragment_as_path(
|
||||
&self,
|
||||
cached_path: &CachedPath,
|
||||
specifier: &Specifier,
|
||||
specifier: &str,
|
||||
ctx: &ResolveContext,
|
||||
) -> Option<CachedPath> {
|
||||
if ctx.borrow().fragment.is_some() && ctx.borrow().query.is_none() {
|
||||
let fragment = ctx.borrow_mut().fragment.take().unwrap();
|
||||
let path = format!("{}{fragment}", specifier.path());
|
||||
let mut specifier = specifier.clone();
|
||||
specifier.set_path(&path);
|
||||
if let Ok(path) = self.require(cached_path, &specifier, ctx) {
|
||||
let path = format!("{specifier}{fragment}");
|
||||
if let Ok(path) = self.require(cached_path, &path, ctx) {
|
||||
return Some(path);
|
||||
}
|
||||
ctx.borrow_mut().fragment.replace(fragment);
|
||||
|
|
@ -689,7 +686,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
ctx.with_resolving_alias(specifier.path().to_string());
|
||||
ctx.with_fully_specified(false);
|
||||
let cached_path = self.cache.value(package_json.directory());
|
||||
self.require(&cached_path, &specifier, ctx).map(Some)
|
||||
self.require(&cached_path, specifier.path(), ctx).map(Some)
|
||||
}
|
||||
|
||||
/// enhanced-resolve: AliasPlugin for [ResolveOptions::alias] and [ResolveOptions::fallback].
|
||||
|
|
@ -712,27 +709,37 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
}
|
||||
for r in specifiers {
|
||||
match r {
|
||||
AliasValue::Path(alias) => {
|
||||
let specifier = Specifier::parse(alias).map_err(ResolveError::Specifier)?;
|
||||
let alias = specifier.path();
|
||||
if inner_request.as_ref() != alias
|
||||
&& !inner_request
|
||||
.strip_prefix(alias)
|
||||
.is_some_and(|prefix| prefix.starts_with('/'))
|
||||
{
|
||||
let new_specifier =
|
||||
format!("{alias}{}", &inner_request[alias_key.len()..]);
|
||||
let new_specifier = Specifier::parse(&new_specifier)
|
||||
.map_err(ResolveError::Specifier)?;
|
||||
ctx.with_fully_specified(false);
|
||||
// Alias may contain `?query`, pass it along.
|
||||
ctx.with_query_fragment(specifier.query, specifier.fragment);
|
||||
match self.require(cached_path, &new_specifier, ctx) {
|
||||
Err(ResolveError::NotFound(_)) => { /* noop */ }
|
||||
Ok(path) => return Ok(Some(path)),
|
||||
Err(err) => return Err(err),
|
||||
AliasValue::Path(alias_value) => {
|
||||
let specifier =
|
||||
Specifier::parse(alias_value).map_err(ResolveError::Specifier)?;
|
||||
|
||||
// `#` can be a fragment or a path, try fragment as path first
|
||||
if specifier.query.is_none() && specifier.fragment.is_some() {
|
||||
if let Some(path) = self.load_alias_value(
|
||||
cached_path,
|
||||
alias_key,
|
||||
alias_value, // pass in original alias value, not parsed
|
||||
inner_request.as_ref(),
|
||||
ctx,
|
||||
)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
|
||||
// Then try path without query and fragment
|
||||
let old_query = ctx.borrow().query.clone();
|
||||
let old_fragment = ctx.borrow().fragment.clone();
|
||||
ctx.with_query_fragment(specifier.query, specifier.fragment);
|
||||
if let Some(path) = self.load_alias_value(
|
||||
cached_path,
|
||||
alias_key,
|
||||
specifier.path(), // pass in passed alias value
|
||||
inner_request.as_ref(),
|
||||
ctx,
|
||||
)? {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
ctx.with_query_fragment(old_query.as_deref(), old_fragment.as_deref());
|
||||
}
|
||||
AliasValue::Ignore => {
|
||||
let path = cached_path.path().normalize_with(alias_key);
|
||||
|
|
@ -744,6 +751,28 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
fn load_alias_value(
|
||||
&self,
|
||||
cached_path: &CachedPath,
|
||||
alias_key: &str,
|
||||
alias_value: &str,
|
||||
request: &str,
|
||||
ctx: &ResolveContext,
|
||||
) -> ResolveState {
|
||||
if request != alias_value
|
||||
&& !request.strip_prefix(alias_value).is_some_and(|prefix| prefix.starts_with('/'))
|
||||
{
|
||||
let new_specifier = format!("{alias_value}{}", &request[alias_key.len()..]);
|
||||
ctx.with_fully_specified(false);
|
||||
return match self.require(cached_path, &new_specifier, ctx) {
|
||||
Err(ResolveError::NotFound(_)) => Ok(None),
|
||||
Ok(path) => return Ok(Some(path)),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Given an extension alias map `{".js": [".ts", "js"]}`,
|
||||
/// load the mapping instead of the provided extension
|
||||
///
|
||||
|
|
@ -823,7 +852,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
let specifier = Specifier::parse(&subpath).map_err(ResolveError::Specifier)?;
|
||||
ctx.with_fully_specified(false);
|
||||
ctx.with_query_fragment(specifier.query, specifier.fragment);
|
||||
return self.require(&cached_path, &specifier, ctx).map(Some);
|
||||
return self.require(&cached_path, specifier.path(), ctx).map(Some);
|
||||
}
|
||||
parent_url.pop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,47 +4,25 @@ use std::borrow::Cow;
|
|||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Specifier<'a> {
|
||||
path: Cow<'a, str>,
|
||||
pub kind: SpecifierKind,
|
||||
pub query: Option<&'a str>,
|
||||
pub fragment: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum SpecifierKind {
|
||||
/// `/path`
|
||||
Absolute,
|
||||
|
||||
/// `./path`, `../path`
|
||||
Relative,
|
||||
|
||||
/// `#path`
|
||||
Hash,
|
||||
|
||||
/// Specifier without any leading syntax is called a bare specifier.
|
||||
Bare,
|
||||
}
|
||||
|
||||
impl<'a> Specifier<'a> {
|
||||
pub fn path(&'a self) -> &'a str {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_path(&mut self, path: &'a str) {
|
||||
self.path = Cow::Borrowed(path);
|
||||
}
|
||||
|
||||
pub fn parse(specifier: &'a str) -> Result<Specifier<'a>, SpecifierError> {
|
||||
if specifier.is_empty() {
|
||||
return Err(SpecifierError::Empty);
|
||||
}
|
||||
let (kind, offset) = match specifier.as_bytes()[0] {
|
||||
b'/' => (SpecifierKind::Absolute, 1),
|
||||
b'.' => (SpecifierKind::Relative, 1),
|
||||
b'#' => (SpecifierKind::Hash, 1),
|
||||
_ => (SpecifierKind::Bare, 0),
|
||||
let offset = match specifier.as_bytes()[0] {
|
||||
b'/' | b'.' | b'#' => 1,
|
||||
_ => 0,
|
||||
};
|
||||
let (path, query, fragment) = Self::parse_query_framgment(specifier, offset);
|
||||
Ok(Self { path, kind, query, fragment })
|
||||
Ok(Self { path, query, fragment })
|
||||
}
|
||||
|
||||
fn parse_query_framgment(
|
||||
|
|
@ -99,13 +77,7 @@ impl<'a> Specifier<'a> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Specifier, SpecifierError, SpecifierKind};
|
||||
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn size_asserts() {
|
||||
static_assertions::assert_eq_size!(Specifier, [u8; 64]);
|
||||
}
|
||||
use super::{Specifier, SpecifierError};
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
|
|
@ -118,7 +90,6 @@ mod tests {
|
|||
let specifier = "/test?#";
|
||||
let parsed = Specifier::parse(specifier)?;
|
||||
assert_eq!(parsed.path, "/test");
|
||||
assert_eq!(parsed.kind, SpecifierKind::Absolute);
|
||||
assert_eq!(parsed.query, Some("?"));
|
||||
assert_eq!(parsed.fragment, Some("#"));
|
||||
Ok(())
|
||||
|
|
@ -132,7 +103,6 @@ mod tests {
|
|||
r.push_str("?#");
|
||||
let parsed = Specifier::parse(&r)?;
|
||||
assert_eq!(parsed.path, specifier);
|
||||
assert_eq!(parsed.kind, SpecifierKind::Relative);
|
||||
assert_eq!(parsed.query, Some("?"));
|
||||
assert_eq!(parsed.fragment, Some("#"));
|
||||
}
|
||||
|
|
@ -147,7 +117,6 @@ mod tests {
|
|||
r.push_str("?#");
|
||||
let parsed = Specifier::parse(&r)?;
|
||||
assert_eq!(parsed.path, specifier);
|
||||
assert_eq!(parsed.kind, SpecifierKind::Hash);
|
||||
assert_eq!(parsed.query, Some("?"));
|
||||
assert_eq!(parsed.fragment, Some("#"));
|
||||
}
|
||||
|
|
@ -162,7 +131,6 @@ mod tests {
|
|||
r.push_str("?#");
|
||||
let parsed = Specifier::parse(&r)?;
|
||||
assert_eq!(parsed.path, specifier);
|
||||
assert_eq!(parsed.kind, SpecifierKind::Bare);
|
||||
assert_eq!(parsed.query, Some("?"));
|
||||
assert_eq!(parsed.fragment, Some("#"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ fn alias() {
|
|||
("ignored".into(), vec![AliasValue::Ignore]),
|
||||
// not part of enhanced-resolve, added to make sure query in alias value works
|
||||
("alias_query".into(), vec![AliasValue::Path("a?query_after".into())]),
|
||||
("alias_fragment".into(), vec![AliasValue::Path("a#fragment_after".into())]),
|
||||
],
|
||||
modules: vec!["/".into()],
|
||||
..ResolveOptions::default()
|
||||
|
|
@ -92,6 +93,7 @@ fn alias() {
|
|||
("should resolve a file in multiple aliased dirs 2", "multiAlias/anotherDir", "/e/anotherDir/index"),
|
||||
// not part of enhanced-resolve, added to make sure query in alias value works
|
||||
("should resolve query in alias value", "alias_query?query_before", "/a/index?query_after"),
|
||||
("should resolve query in alias value", "alias_fragment#fragment_before", "/a/index#fragment_after"),
|
||||
];
|
||||
|
||||
for (comment, request, expected) in pass {
|
||||
|
|
|
|||
Loading…
Reference in a new issue