Skip to content

Commit 64b3262

Browse files
authored
feat: support pass closure to restriction (#60)
1 parent 6cb5b89 commit 64b3262

6 files changed

Lines changed: 119 additions & 35 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ document-features = { version = "0.2.8", optional = true }
9696

9797
[dev-dependencies]
9898
vfs = "0.12.0" # for testing with in memory file system
99+
regex = "1.11.1"
99100
rayon = { version = "1.10.0" }
100101
criterion2 = { version = "2.0.0", default-features = false, package = "codspeed-criterion-compat" }
101102
normalize-path = { version = "0.2.1" }

src/error.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,6 @@ pub enum ResolveError {
6565
#[error("{0:?}")]
6666
JSON(JSONError),
6767

68-
/// Restricted by `ResolveOptions::restrictions`
69-
#[error(r#"Path "{0}" restricted by {0}"#)]
70-
Restriction(PathBuf, PathBuf),
71-
7268
#[error(r#"Invalid module "{0}" specifier is not a valid subpath for the "exports" resolution of {1}"#)]
7369
InvalidModuleSpecifier(String, PathBuf),
7470

src/lib.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
246246
let cached_path = self.cache.value(path);
247247
let cached_path = self.require(&cached_path, specifier, ctx)?;
248248
let path = self.load_realpath(&cached_path)?;
249-
// enhanced-resolve: restrictions
250-
self.check_restrictions(&path)?;
249+
251250
let package_json = cached_path.find_package_json(&self.cache.fs, &self.options, ctx)?;
252251
if let Some(package_json) = &package_json {
253252
// path must be inside the package.
@@ -623,7 +622,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
623622
}
624623
}
625624

626-
fn check_restrictions(&self, path: &Path) -> Result<(), ResolveError> {
625+
fn check_restrictions(&self, path: &Path) -> bool {
627626
// https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/RestrictionsPlugin.js#L19-L24
628627
fn is_inside(path: &Path, parent: &Path) -> bool {
629628
if !path.starts_with(parent) {
@@ -638,18 +637,17 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
638637
match restriction {
639638
Restriction::Path(restricted_path) => {
640639
if !is_inside(path, restricted_path) {
641-
return Err(ResolveError::Restriction(
642-
path.to_path_buf(),
643-
restricted_path.clone(),
644-
));
640+
return false;
645641
}
646642
}
647-
Restriction::RegExp(_) => {
648-
return Err(ResolveError::Unimplemented("Restriction with regex"))
643+
Restriction::Fn(f) => {
644+
if !f(path) {
645+
return false;
646+
}
649647
}
650648
}
651649
}
652-
Ok(())
650+
true
653651
}
654652

655653
fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
@@ -658,7 +656,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
658656
let cached_path = self.cache.value(&main_path);
659657
if self.options.enforce_extension.is_disabled() {
660658
if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
661-
return Ok(Some(path));
659+
if self.check_restrictions(path.path()) {
660+
return Ok(Some(path));
661+
}
662662
}
663663
}
664664
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
@@ -690,7 +690,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
690690
{
691691
return Ok(Some(path));
692692
}
693-
if cached_path.is_file(&self.cache.fs, ctx) {
693+
if cached_path.is_file(&self.cache.fs, ctx) && self.check_restrictions(cached_path.path()) {
694694
return Ok(Some(cached_path.clone()));
695695
}
696696
Ok(None)
@@ -988,7 +988,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
988988
// Complete when resolving to self `{"./a.js": "./a.js"}`
989989
if new_specifier.strip_prefix("./").filter(|s| path.ends_with(Path::new(s))).is_some() {
990990
return if cached_path.is_file(&self.cache.fs, ctx) {
991-
Ok(Some(cached_path.clone()))
991+
if self.check_restrictions(cached_path.path()) {
992+
Ok(Some(cached_path.clone()))
993+
} else {
994+
Ok(None)
995+
}
992996
} else {
993997
Err(ResolveError::NotFound(new_specifier.to_string()))
994998
};
@@ -1145,6 +1149,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
11451149
if !cached_path.is_file(&self.cache.fs, ctx) {
11461150
ctx.with_fully_specified(false);
11471151
return Ok(None);
1152+
} else if !self.check_restrictions(cached_path.path()) {
1153+
return Ok(None);
11481154
}
11491155
// Create a meaningful error message.
11501156
let dir = path.parent().unwrap().to_path_buf();
@@ -1348,8 +1354,10 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
13481354
// 1. Return the URL resolution of main in packageURL.
13491355
let path = cached_path.path().normalize_with(main_field);
13501356
let cached_path = self.cache.value(&path);
1351-
if cached_path.is_file(&self.cache.fs, ctx) {
1352-
return Ok(Some(cached_path));
1357+
if cached_path.is_file(&self.cache.fs, ctx)
1358+
&& self.check_restrictions(cached_path.path())
1359+
{
1360+
return Ok(Some(cached_path.clone()));
13531361
}
13541362
}
13551363
}

src/options.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::path::Path;
2+
use std::sync::Arc;
23
use std::{fmt, path::PathBuf};
34

45
/// Module Resolution Options
@@ -418,10 +419,19 @@ where
418419
}
419420

420421
/// Value for [ResolveOptions::restrictions]
421-
#[derive(Debug, Clone)]
422+
#[derive(Clone)]
422423
pub enum Restriction {
423424
Path(PathBuf),
424-
RegExp(String),
425+
Fn(Arc<dyn Fn(&Path) -> bool + Sync + Send>),
426+
}
427+
428+
impl std::fmt::Debug for Restriction {
429+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430+
match self {
431+
Self::Path(path) => write!(f, "Path({path:?})"),
432+
Self::Fn(_) => write!(f, "Fn(<function>)"),
433+
}
434+
}
425435
}
426436

427437
/// Tsconfig Options for [ResolveOptions::tsconfig]

src/tests/restrictions.rs

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,48 @@
11
//! <https://github.com/webpack/enhanced-resolve/blob/main/test/restrictions.test.js>
22
3+
use std::sync::Arc;
4+
5+
use regex::Regex;
6+
37
use crate::{ResolveError, ResolveOptions, Resolver, Restriction};
48

5-
// TODO: regex
6-
// * should respect RegExp restriction
7-
// * should try to find alternative #1
8-
// * should try to find alternative #2
9-
// * should try to find alternative #3
9+
#[test]
10+
fn should_respect_regexp_restriction() {
11+
let f = super::fixture().join("restrictions");
12+
13+
let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
14+
let resolver1 = Resolver::new(ResolveOptions {
15+
extensions: vec![".js".into()],
16+
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
17+
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
18+
}))],
19+
..ResolveOptions::default()
20+
});
21+
22+
let resolution = resolver1.resolve(&f, "pck1").map(|r| r.full_path());
23+
assert_eq!(resolution, Err(ResolveError::NotFound("pck1".to_string())));
24+
}
25+
26+
#[test]
27+
fn should_try_to_find_alternative_1() {
28+
let f = super::fixture().join("restrictions");
29+
30+
let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
31+
let resolver1 = Resolver::new(ResolveOptions {
32+
extensions: vec![".js".into(), ".css".into()],
33+
main_files: vec!["index".into()],
34+
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
35+
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
36+
}))],
37+
..ResolveOptions::default()
38+
});
39+
40+
let resolution = resolver1.resolve(&f, "pck1").map(|r| r.full_path());
41+
assert_eq!(resolution, Ok(f.join("node_modules/pck1/index.css")));
42+
}
1043

