mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(resolver): implement fallback (#572)
This commit is contained in:
parent
05b77a5a15
commit
3f07306be0
7 changed files with 133 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ use crate::request::RequestError;
|
|||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ResolveError {
|
||||
/// Path not found
|
||||
NotFound(Box<Path>),
|
||||
|
||||
/// Ignored path
|
||||
///
|
||||
/// Derived from ignored path (false value) from browser field in package.json
|
||||
|
|
@ -20,8 +17,8 @@ pub enum ResolveError {
|
|||
/// See <https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module>
|
||||
Ignored(Box<Path>),
|
||||
|
||||
/// The provided path request cannot be parsed
|
||||
Request(RequestError),
|
||||
/// Path not found
|
||||
NotFound(Box<Path>),
|
||||
|
||||
/// 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,
|
||||
|
|
|
|||
|
|
@ -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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
) -> Result<Resolution, ResolveError> {
|
||||
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<Fs: FileSystem> ResolverGeneric<Fs> {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
pub type Alias = Vec<(String, Vec<AliasValue>)>;
|
||||
|
||||
#[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<AliasValue>)>,
|
||||
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<String>,
|
||||
|
||||
/// 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()],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
92
crates/oxc_resolver/tests/enhanced_resolve/test/fallback.rs
Normal file
92
crates/oxc_resolver/tests/enhanced_resolve/test/fallback.rs
Normal file
|
|
@ -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::<MemoryFS>::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(())
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue