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:
Boshen 2023-08-14 15:04:09 +08:00 committed by GitHub
parent 7c3e29d421
commit 4fa6aafa3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 72 additions and 73 deletions

View file

@ -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();
}

View file

@ -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("#"));
}

View file

@ -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 {