Skip to content

Commit 18ac29c

Browse files
authored
feat(plugin/replace): prevent assignment (#2093)
related issue: #2057 This pr implements the same function with rollup's preventAssignment: https://github.com/rollup/plugins/blob/master/packages/replace/src/index.js#L77-L82 And part of the logic is put into the match loop, because of `fancy-regex`'s limitation that variable size of expression cannot be used in lookbehind (fancy-regex/fancy-regex#74 and fancy-regex/fancy-regex#109)
1 parent 3f8d8a2 commit 18ac29c

File tree

13 files changed

+83
-14
lines changed

13 files changed

+83
-14
lines changed

crates/rolldown_binding/src/options/plugin/binding_builtin_plugin.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ impl TryFrom<BindingBuiltinPlugin> for Arc<dyn Pluginable> {
289289
|| ReplaceOptions::default().delimiters,
290290
|raw| (raw[0].clone(), raw[1].clone()),
291291
),
292+
prevent_assignment: opts.prevent_assignment.unwrap_or(false),
292293
}
293294
})))
294295
}
@@ -304,4 +305,5 @@ pub struct BindingReplacePluginConfig {
304305
pub values: HashMap<String, String>,
305306
#[napi(ts_type = "[string, string]")]
306307
pub delimiters: Option<Vec<String>>,
308+
pub prevent_assignment: Option<bool>,
307309
}

crates/rolldown_plugin_replace/src/plugin.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cmp::Reverse, collections::HashMap};
1+
use std::{cmp::Reverse, collections::HashMap, sync::LazyLock};
22

33
use fancy_regex::{Regex, RegexBuilder};
44
use rolldown_plugin::{HookRenderChunkOutput, HookTransformOutput, Plugin};
@@ -10,20 +10,29 @@ pub struct ReplaceOptions {
1010
pub values: HashMap</* Target */ String, /* Replacement */ String>,
1111
/// Default to `("\\b", "\\b(?!\\.)")`. To prevent `typeof window.document` from being replaced by config item `typeof window` => `"object"`.
1212
pub delimiters: (String, String),
13+
pub prevent_assignment: bool,
1314
}
1415

1516
impl Default for ReplaceOptions {
1617
fn default() -> Self {
17-
Self { values: HashMap::default(), delimiters: ("\\b".to_string(), "\\b(?!\\.)".to_string()) }
18+
Self {
19+
values: HashMap::default(),
20+
delimiters: ("\\b".to_string(), "\\b(?!\\.)".to_string()),
21+
prevent_assignment: false,
22+
}
1823
}
1924
}
2025

2126
#[derive(Debug)]
2227
pub struct ReplacePlugin {
2328
matcher: Regex,
29+
prevent_assignment: bool,
2430
values: FxHashMap</* Target */ String, /* Replacement */ String>,
2531
}
2632

