feat(resolver): implement fallback (#572)

This commit is contained in:
Boshen 2023-07-19 14:42:43 +08:00 committed by GitHub
parent 05b77a5a15
commit 3f07306be0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 15 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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"),

View 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(())
}

View file

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