feat(resolver): implement resolveToContext (#694)

This commit is contained in:
Boshen 2023-08-07 14:30:32 +08:00 committed by GitHub
parent 59f5dc1906
commit 9b2d3fce6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 153 additions and 70 deletions

View file

@ -26,7 +26,7 @@
| ✅ | modules | ["node_modules"] | A list of directories to resolve modules from, can be absolute path or folder name |
| | plugins | [] | A list of additional resolve plugins which should be applied |
| | resolver | undefined | A prepared Resolver to which the plugins are attached |
| | resolveToContext | false | Resolve to a context instead of a file |
| | resolveToContext | false | Resolve to a context instead of a file |
| ✅ | preferRelative | false | Prefer to resolve module requests as relative request and fallback to resolving as module |
| ✅ | preferAbsolute | false | Prefer to resolve server-relative urls as absolute paths before falling back to resolve in roots |
| ✅ | restrictions | [] | A list of resolve restrictions |
@ -61,9 +61,9 @@ Crossed out test files are irrelevant.
- [ ] plugins.test.js
- [ ] pnp.test.js
- [x] ~pr-53.test.js~
- [x] resolve.test.js (need to add resolveToContext)
- [x] resolve.test.js
- [x] restrictions.test.js (partially done, regex is not supported yet)
- [x] roots.test.js (need to add resolveToContext)
- [x] roots.test.js
- [x] scoped-packages.test.js
- [x] simple.test.js
- [x] symlink.test.js

View file

@ -109,7 +109,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
Self { options: options.sanitize(), cache: Cache::default() }
}
pub fn new_with_file_system(options: ResolveOptions, file_system: Fs) -> Self {
pub fn new_with_file_system(file_system: Fs, options: ResolveOptions) -> Self {
Self { cache: Cache::new(file_system), ..Self::new(options) }
}
@ -424,6 +424,12 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
}
fn load_as_directory(&self, cached_path: &CachedPath, ctx: &ResolveContext) -> ResolveState {
if !cached_path.is_dir(&self.cache.fs) {
return Ok(None);
}
if self.options.resolve_to_context {
return Ok(Some(cached_path.clone()));
}
// TODO: Only package.json is supported, so warn about having other values
// Checking for empty files is needed for omitting checks on package.json
// 1. If X/package.json is a file,
@ -481,10 +487,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
}
}
// c. LOAD_AS_DIRECTORY(DIR/X)
if cached_path.is_dir(&self.cache.fs) {
if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
return Ok(Some(path));
}
if let Some(path) = self.load_as_directory(&cached_path, ctx)? {
return Ok(Some(path));
}
node_module_path.pop();
}

View file