33+
static NON_ASSIGNMENT_MATCHER: LazyLock<Regex> =
34+
LazyLock::new(|| Regex::new("\\b(?:const|let|var)\\s+$").expect("Should be valid regex"));
35+
2736
impl ReplacePlugin {
2837
pub fn new(values: HashMap<String, String>) -> Self {
2938
Self::with_options(ReplaceOptions { values, ..Default::default() })
@@ -34,17 +43,20 @@ impl ReplacePlugin {
3443
// Sort by length in descending order so that longer targets are matched first.
3544
keys.sort_by_key(|key| Reverse(key.len()));
3645

46+
let lookahead = if options.prevent_assignment { "(?!\\s*=[^=])" } else { "" };
47+
3748
let joined_keys = keys.iter().map(|key| fancy_regex::escape(key)).collect::<Vec<_>>().join("|");
3849
let (delimiter_left, delimiter_right) = &options.delimiters;
3950
// https://rustexp.lpil.uk/
40-
let pattern = format!("{delimiter_left}({joined_keys}){delimiter_right}");
51+
let pattern = format!("{delimiter_left}({joined_keys}){delimiter_right}{lookahead}");
4152
Self {
4253
matcher: RegexBuilder::new(&pattern)
4354
// Give a `usize::MAX` will cause bundle time tripled in some cases, so we need to use sensible limit
4455
// to have a balance between performance and correctness.
4556
.backtrack_limit(1_000_000)
4657
.build()
4758
.unwrap_or_else(|_| panic!("Invalid regex {pattern:?}")),
59+
prevent_assignment: options.prevent_assignment,
4860
values: options.values.into_iter().collect(),
4961
}
5062
}
@@ -72,6 +84,11 @@ impl ReplacePlugin {
7284
let Some(matched) = captures.get(1) else {
7385
break;
7486
};
87+
if self.prevent_assignment
88+
&& NON_ASSIGNMENT_MATCHER.is_match(&code[0..matched.start()]).unwrap_or(false)
89+
{
90+
continue;
91+
}
7592
let Some(replacement) = self.values.get(matched.as_str()) else {
7693
break;
7794
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: crates/rolldown_testing/src/integration_test.rs
3+
---
4+
# Assets
5+
6+
## input.mjs
7+
8+
```js
9+
10+
//#region input.js
11+
process.env.DEBUG = "test";
12+
if (replaced === "production") {
13+
console.log("");
14+
}
15+
if (world == "world") {
16+
let hello = "world";
17+
console.log(world);
18+
}
19+
20+
//#endregion
21+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
11
process.env.DEBUG = 'test';
2+
3+
if (process.env.DEBUG === 'production') {
4+
console.log('');
5+
}
6+
7+
if (hello == 'world') {
8+
let hello = 'world';
9+
console.log(hello);
10+
}

crates/rolldown_plugin_replace/tests/form/assignment/mod.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ use rolldown_plugin_replace::{ReplaceOptions, ReplacePlugin};
66
use rolldown_testing::{abs_file_dir, integration_test::IntegrationTest, test_config::TestMeta};
77

88
// doesn't replace lvalue in assignment
9-
// #[tokio::test(flavor = "multi_thread")]
10-
#[allow(dead_code)]
9+
#[tokio::test(flavor = "multi_thread")]
1110
async fn assignment() {
1211
let cwd = abs_file_dir!();
1312

@@ -19,8 +18,12 @@ async fn assignment() {
1918
..Default::default()
2019
},
2120
vec![Arc::new(ReplacePlugin::with_options(ReplaceOptions {
22-
values: [("process.env.DEBUG".to_string(), "replaced".to_string())].into(),
23-
// TODO: prevent_assignment: true
21+
values: [
22+
("process.env.DEBUG".to_string(), "replaced".to_string()),
23+
("hello".to_string(), "world".to_string()),
24+
]
25+
.into(),
26+
prevent_assignment: true,
2427
..Default::default()
2528
}))],
2629
)

crates/rolldown_plugin_replace/tests/form/delimiters/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ async fn replace_strings() {
1919
vec![Arc::new(ReplacePlugin::with_options(ReplaceOptions {
2020
values: [("original".to_string(), "replaced".to_string())].into(),
2121
delimiters: ("<%".to_string(), "%>".to_string()),
22+
..Default::default()
2223
}))],
2324
)
2425
.await;

crates/rolldown_plugin_replace/tests/form/process_check/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use rolldown_plugin_replace::{ReplaceOptions, ReplacePlugin};
66
use rolldown_testing::{abs_file_dir, integration_test::IntegrationTest, test_config::TestMeta};
77

88
// Handles process type guards in replacements
9-
// #[tokio::test(flavor = "multi_thread")]
10-
#[allow(dead_code)]
9+
#[tokio::test(flavor = "multi_thread")]
10+
#[ignore = "TODO: implement the feature"]
1111
async fn process_check() {
1212
let cwd = abs_file_dir!();
1313

@@ -20,7 +20,7 @@ async fn process_check() {
2020
},
2121
vec![Arc::new(ReplacePlugin::with_options(ReplaceOptions {
2222
values: [("process.env.NODE_ENV".to_string(), "\"production\"".to_string())].into(),
23-
// TODO: prevent_assignment: true
23+
prevent_assignment: true,
2424
// TODO: object_guards: true
2525
..Default::default()
2626
}))],

crates/rolldown_plugin_replace/tests/form/special_characters/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async fn special_characters() {
2020
vec![Arc::new(ReplacePlugin::with_options(ReplaceOptions {
2121
values: [("require('one')".to_string(), "1".to_string())].into(),
2222
delimiters: (String::new(), String::new()),
23+
..Default::default()
2324
}))],
2425
)
2526
.await;

crates/rolldown_plugin_replace/tests/form/special_delimiters/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async fn special_delimiters() {
2020
vec![Arc::new(ReplacePlugin::with_options(ReplaceOptions {
2121
values: [("special".to_string(), "replaced".to_string())].into(),
2222
delimiters: ("\\b".to_string(), "\\b".to_string()),
23+
..Default::default()
2324
}))],
2425
)
2526
.await;

crates/rolldown_plugin_replace/tests/form/ternary_operator/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async fn ternary_operator() {
2424
("exprIfFalse".to_string(), "third".to_string()),
2525
]
2626
.into(),
27-
// TODO: prevent_assignment: true
27+
prevent_assignment: true,
2828
..Default::default()
2929
}))],
3030
)

0 commit comments

Comments
 (0)