diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index fa108adf5..82d418dd7 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -31,7 +31,7 @@ | | 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 | -| | roots | [] | A list of root paths | +| ✅ | roots | [] | A list of root paths | | | symlinks | true | Whether to resolve symlinks to their symlinked location | | | unsafeCache | false | Use this cache object to unsafely cache the successful requests @@ -59,9 +59,9 @@ Tests ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve - [ ] plugins.test.js - [ ] pnp.test.js - [ ] pr-53.test.js -- [x] resolve.test.js (partially done) +- [x] resolve.test.js (need to add resolveToContext) - [ ] restrictions.test.js -- [ ] roots.test.js +- [x] roots.test.js (need to add resolveToContext, resolverPreferAbsolute) - [x] scoped-packages.test.js - [x] simple.test.js - [ ] symlink.test.js diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index e405dfab2..23528e66d 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -112,25 +112,22 @@ impl ResolverGeneric { RequestPath::Absolute(absolute_path) => { path = Path::new("/"); request_str = absolute_path; + if !self.options.roots.is_empty() { + for root in &self.options.roots { + if let Ok(path) = + self.require_relative(root, absolute_path.trim_start_matches('/')) + { + return Ok(path); + } + } + return Err(ResolveError::NotFound( + Path::new(absolute_path).to_path_buf().into_boxed_path(), + )); + } } // 3. If X begins with './' or '/' or '../' RequestPath::Relative(relative_path) => { - if let Some(path) = self.load_package_self(path, relative_path)? { - return Ok(path); - } - let path = path.normalize_with(relative_path); - // a. LOAD_AS_FILE(Y + X) - if !relative_path.ends_with('/') { - if let Some(path) = self.load_as_file(&path)? { - return Ok(path); - } - } - // b. LOAD_AS_DIRECTORY(Y + X) - if let Some(path) = self.load_as_directory(&path)? { - return Ok(path); - } - // c. THROW "not found" - return Err(ResolveError::NotFound(path.into_boxed_path())); + return self.require_relative(path, relative_path); } // 4. If X begins with '#' RequestPath::Hash(hash_path) => { @@ -156,6 +153,26 @@ impl ResolverGeneric { Err(ResolveError::NotFound(path.to_path_buf().into_boxed_path())) } + // 3. If X begins with './' or '/' or '../' + fn require_relative(&self, path: &Path, request_str: &str) -> Result { + if let Some(path) = self.load_package_self(path, request_str)? { + return Ok(path); + } + let path = path.normalize_with(request_str); + // a. LOAD_AS_FILE(Y + X) + if !request_str.ends_with('/') { + if let Some(path) = self.load_as_file(&path)? { + return Ok(path); + } + } + // b. LOAD_AS_DIRECTORY(Y + X) + if let Some(path) = self.load_as_directory(&path)? { + return Ok(path); + } + // c. THROW "not found" + Err(ResolveError::NotFound(path.into_boxed_path())) + } + #[allow(clippy::unnecessary_wraps)] fn load_as_file(&self, path: &Path) -> ResolveState { // enhanced-resolve feature: extension_alias diff --git a/crates/oxc_resolver/src/options.rs b/crates/oxc_resolver/src/options.rs index 369f56ab5..b32795748 100644 --- a/crates/oxc_resolver/src/options.rs +++ b/crates/oxc_resolver/src/options.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + pub type Alias = Vec<(String, Vec)>; #[derive(Debug, Clone)] @@ -58,6 +60,12 @@ pub struct ResolveOptions { /// /// Default `["node_modules"]` pub modules: Vec, + + /// A list of directories where requests of server-relative URLs (starting with '/') are resolved. + /// On non-Windows systems these requests are resolved as an absolute path first. + /// + /// Default `[]` + pub roots: Vec, } impl Default for ResolveOptions { @@ -72,6 +80,7 @@ impl Default for ResolveOptions { fallback: vec![], main_files: vec!["index".into()], modules: vec!["node_modules".into()], + roots: vec![], } } } diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs index 575257fcc..baee4b16d 100644 --- a/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/mod.rs @@ -5,6 +5,7 @@ mod extensions; mod fallback; mod incorrect_description_file; mod resolve; +mod roots; mod scoped_packages; mod simple; diff --git a/crates/oxc_resolver/tests/enhanced_resolve/test/roots.rs b/crates/oxc_resolver/tests/enhanced_resolve/test/roots.rs new file mode 100644 index 000000000..ab1e9b4f5 --- /dev/null +++ b/crates/oxc_resolver/tests/enhanced_resolve/test/roots.rs @@ -0,0 +1,49 @@ +//! + +use std::env; + +use oxc_resolver::{AliasValue, Resolution, ResolveError, ResolveOptions, Resolver}; + +#[test] +fn roots() { + let f = super::fixture(); + + 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()], + ..ResolveOptions::default() + }); + + #[rustfmt::skip] + let pass = [ + ("should respect roots option", "/fixtures/b.js", f.join("b.js")), + ("should try another root option, if it exists", "/b.js", f.join("b.js")), + ("should respect extension", "/fixtures/b", f.join("b.js")), + ("should resolve in directory", "/fixtures/extensions/dir", f.join("extensions/dir/index.js")), + ("should respect aliases", "foo/b", f.join("b.js")), + ]; + + for (comment, request, expected) in pass { + let resolved_path = resolver.resolve(&f, request).map(Resolution::full_path); + assert_eq!(resolved_path, Ok(expected), "{comment} {request}"); + } + + #[rustfmt::skip] + let fail = [ + ("should not work with relative path", "fixtures/b.js", ResolveError::NotFound(f.clone().into_boxed_path())) + ]; + + for (comment, request, expected) in fail { + let resolution = resolver.resolve(&f, request); + assert_eq!(resolution, Err(expected), "{comment} {request}"); + } +} + +#[test] +#[ignore = "resolve_to_context"] +fn resolve_to_context() {} + +#[test] +#[ignore = "resolver_prefer_absolute"] +fn resolver_prefer_absolute() {}