diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index f4a302834..2dacdd615 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -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 ResolverGeneric { 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 ResolverGeneric { fn require( &self, cached_path: &CachedPath, - specifier: &Specifier, + specifier: &str, ctx: &ResolveContext, ) -> Result { ctx.test_for_infinite_recursion()?; @@ -204,27 +204,26 @@ impl ResolverGeneric { // 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 ResolverGeneric { fn try_fragment_as_path( &self, cached_path: &CachedPath, - specifier: &Specifier, + specifier: &str, ctx: &ResolveContext, ) -> Option { 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 ResolverGeneric { 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 ResolverGeneric { } 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 ResolverGeneric { 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 ResolverGeneric { 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(); } diff --git a/crates/oxc_resolver/src/specifier.rs b/crates/oxc_resolver/src/specifier.rs index 80df5a6a3..fb679507d 100644 --- a/crates/oxc_resolver/src/specifier.rs +++ b/crates/oxc_resolver/src/specifier.rs @@ -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, 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("#")); } diff --git a/crates/oxc_resolver/src/tests/alias.rs b/crates/oxc_resolver/src/tests/alias.rs index 3ababeec3..1c7eca2be 100644 --- a/crates/oxc_resolver/src/tests/alias.rs +++ b/crates/oxc_resolver/src/tests/alias.rs @@ -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 {