Skip to content

Commit ad9309d

Browse files
SyMindJounQin
authored andcommitted
feat: support pass closure to restriction
back port web-infra-dev/rspack-resolver#60
1 parent 5f782a9 commit ad9309d

File tree

8 files changed

+133
-30
lines changed

8 files changed

+133
-30
lines changed

Cargo.lock

Lines changed: 14 additions & 0 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
@@ -100,6 +100,7 @@ dirs = { version = "6.0.0" }
100100
normalize-path = { version = "0.2.1" }
101101
pico-args = "0.5.0"
102102
rayon = { version = "1.10.0" }
103+
regex = "1.11.1"
103104
vfs = "0.12.1" # for testing with in memory file system
104105

105106
[features]

napi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ oxc_resolver = { workspace = true }
2525
napi = { version = "3.0.0-beta", default-features = false, features = ["napi3", "serde-json"] }
2626
napi-derive = { version = "3.0.0-beta" }
2727
tracing-subscriber = { version = "0.3.19", optional = true, default-features = false, features = ["std", "fmt"] } # Omit the `regex` feature
28+
regex = "1.11.1"
2829

2930
[target.'cfg(not(any(target_os = "linux", target_os = "freebsd", target_arch = "arm", target_family = "wasm")))'.dependencies]
3031
mimalloc-safe = { version = "0.1.53", optional = true, features = ["skip_collect_on_exit"] }

napi/src/options.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use std::{collections::HashMap, path::PathBuf};
1+
use std::{collections::HashMap, path::PathBuf, sync::Arc};
22

33
use napi::Either;
44
use napi_derive::napi;
5+
use regex::Regex;
56

67
/// Module Resolution Options
78
///
@@ -219,7 +220,12 @@ impl From<Restriction> for oxc_resolver::Restriction {
219220
(None, None) => {
220221
panic!("Should specify path or regex")
221222
}
222-
(None, Some(regex)) => oxc_resolver::Restriction::RegExp(regex),
223+
(None, Some(regex)) => {
224+
let re = Regex::new(&regex).unwrap();
225+
oxc_resolver::Restriction::Fn(Arc::new(move |path| {
226+
re.is_match(path.to_str().unwrap_or_default())
227+
}))
228+
}
223229
(Some(path), None) => oxc_resolver::Restriction::Path(PathBuf::from(path)),
224230
(Some(_), Some(_)) => {
225231
panic!("Restriction can't be path and regex at the same time")

src/error.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,6 @@ pub enum ResolveError {
8282
#[error("{0:?}")]
8383
Json(JSONError),
8484

85-
/// Restricted by `ResolveOptions::restrictions`
86-
#[error(r#"Path "{0}" restricted by {0}"#)]
87-
Restriction(PathBuf, PathBuf),
88-
8985
#[error(r#"Invalid module "{0}" specifier is not a valid subpath for the "exports" resolution of {1}"#)]
9086
InvalidModuleSpecifier(String, PathBuf),
9187

src/lib.rs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,6 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
276276
cached_path.to_path_buf()
277277
};
278278

279-
// enhanced-resolve: restrictions
280-
self.check_restrictions(&path)?;
281279
let package_json = self.find_package_json_for_a_package(&cached_path, ctx)?;
282280
if let Some(package_json) = &package_json {
283281
// path must be inside the package.
@@ -751,7 +749,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
751749
}
752750
}
753751

754-
fn check_restrictions(&self, path: &Path) -> Result<(), ResolveError> {
752+
fn check_restrictions(&self, path: &Path) -> bool {
755753
// https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/RestrictionsPlugin.js#L19-L24
756754
fn is_inside(path: &Path, parent: &Path) -> bool {
757755
if !path.starts_with(parent) {
@@ -766,26 +764,27 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
766764
match restriction {
767765
Restriction::Path(restricted_path) => {
768766
if !is_inside(path, restricted_path) {
769-
return Err(ResolveError::Restriction(
770-
path.to_path_buf(),
771-
restricted_path.clone(),
772-
));
767+
return false;
773768
}
774769
}
775-
Restriction::RegExp(_) => {
776-
return Err(ResolveError::Unimplemented("Restriction with regex"));
770+
Restriction::Fn(f) => {
771+
if !f(path) {
772+
return false;
773+
}
777774
}
778775
}
779776
}
780-
Ok(())
777+
true
781778
}
782779

783780
fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
784781
for main_file in &self.options.main_files {
785782
let cached_path = cached_path.normalize_with(main_file, self.cache.as_ref());
786783
if self.options.enforce_extension.is_disabled() {
787784
if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
788-
return Ok(Some(path));
785+
if self.check_restrictions(path.path()) {
786+
return Ok(Some(path));
787+
}
789788
}
790789
}
791790
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
@@ -831,7 +830,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
831830
if let Some(path) = self.load_browser_field_or_alias(cached_path, ctx)? {
832831
return Ok(Some(path));
833832
}
834-
if self.cache.is_file(cached_path, ctx) {
833+
if self.cache.is_file(cached_path, ctx) && self.check_restrictions(cached_path.path()) {
835834
return Ok(Some(cached_path.clone()));
836835
}
837836
Ok(None)
@@ -1148,7 +1147,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
11481147
// Complete when resolving to self `{"./a.js": "./a.js"}`
11491148
if new_specifier.strip_prefix("./").filter(|s| path.ends_with(Path::new(s))).is_some() {
11501149
return if self.cache.is_file(cached_path, ctx) {
1151-
Ok(Some(cached_path.clone()))
1150+
if self.check_restrictions(cached_path.path()) {
1151+
Ok(Some(cached_path.clone()))
1152+
} else {
1153+
Ok(None)
1154+
}
11521155
} else {
11531156
Err(ResolveError::NotFound(new_specifier.to_string()))
11541157
};
@@ -1321,6 +1324,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
13211324
if !self.cache.is_file(cached_path, ctx) {
13221325
ctx.with_fully_specified(false);
13231326
return Ok(None);
1327+
} else if !self.check_restrictions(cached_path.path()) {
1328+
return Ok(None);
13241329
}
13251330
// Create a meaningful error message.
13261331
let dir = path.parent().unwrap().to_path_buf();
@@ -1557,7 +1562,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
15571562
// 1. Return the URL resolution of main in packageURL.
15581563
let cached_path =
15591564
cached_path.normalize_with(main_field, self.cache.as_ref());
1560-
if self.cache.is_file(&cached_path, ctx) {
1565+
if self.cache.is_file(&cached_path, ctx)
1566+
&& self.check_restrictions(cached_path.path())
1567+
{
15611568
return Ok(Some(cached_path));
15621569
}
15631570
}

src/options.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{
22
fmt,
33
path::{Path, PathBuf},
4+
sync::Arc,
45
};
56

67
/// Module Resolution Options
@@ -456,10 +457,19 @@ where
456457
}
457458

458459
/// Value for [ResolveOptions::restrictions]
459-
#[derive(Debug, Clone)]
460+
#[derive(Clone)]
460461
pub enum Restriction {
461462
Path(PathBuf),
462-
RegExp(String),
463+
Fn(Arc<dyn Fn(&Path) -> bool + Sync + Send>),
464+
}
465+
466+
impl std::fmt::Debug for Restriction {
467+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468+
match self {
469+
Self::Path(path) => write!(f, "Path(\"{}\")", path.display()),
470+
Self::Fn(_) => write!(f, "Fn(<function>)"),
471+
}
472+
}
463473
}
464474

465475
/// 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().is_some_and(|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().is_some_and(|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().is_some_and(|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().is_some_and(|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)