Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ document-features = { version = "0.2.8", optional = true }

[dev-dependencies]
vfs = "0.12.0" # for testing with in memory file system
regex = "1.11.1"
rayon = { version = "1.10.0" }
criterion2 = { version = "2.0.0", default-features = false, package = "codspeed-criterion-compat" }
normalize-path = { version = "0.2.1" }
Expand Down
4 changes: 0 additions & 4 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ pub enum ResolveError {
#[error("{0:?}")]
JSON(JSONError),

/// Restricted by `ResolveOptions::restrictions`
#[error(r#"Path "{0}" restricted by {0}"#)]
Restriction(PathBuf, PathBuf),

#[error(r#"Invalid module "{0}" specifier is not a valid subpath for the "exports" resolution of {1}"#)]
InvalidModuleSpecifier(String, PathBuf),

Expand Down
38 changes: 23 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
let cached_path = self.cache.value(path);
let cached_path = self.require(&cached_path, specifier, ctx)?;
let path = self.load_realpath(&cached_path)?;
// enhanced-resolve: restrictions
self.check_restrictions(&path)?;

let package_json = cached_path.find_package_json(&self.cache.fs, &self.options, ctx)?;
if let Some(package_json) = &package_json {
// path must be inside the package.
Expand Down Expand Up @@ -623,7 +622,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
}
}

fn check_restrictions(&self, path: &Path) -> Result<(), ResolveError> {
fn check_restrictions(&self, path: &Path) -> bool {
// 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) {
Expand All @@ -638,18 +637,17 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
match restriction {
Restriction::Path(restricted_path) => {
if !is_inside(path, restricted_path) {
return Err(ResolveError::Restriction(
path.to_path_buf(),
restricted_path.clone(),
));
return false;
}
}
Restriction::RegExp(_) => {
return Err(ResolveError::Unimplemented("Restriction with regex"))
Restriction::Fn(f) => {
if !f(path) {
return false;
}
}
}
}
Ok(())
true
}

fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
Expand All @@ -658,7 +656,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
let cached_path = self.cache.value(&main_path);
if self.options.enforce_extension.is_disabled() {
if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
return Ok(Some(path));
if self.check_restrictions(path.path()) {
return Ok(Some(path));
}
}
}
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
Expand Down Expand Up @@ -690,7 +690,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
{
return Ok(Some(path));
}
if cached_path.is_file(&self.cache.fs, ctx) {
if cached_path.is_file(&self.cache.fs, ctx) && self.check_restrictions(cached_path.path()) {
return Ok(Some(cached_path.clone()));
}
Ok(None)
Expand Down Expand Up @@ -988,7 +988,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// Complete when resolving to self `{"./a.js": "./a.js"}`
if new_specifier.strip_prefix("./").filter(|s| path.ends_with(Path::new(s))).is_some() {
return if cached_path.is_file(&self.cache.fs, ctx) {
Ok(Some(cached_path.clone()))
if self.check_restrictions(cached_path.path()) {
Ok(Some(cached_path.clone()))
} else {
Ok(None)
}
} else {
Err(ResolveError::NotFound(new_specifier.to_string()))
};
Expand Down Expand Up @@ -1145,6 +1149,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
if !cached_path.is_file(&self.cache.fs, ctx) {
ctx.with_fully_specified(false);
return Ok(None);
} else if !self.check_restrictions(cached_path.path()) {
return Ok(None);
}
// Create a meaningful error message.
let dir = path.parent().unwrap().to_path_buf();
Expand Down Expand Up @@ -1348,8 +1354,10 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
// 1. Return the URL resolution of main in packageURL.
let path = cached_path.path().normalize_with(main_field);
let cached_path = self.cache.value(&path);
if cached_path.is_file(&self.cache.fs, ctx) {
return Ok(Some(cached_path));
if cached_path.is_file(&self.cache.fs, ctx)
&& self.check_restrictions(cached_path.path())
{
return Ok(Some(cached_path.clone()));
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/options.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::path::Path;
use std::sync::Arc;
use std::{fmt, path::PathBuf};

/// Module Resolution Options
Expand Down Expand Up @@ -418,10 +419,19 @@ where
}

/// Value for [ResolveOptions::restrictions]
#[derive(Debug, Clone)]
#[derive(Clone)]
pub enum Restriction {
Path(PathBuf),
RegExp(String),
Fn(Arc<dyn Fn(&Path) -> bool + Sync + Send>),
}

impl std::fmt::Debug for Restriction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Path(path) => write!(f, "Path({path:?})"),
Self::Fn(_) => write!(f, "Fn(<function>)"),
}
}
}

/// Tsconfig Options for [ResolveOptions::tsconfig]
Expand Down
84 changes: 76 additions & 8 deletions src/tests/restrictions.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
//! <https://github.com/webpack/enhanced-resolve/blob/main/test/restrictions.test.js>

use std::sync::Arc;

use regex::Regex;

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]
fn should_respect_regexp_restriction() {
let f = super::fixture().join("restrictions");

let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
let resolver1 = Resolver::new(ResolveOptions {
extensions: vec![".js".into()],
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
}))],
..ResolveOptions::default()
});

let resolution = resolver1.resolve(&f, "pck1").map(|r| r.full_path());
assert_eq!(resolution, Err(ResolveError::NotFound("pck1".to_string())));
}

#[test]
fn should_try_to_find_alternative_1() {
let f = super::fixture().join("restrictions");

let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
let resolver1 = Resolver::new(ResolveOptions {
extensions: vec![".js".into(), ".css".into()],
main_files: vec!["index".into()],
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
}))],
..ResolveOptions::default()
});

let resolution = resolver1.resolve(&f, "pck1").map(|r| r.full_path());
assert_eq!(resolution, Ok(f.join("node_modules/pck1/index.css")));
}

// should respect string restriction
#[test]
fn restriction1() {
fn should_respect_string_restriction() {
let fixture = super::fixture();
let f = fixture.join("restrictions");

Expand All @@ -21,5 +53,41 @@ fn restriction1() {
});

let resolution = resolver.resolve(&f, "pck2");
assert_eq!(resolution, Err(ResolveError::Restriction(fixture.join("c.js"), f)));
assert_eq!(resolution, Err(ResolveError::NotFound("pck2".to_string())));
}

#[test]
fn should_try_to_find_alternative_2() {
let f = super::fixture().join("restrictions");

let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
let resolver1 = Resolver::new(ResolveOptions {
extensions: vec![".js".into(), ".css".into()],
main_fields: vec!["main".into(), "style".into()],
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
}))],
..ResolveOptions::default()
});

let resolution = resolver1.resolve(&f, "pck2").map(|r| r.full_path());
assert_eq!(resolution, Ok(f.join("node_modules/pck2/index.css")));
}

#[test]
fn should_try_to_find_alternative_3() {
let f = super::fixture().join("restrictions");

let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
let resolver1 = Resolver::new(ResolveOptions {
extensions: vec![".js".into()],
main_fields: vec!["main".into(), "module".into(), "style".into()],
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
}))],
..ResolveOptions::default()
});

let resolution = resolver1.resolve(&f, "pck2").map(|r| r.full_path());
assert_eq!(resolution, Ok(f.join("node_modules/pck2/index.css")));
}
Loading