Skip to content

Commit a118755

Browse files
JounQinSyMind
andauthored
feat: support pass closure to restriction (#604)
Co-authored-by: Cong-Cong Pan <[email protected]>
1 parent d0732b4 commit a118755

File tree

8 files changed

+121
-30
lines changed

8 files changed

+121
-30
lines changed

Cargo.lock

Lines changed: 2 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
@@ -97,6 +97,7 @@ document-features = { version = "0.2.11", optional = true }
9797
[dev-dependencies]
9898
criterion2 = { version = "3.0.1", default-features = false }
9999
dirs = { version = "6.0.0" }
100+
fancy-regex = { version = "^0.14.0", default-features = false, features = ["std"] }
100101
normalize-path = { version = "0.2.1" }
101102
pico-args = "0.5.0"
102103
rayon = { version = "1.10.0" }

napi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ doctest = false
2222
[dependencies]
2323
oxc_resolver = { workspace = true }
2424

25+
fancy-regex = { version = "^0.14.0", default-features = false, features = ["std"] }
2526
napi = { version = "3.0.0-beta.12", default-features = false, features = ["napi3", "serde-json"] }
2627
napi-derive = { version = "3.0.0-beta.12" }
2728
tracing-subscriber = { version = "0.3.19", optional = true, default-features = false, features = ["std", "fmt"] } # Omit the `regex` feature

napi/src/options.rs

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

3+
use fancy_regex::Regex;
34
use napi::Either;
45
use napi_derive::napi;
56

@@ -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()).unwrap_or(false)
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
@@ -278,8 +278,6 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
278278
cached_path.to_path_buf()
279279
};
280280

281-
// enhanced-resolve: restrictions
282-
self.check_restrictions(&path)?;
283281
let package_json = self.find_package_json_for_a_package(&cached_path, ctx)?;
284282
if let Some(package_json) = &package_json {
285283
// path must be inside the package.
@@ -753,7 +751,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
753751
}
754752
}
755753

756-
fn check_restrictions(&self, path: &Path) -> Result<(), ResolveError> {
754+
fn check_restrictions(&self, path: &Path) -> bool {
757755
// https://github.com/webpack/enhanced-resolve/blob/a998c7d218b7a9ec2461fc4fddd1ad5dd7687485/lib/RestrictionsPlugin.js#L19-L24
758756
fn is_inside(path: &Path, parent: &Path) -> bool {
759757
if !path.starts_with(parent) {
@@ -768,26 +766,27 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
768766
match restriction {
769767
Restriction::Path(restricted_path) => {
770768
if !is_inside(path, restricted_path) {
771-
return Err(ResolveError::Restriction(
772-
path.to_path_buf(),
773-
restricted_path.clone(),
774-
));
769+
return false;
775770
}
776771
}
777-
Restriction::RegExp(_) => {
778-
return Err(ResolveError::Unimplemented("Restriction with regex"));
772+
Restriction::Fn(f) => {
773+
if !f(path) {
774+
return false;
775+
}
779776
}
780777
}
781778
}
782-
Ok(())
779+
true
783780
}
784781

785782
fn load_index(&self, cached_path: &CachedPath, ctx: &mut Ctx) -> ResolveResult {
786783
for main_file in &self.options.main_files {
787784
let cached_path = cached_path.normalize_with(main_file, self.cache.as_ref());
788785
if self.options.enforce_extension.is_disabled() {
789786
if let Some(path) = self.load_alias_or_file(&cached_path, ctx)? {
790-
return Ok(Some(path));
787+
if self.check_restrictions(path.path()) {
788+
return Ok(Some(path));
789+
}
791790
}
792791
}
793792
// 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
@@ -833,7 +832,7 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
833832
if let Some(path) = self.load_browser_field_or_alias(cached_path, ctx)? {
834833
return Ok(Some(path));
835834
}
836-
if self.cache.is_file(cached_path, ctx) {
835+
if self.cache.is_file(cached_path, ctx) && self.check_restrictions(cached_path.path()) {
837836
return Ok(Some(cached_path.clone()));
838837
}
839838
Ok(None)
@@ -1150,7 +1149,11 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
11501149
// Complete when resolving to self `{"./a.js": "./a.js"}`
11511150
if new_specifier.strip_prefix("./").filter(|s| path.ends_with(Path::new(s))).is_some() {
11521151
return if self.cache.is_file(cached_path, ctx) {
1153-
Ok(Some(cached_path.clone()))
1152+
if self.check_restrictions(cached_path.path()) {
1153+
Ok(Some(cached_path.clone()))
1154+
} else {
1155+
Ok(None)
1156+
}
11541157
} else {
11551158
Err(ResolveError::NotFound(new_specifier.to_string()))
11561159
};
@@ -1323,6 +1326,8 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
13231326
if !self.cache.is_file(cached_path, ctx) {
13241327
ctx.with_fully_specified(false);
13251328
return Ok(None);
1329+
} else if !self.check_restrictions(cached_path.path()) {
1330+
return Ok(None);
13261331
}
13271332
// Create a meaningful error message.
13281333
let dir = path.parent().unwrap().to_path_buf();
@@ -1559,7 +1564,9 @@ impl<Fs: FileSystem> ResolverGeneric<Fs> {
15591564
// 1. Return the URL resolution of main in packageURL.
15601565
let cached_path =
15611566
cached_path.normalize_with(main_field, self.cache.as_ref());
1562-
if self.cache.is_file(&cached_path, ctx) {
1567+
if self.cache.is_file(&cached_path, ctx)
1568+
&& self.check_restrictions(cached_path.path())
1569+
{
15631570
return Ok(Some(cached_path));
15641571
}
15651572
}

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 fancy_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).unwrap_or(false))
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).unwrap_or(false))
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).unwrap_or(false))
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).unwrap_or(false))
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)