@ -74,6 +74,11 @@ pub struct ResolveOptions {
/// Default `["node_modules"]`
pub modules: Vec<String>,
/// Resolve to a context instead of a file.
///
/// Default `false`
pub resolve_to_context: bool,
/// Prefer to resolve module requests as relative requests instead of using modules from node_modules directories.
///
/// Default `false`
@ -136,6 +141,7 @@ impl Default for ResolveOptions {
fully_specified: false,
main_files: vec!["index".into()],
modules: vec!["node_modules".into()],
resolve_to_context: false,
prefer_relative: false,
prefer_absolute: false,
restrictions: vec![],

View file

@ -27,26 +27,35 @@ fn alias() {
("/e/dir/file", ""),
]);
#[rustfmt::skip]
let options = ResolveOptions {
alias: 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())]),
// alias configuration should work
("#".into(), vec![AliasValue::Path("/c/dir".into())]),
("@".into(), vec![AliasValue::Path("/c/dir".into())]),
("ignored".into(), vec![AliasValue::Ignore]),
],
modules: vec!["/".into()],
..ResolveOptions::default()
};
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(options, file_system);
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(
file_system,
ResolveOptions {
alias: 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())]),
// alias configuration should work
("#".into(), vec![AliasValue::Path("/c/dir".into())]),
("@".into(), vec![AliasValue::Path("/c/dir".into())]),
("ignored".into(), vec![AliasValue::Ignore]),
],
modules: vec!["/".into()],
..ResolveOptions::default()
},
);
#[rustfmt::skip]
let pass = [

View file

@ -27,23 +27,32 @@ fn fallback() {
("/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);
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(
file_system,
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()
},
);
#[rustfmt::skip]
let pass = [

View file

@ -6,12 +6,8 @@ use crate::{AliasValue, ResolveOptions, ResolverGeneric};
use super::memory_fs::MemoryFS;
#[test]
#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
fn test() {
use crate::Resolution;
let file_system = MemoryFS::new(&[
fn file_system() -> MemoryFS {
MemoryFS::new(&[
("/a/node_modules/package1/index.js", ""),
("/a/node_modules/package1/file.js", ""),
("/a/node_modules/package2/package.json", r#"{"main":"a"}"#),
@ -24,19 +20,27 @@ fn test() {
("/a/abc.js", ""),
("/a/dir/index.js", ""),
("/a/index.js", ""),
]);
])
}
let options = ResolveOptions {
alias: vec![
("alias1".into(), vec![AliasValue::Path("/a/abc".into())]),
("alias2".into(), vec![AliasValue::Path("/a".into())]),
],
alias_fields: vec!["browser".into()],
fully_specified: true,
..ResolveOptions::default()
};
#[test]
#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
fn test() {
use crate::Resolution;
let file_system = file_system();
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(options, file_system);
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(
file_system,
ResolveOptions {
alias: vec![
("alias1".into(), vec![AliasValue::Path("/a/abc".into())]),
("alias2".into(), vec![AliasValue::Path("/a".into())]),
],
alias_fields: vec!["browser".into()],
fully_specified: true,
..ResolveOptions::default()
},
);
let failing_resolves = [
("no extensions", "./abc"),
@ -70,3 +74,41 @@ fn test() {
assert_eq!(resolution, Ok(PathBuf::from(expected)), "{comment} {request}");
}
}
#[test]
#[cfg(not(target_os = "windows"))] // MemoryFS's path separator is always `/` so the test will not pass in windows.
fn resolve_to_context() {
use crate::Resolution;
let file_system = file_system();
let resolver = ResolverGeneric::<MemoryFS>::new_with_file_system(
file_system,
ResolveOptions {
alias: vec![
("alias1".into(), vec![AliasValue::Path("/a/abc".into())]),
("alias2".into(), vec![AliasValue::Path("/a".into())]),
],
alias_fields: vec!["browser".into()],
fully_specified: true,
resolve_to_context: true,
..ResolveOptions::default()
},
);
let successful_resolves = [
("current folder", ".", "/a"),
("current folder 2", "./", "/a"),
("relative directory", "./dir", "/a/dir"),
("relative directory 2", "./dir/", "/a/dir"),
("relative directory with query and fragment", "./dir?123#456", "/a/dir?123#456"),
("relative directory with query and fragment 2", "./dir/?123#456", "/a/dir?123#456"),
("absolute directory", "/a/dir", "/a/dir"),
("directory in package", "package3/dir", "/a/node_modules/package3/dir"),
];
for (comment, request, expected) in successful_resolves {
let resolution = resolver.resolve("/a", request).map(Resolution::full_path);
assert_eq!(resolution, Ok(PathBuf::from(expected)), "{comment} {request}");
}
}

View file

@ -71,17 +71,17 @@ fn prefer_relative() {
}
#[test]
#[ignore = "add resolveToContext option"]
fn resolve_context() {
let f = super::fixture();
let resolver = Resolver::default();
let resolver =
Resolver::new(ResolveOptions { resolve_to_context: true, ..ResolveOptions::default() });
#[rustfmt::skip]
let data = [
("context for fixtures", f.clone(), "./", f.clone()),
("context for fixtures/lib", f.clone(), "./lib", f.join("lib")),
("context for fixtures with ..", f.clone(), "./lib/../../fixtures/./lib/..", f.clone()),
("context for fixtures with query", f.clone(), "./?query", f.clone().with_file_name("?query")),
("context for fixtures with query", f.clone(), "./?query", f.clone().with_file_name("fixtures?query")),
];
for (comment, path, request, expected) in data {

View file

@ -1,9 +1,13 @@
//! <https://github.com/webpack/enhanced-resolve/blob/main/test/roots.test.js>
use std::env;
use std::{env, path::PathBuf};
use crate::{AliasValue, Resolution, ResolveError, ResolveOptions, Resolver};
fn dirname() -> PathBuf {
env::current_dir().unwrap().join("tests/enhanced_resolve/test")
}
#[test]
fn roots() {
let f = super::fixture();
@ -11,7 +15,7 @@ fn roots() {
let resolver = Resolver::new(ResolveOptions {
extensions: vec![".js".into()],
alias: vec![("foo".into(), vec![AliasValue::Path("/fixtures".into())])],
roots: vec![env::current_dir().unwrap().join("tests/enhanced_resolve/test"), f.clone()],
roots: vec![dirname(), f.clone()],
..ResolveOptions::default()
});
@ -42,8 +46,17 @@ fn roots() {
}
#[test]
#[ignore = "resolve_to_context"]
fn resolve_to_context() {}
fn resolve_to_context() {
let f = super::fixture();
let resolver = Resolver::new(ResolveOptions {
roots: vec![dirname(), f.clone()],
resolve_to_context: true,
..ResolveOptions::default()
});
let resolved_path = resolver.resolve(&f, "/fixtures/lib").map(Resolution::full_path);
let expected = f.join("lib");
assert_eq!(resolved_path, Ok(expected));
}
#[test]
fn prefer_absolute() {
@ -52,7 +65,7 @@ fn prefer_absolute() {
let resolver = Resolver::new(ResolveOptions {
extensions: vec![".js".into()],
alias: vec![("foo".into(), vec![AliasValue::Path("/fixtures".into())])],
roots: vec![env::current_dir().unwrap().join("tests/enhanced_resolve/test"), f.clone()],
roots: vec![dirname(), f.clone()],
prefer_absolute: true,
..ResolveOptions::default()
});