diff --git a/crates/oxc_resolver/README.md b/crates/oxc_resolver/README.md index 272c7d98e..fe32ba71f 100644 --- a/crates/oxc_resolver/README.md +++ b/crates/oxc_resolver/README.md @@ -29,7 +29,7 @@ | | 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 | +| ✅ | restrictions | [] | A list of resolve restrictions | | ✅ | 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 @@ -62,7 +62,7 @@ Crossed out test files are irrelevant. - [ ] pnp.test.js - [x] ~pr-53.test.js~ - [x] resolve.test.js (need to add resolveToContext) -- [ ] restrictions.test.js +- [x] restrictions.test.js (partially done, regex is not supported yet) - [x] roots.test.js (need to add resolveToContext) - [x] scoped-packages.test.js - [x] simple.test.js diff --git a/crates/oxc_resolver/src/error.rs b/crates/oxc_resolver/src/error.rs index 4366ac0f8..f904c586d 100644 --- a/crates/oxc_resolver/src/error.rs +++ b/crates/oxc_resolver/src/error.rs @@ -28,6 +28,9 @@ pub enum ResolveError { /// JSON parse error JSON(JSONError), + /// Restricted by `ResolveOptions::restrictions` + Restriction(PathBuf), + // TODO: TypeError [ERR_INVALID_MODULE_SPECIFIER]: Invalid module "./dist/../../../a.js" specifier is not a valid subpath for the "exports" resolution of /xxx/package.json InvalidModuleSpecifier(String), @@ -47,6 +50,8 @@ pub enum ResolveError { InvalidPackageConfigDirectory(PathBuf), PackageImportNotDefined(String), + + Unimplemented(&'static str), } #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/crates/oxc_resolver/src/lib.rs b/crates/oxc_resolver/src/lib.rs index cadfca601..bfcfc8c27 100644 --- a/crates/oxc_resolver/src/lib.rs +++ b/crates/oxc_resolver/src/lib.rs @@ -46,7 +46,7 @@ use crate::{ pub use crate::{ error::{JSONError, ResolveError}, file_system::{FileMetadata, FileSystem}, - options::{Alias, AliasValue, ResolveOptions}, + options::{Alias, AliasValue, ResolveOptions, Restriction}, resolution::Resolution, }; @@ -138,6 +138,8 @@ impl ResolverGeneric { .and_then(|value| value.ok_or(err)) })?; let path = self.load_realpath(&cached_path).unwrap_or_else(|| cached_path.to_path_buf()); + // enhanced_resolve: restrictions + self.check_restrictions(&path)?; let ctx = ctx.borrow(); Ok(Resolution { path, @@ -361,6 +363,34 @@ impl ResolverGeneric { } } + fn check_restrictions(&self, path: &Path) -> Result<(), ResolveError> { + // https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/RestrictionsPlugin.js#L19-L24 + fn is_inside(path: &Path, parent: &Path) -> bool { + if !path.starts_with(parent) { + return false; + } + if path.as_os_str().len() == parent.as_os_str().len() { + return true; + } + path.strip_prefix(parent).is_ok_and(|p| p == Path::new("./")) + } + for restriction in &self.options.restrictions { + match restriction { + Restriction::Path(restricted_path) => { + if !is_inside(path, restricted_path) { + return Err(ResolveError::Restriction(path.to_path_buf())); + } + } + Restriction::RegExp(_) => { + return Err(ResolveError::Unimplemented( + "Restriction with regex is unimplemented.", + )) + } + } + } + Ok(()) + } + fn load_index(&self, cached_path: &CachedPath, ctx: &ResolveContext) -> ResolveState { for main_file in &self.options.main_files { let main_path = cached_path.path().join(main_file); diff --git a/crates/oxc_resolver/src/options.rs b/crates/oxc_resolver/src/options.rs index 8b8da0063..cd525e2c4 100644 --- a/crates/oxc_resolver/src/options.rs +++ b/crates/oxc_resolver/src/options.rs @@ -84,6 +84,11 @@ pub struct ResolveOptions { /// Default `false` pub prefer_absolute: bool, + /// A list of resolve restrictions to restrict the paths that a request can be resolved on. + /// + /// Default `[]` + pub restrictions: 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. /// @@ -111,6 +116,12 @@ pub enum AliasValue { Ignore, } +#[derive(Debug, Clone)] +pub enum Restriction { + Path(PathBuf), + RegExp(String), +} + impl Default for ResolveOptions { fn default() -> Self { Self { @@ -127,6 +138,7 @@ impl Default for ResolveOptions { modules: vec!["node_modules".into()], prefer_relative: false, prefer_absolute: false, + restrictions: vec![], roots: vec![], symlinks: true, } diff --git a/crates/oxc_resolver/src/tests/mod.rs b/crates/oxc_resolver/src/tests/mod.rs index 54e5eb8b5..eb29a58db 100644 --- a/crates/oxc_resolver/src/tests/mod.rs +++ b/crates/oxc_resolver/src/tests/mod.rs @@ -9,6 +9,7 @@ mod imports_field; mod incorrect_description_file; mod memory_fs; mod resolve; +mod restrictions; mod roots; mod scoped_packages; mod simple; diff --git a/crates/oxc_resolver/src/tests/restrictions.rs b/crates/oxc_resolver/src/tests/restrictions.rs new file mode 100644 index 000000000..eeec24b44 --- /dev/null +++ b/crates/oxc_resolver/src/tests/restrictions.rs @@ -0,0 +1,25 @@ +//! + +use crate::{ResolveError, ResolveOptions, Resolver, Restriction}; + +// TODO: regex +// * should respect RegExp restriction +// * should try to find alternative #1 +// * should try to find alternative #2 +// * should try to find alternative #3 + +#[test] +// should respect string restriction +fn restriction1() { + let fixture = super::fixture(); + let f = fixture.join("restrictions"); + + let resolver = Resolver::new(ResolveOptions { + extensions: vec![".js".into()], + restrictions: vec![Restriction::Path(f.clone())], + ..ResolveOptions::default() + }); + + let resolution = resolver.resolve(&f, "pck2"); + assert_eq!(resolution, Err(ResolveError::Restriction(fixture.join("c.js")))); +}