diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index 15563d910..fa108adf5 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -19,7 +19,7 @@ | ✅ | enforceExtension | false | Enforce that a extension from extensions must be used | | | exportsFields | ["exports"] | A list of exports fields in description files | | ✅ | extensions | [".js", ".json", ".node"] | A list of extensions which should be tried for files | -| | fallback | [] | Same as `alias`, but only used if default resolving fails | +| ✅ | fallback | [] | Same as `alias`, but only used if default resolving fails | | ✅ | fileSystem | | The file system which should be used | | | fullySpecified | false | Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests) | | | mainFields | ["main"] | A list of main fields in description files | @@ -41,13 +41,13 @@ Tests ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve - [ ] CachedInputFileSystem.test.js - [ ] SyncAsyncFileSystemDecorator.test.js -- [x] alias.test.js (partially done) +- [x] alias.test.js (need to fix a todo) - [x] browserField.test.js (reading the browser field is currently static - not read from the `browserField` option) - [ ] dependencies.test.js - [ ] exportsField.test.js - [x] extension-alias.test.js - [x] extensions.test.js -- [ ] fallback.test.js +- [x] fallback.test.js (need to fix a todo) - ~[ ] forEachBail.test.js~ - [ ] fullSpecified.test.js - [ ] getPaths.test.js diff --git a/crates/oxc_resolver/src/error.rs b/crates/oxc_resolver/src/error.rs index 51f7a524f..81a9f46ef 100644 --- a/crates/oxc_resolver/src/error.rs +++ b/crates/oxc_resolver/src/error.rs @@ -4,9 +4,6 @@ use crate::request::RequestError; #[derive(Debug, Clone, Eq, PartialEq)] pub enum ResolveError { - /// Path not found - NotFound(Box), - /// Ignored path /// /// Derived from ignored path (false value) from browser field in package.json @@ -20,8 +17,8 @@ pub enum ResolveError { /// See Ignored(Box), - /// The provided path request cannot be parsed - Request(RequestError), + /// Path not found + NotFound(Box), /// All of the aliased extension are not found ExtensionAlias, @@ -29,6 +26,9 @@ pub enum ResolveError { /// All of the aliases are not found Alias(String), + /// The provided path request cannot be parsed + Request(RequestError), + /// JSON parse error JSON(JSONError), } @@ -42,6 +42,10 @@ pub struct JSONError { } impl ResolveError { + pub fn is_not_found(&self) -> bool { + matches!(self, Self::NotFound(_) | Self::ExtensionAlias | Self::Alias(_)) + } + pub(crate) fn from_serde_json_error(path: PathBuf, error: &serde_json::Error) -> Self { Self::JSON(JSONError { path, diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index 8266ea6af..e405dfab2 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -25,7 +25,7 @@ pub use crate::{ cache::Cache, error::{JSONError, ResolveError}, file_system::{FileMetadata, FileSystem, FileSystemOs}, - options::{AliasValue, ResolveOptions}, + options::{Alias, AliasValue, ResolveOptions}, resolution::Resolution, }; use crate::{ @@ -71,10 +71,23 @@ impl ResolverGeneric { ) -> Result { let path = path.as_ref(); let request = Request::parse(request_str).map_err(ResolveError::Request)?; - let path = if let Some(path) = self.load_alias(path, request.path.as_str())? { + let path = if let Some(path) = + self.load_alias(path, request.path.as_str(), &self.options.alias)? + { path } else { - self.require(path, &request)? + let result = self.require(path, &request); + if result.as_ref().is_err_and(ResolveError::is_not_found) { + if let Some(path) = + self.load_alias(path, request.path.as_str(), &self.options.fallback)? + { + path + } else { + result? + } + } else { + result? + } }; Ok(Resolution { path, @@ -261,8 +274,8 @@ impl ResolverGeneric { Ok(None) } - fn load_alias(&self, path: &Path, request_str: &str) -> ResolveState { - for (alias, requests) in &self.options.alias { + fn load_alias(&self, path: &Path, request_str: &str, alias: &Alias) -> ResolveState { + for (alias, requests) in alias { let exact_match = alias.strip_prefix(request_str).is_some_and(|c| c == "$"); if request_str.starts_with(alias) || exact_match { for request in requests { diff --git a/crates/oxc_resolver/src/options.rs b/crates/oxc_resolver/src/options.rs index 6ed1f6efd..369f56ab5 100644 --- a/crates/oxc_resolver/src/options.rs +++ b/crates/oxc_resolver/src/options.rs @@ -1,3 +1,5 @@ +pub type Alias = Vec<(String, Vec)>; + #[derive(Debug, Clone)] pub enum AliasValue { /// The path value @@ -10,7 +12,7 @@ pub enum AliasValue { #[derive(Debug, Clone)] pub struct ResolveOptions { /// A list of module alias configurations or an object which maps key to value - pub alias: Vec<(String, Vec)>, + pub alias: Alias, /// A list of alias fields in description files. /// Specify a field, such as `browser`, to be parsed according to [this specification](https://github.com/defunctzombie/package-browser-field-spec). @@ -42,6 +44,11 @@ pub struct ResolveOptions { /// Default `[".js", ".json", ".node"]` pub extensions: Vec, + /// Same as [ResolveOptions::alias], Redirect module requests when normal resolving fails. . + /// + /// Default `[]` + pub fallback: Alias, + /// A list of main files in directories. /// /// Default `["index"]` @@ -62,6 +69,7 @@ impl Default for ResolveOptions { enforce_extension: None, extension_alias: vec![], extensions: vec![".js".into(), ".json".into(), ".node".into()], + fallback: vec![], main_files: vec!["index".into()], modules: vec!["node_modules".into()], } diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/alias.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/alias.rs index 170bb1dca..1794f2ac2 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/alias.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/alias.rs @@ -70,7 +70,7 @@ fn alias() -> Result<(), ResolveError> { ("should resolve a file aliased module 1", "b", "/a/index"), ("should resolve a file aliased module 2", "c", "/a/index"), ("should resolve a file aliased module with a query 1", "b?query", "/a/index?query"), - ("should resolve a file aliased module with a query 1", "c?query", "/a/index?query"), + ("should resolve a file aliased module with a query 2", "c?query", "/a/index?query"), ("should resolve a path in a file aliased module 1", "b/index", "/b/index"), ("should resolve a path in a file aliased module 2", "b/dir", "/b/dir/index"), ("should resolve a path in a file aliased module 3", "b/dir/index", "/b/dir/index"), diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/fallback.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/fallback.rs new file mode 100644 index 000000000..775ab6bfa --- /dev/null +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/fallback.rs @@ -0,0 +1,92 @@ +//! https://github.com/webpack/enhanced-resolve/blob/main/test/fallback.test.js + +use std::path::Path; + +use oxc_resolver::{AliasValue, ResolveError, ResolveOptions, ResolverGeneric}; + +use crate::MemoryFS; + +#[test] +#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows. +fn fallback() -> Result<(), ResolveError> { + let f = Path::new("/"); + + let file_system = MemoryFS::new(&[ + ("/a/index", ""), + ("/a/dir/index", ""), + ("/recursive/index", ""), + ("/recursive/dir/index", ""), + ("/b/index", ""), + ("/b/dir/index", ""), + ("/c/index", ""), + ("/c/dir/index", ""), + ("/d/index.js", ""), + ("/d/dir/.empty", ""), + ("/e/index", ""), + ("/e/anotherDir/index", ""), + ("/e/dir/file", ""), + ]); + + #[rustfmt::skip] + let options = ResolveOptions { + fallback: vec![ + ("aliasA".into(), vec![AliasValue::Path("a".into())]), + ("b$".into(), vec![AliasValue::Path("a/index".into())]), + ("c$".into(), vec![AliasValue::Path("/a/index".into())]), + ("multiAlias".into(), vec![AliasValue::Path("b".into()), AliasValue::Path("c".into()), AliasValue::Path("d".into()), AliasValue::Path("e".into()), AliasValue::Path("a".into())]), + ("recursive".into(), vec![AliasValue::Path("recursive/dir".into())]), + ("/d/dir".into(), vec![AliasValue::Path("/c/dir".into())]), + ("/d/index.js".into(), vec![AliasValue::Path("/c/index".into())]), + ("ignored".into(), vec![AliasValue::Ignore]), + ], + modules: vec!["/".into()], + ..ResolveOptions::default() + }; + + let resolver = ResolverGeneric::::new_with_file_system(options, file_system); + + #[rustfmt::skip] + let pass = [ + ("should resolve a not aliased module 1", "a", "/a/index"), + ("should resolve a not aliased module 2", "a/index", "/a/index"), + ("should resolve a not aliased module 3", "a/dir", "/a/dir/index"), + ("should resolve a not aliased module 4", "a/dir/index", "/a/dir/index"), + ("should resolve an fallback module 1", "aliasA", "/a/index"), + ("should resolve an fallback module 2", "aliasA/index", "/a/index"), + ("should resolve an fallback module 3", "aliasA/dir", "/a/dir/index"), + ("should resolve an fallback module 4", "aliasA/dir/index", "/a/dir/index"), + // TODO recursive + // ("should resolve a recursive aliased module 1", "recursive", "/recursive/dir/index"), + // ("should resolve a recursive aliased module 2", "recursive/index", "/recursive/dir/index"), + // ("should resolve a recursive aliased module 3", "recursive/dir", "/recursive/dir/index"), + // ("should resolve a recursive aliased module 4", "recursive/dir/index", "/recursive/dir/index"), + // ("should resolve a recursive aliased module 5", "recursive/file", "/recursive/dir/file"), + ("should resolve a file aliased module with a query 1", "b?query", "/b/index?query"), + ("should resolve a file aliased module with a query 2", "c?query", "/c/index?query"), + ("should resolve a path in a file aliased module 1", "b/index", "/b/index"), + ("should resolve a path in a file aliased module 2", "b/dir", "/b/dir/index"), + ("should resolve a path in a file aliased module 3", "b/dir/index", "/b/dir/index"), + ("should resolve a path in a file aliased module 4", "c/index", "/c/index"), + ("should resolve a path in a file aliased module 5", "c/dir", "/c/dir/index"), + ("should resolve a path in a file aliased module 6", "c/dir/index", "/c/dir/index"), + ("should resolve a file in multiple aliased dirs 1", "multiAlias/dir/file", "/e/dir/file"), + ("should resolve a file in multiple aliased dirs 2", "multiAlias/anotherDir", "/e/anotherDir/index"), + ]; + + for (comment, request, expected) in pass { + let resolution = resolver.resolve(f, request)?; + assert_eq!(resolution.full_path(), Path::new(expected), "{comment} {request}"); + } + + #[rustfmt::skip] + let ignore = [ + ("should resolve an ignore module", "ignored", ResolveError::Ignored(f.join("ignored").into_boxed_path())) + ]; + + for (comment, request, expected) in ignore { + let resolution = resolver.resolve(f, request); + assert_eq!(resolution, Err(expected), "{comment} {request}"); + } + + Ok(()) +} diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs index 70aa8c438..575257fcc 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs @@ -2,6 +2,7 @@ mod alias; mod browser_field; mod extension_alias; mod extensions; +mod fallback; mod incorrect_description_file; mod resolve; mod scoped_packages;