11-
// should respect string restriction
1244
#[test]
13-
fn restriction1() {
45+
fn should_respect_string_restriction() {
1446
let fixture = super::fixture();
1547
let f = fixture.join("restrictions");
1648

@@ -21,5 +53,41 @@ fn restriction1() {
2153
});
2254

2355
let resolution = resolver.resolve(&f, "pck2");
24-
assert_eq!(resolution, Err(ResolveError::Restriction(fixture.join("c.js"), f)));
56+
assert_eq!(resolution, Err(ResolveError::NotFound("pck2".to_string())));
57+
}
58+
59+
#[test]
60+
fn should_try_to_find_alternative_2() {
61+
let f = super::fixture().join("restrictions");
62+
63+
let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
64+
let resolver1 = Resolver::new(ResolveOptions {
65+
extensions: vec![".js".into(), ".css".into()],
66+
main_fields: vec!["main".into(), "style".into()],
67+
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
68+
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
69+
}))],
70+
..ResolveOptions::default()
71+
});
72+
73+
let resolution = resolver1.resolve(&f, "pck2").map(|r| r.full_path());
74+
assert_eq!(resolution, Ok(f.join("node_modules/pck2/index.css")));
75+
}
76+
77+
#[test]
78+
fn should_try_to_find_alternative_3() {
79+
let f = super::fixture().join("restrictions");
80+
81+
let re = Regex::new(r"\.(sass|scss|css)$").unwrap();
82+
let resolver1 = Resolver::new(ResolveOptions {
83+
extensions: vec![".js".into()],
84+
main_fields: vec!["main".into(), "module".into(), "style".into()],
85+
restrictions: vec![Restriction::Fn(Arc::new(move |path| {
86+
path.as_os_str().to_str().map_or(false, |s| re.is_match(s))
87+
}))],
88+
..ResolveOptions::default()
89+
});
90+
91+
let resolution = resolver1.resolve(&f, "pck2").map(|r| r.full_path());
92+
assert_eq!(resolution, Ok(f.join("node_modules/pck2/index.css")));
2593
}

0 commit comments

Comments
 (0)