diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index fe32ba71f..b129984b5 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -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 diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index bfcfc8c27..702a02610 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -109,7 +109,7 @@ impl ResolverGeneric { 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 ResolverGeneric { } 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 ResolverGeneric { } } // 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(); } diff --git a/crates/oxc_resolver/src/options.rs b/crates/oxc_resolver/src/options.rs index cd525e2c4..5aa4d0461 100644 --- a/crates/oxc_resolver/src/options.rs +++ b/crates/oxc_resolver/src/options.rs @@ -74,6 +74,11 @@ pub struct ResolveOptions { /// Default `["node_modules"]` pub modules: Vec, + /// 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![], diff --git a/crates/oxc_resolver/src/tests/alias.rs b/crates/oxc_resolver/src/tests/alias.rs index 50d9797a5..5a2b6ec8a 100644 --- a/crates/oxc_resolver/src/tests/alias.rs +++ b/crates/oxc_resolver/src/tests/alias.rs @@ -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::::new_with_file_system(options, file_system); + let resolver = ResolverGeneric::::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 = [ diff --git a/crates/oxc_resolver/src/tests/fallback.rs b/crates/oxc_resolver/src/tests/fallback.rs index 0312cb819..191319a7c 100644 --- a/crates/oxc_resolver/src/tests/fallback.rs +++ b/crates/oxc_resolver/src/tests/fallback.rs @@ -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::::new_with_file_system(options, file_system); + let resolver = ResolverGeneric::::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 = [ diff --git a/crates/oxc_resolver/src/tests/full_specified.rs b/crates/oxc_resolver/src/tests/full_specified.rs index 49cebde3e..c65f0c292 100644 --- a/crates/oxc_resolver/src/tests/full_specified.rs +++ b/crates/oxc_resolver/src/tests/full_specified.rs @@ -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::::new_with_file_system(options, file_system); + let resolver = ResolverGeneric::::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::::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}"); + } +} diff --git a/crates/oxc_resolver/src/tests/resolve.rs b/crates/oxc_resolver/src/tests/resolve.rs index 066e1258f..ef9fc8316 100644 --- a/crates/oxc_resolver/src/tests/resolve.rs +++ b/crates/oxc_resolver/src/tests/resolve.rs @@ -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 { diff --git a/crates/oxc_resolver/src/tests/roots.rs b/crates/oxc_resolver/src/tests/roots.rs index d925c6f3c..f6925f975 100644 --- a/crates/oxc_resolver/src/tests/roots.rs +++ b/crates/oxc_resolver/src/tests/roots.rs @@ -1,9 +1,13 @@ //! -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() });