fix(napi/minify): accept RegExp for property filters (#25827)

This follows up on #24741 and addresses API feedback received after it was merged.

### Feature

`mangleProps.include` and `mangleProps.exclude` now accept JavaScript `RegExp` values instead of strings.

The binding copies the regular expression source and flags before async minification starts, then compiles the pattern once with Rust's regex engine. Native and WASI bindings expose the same API. The core Rust minifier API is unchanged.

### Example

```js
mangleProps: {
  include: /^_/,
  exclude: /^__public/,
}
```

### Limitation

Matching uses Rust regex syntax. The `i`, `m`, `s`, and `u` flags are supported. Unsupported JavaScript syntax or flags are returned as option errors.

### AI assistance

OpenAI Codex was used for research, implementation, review, and testing. I reviewed and take responsibility for the changes.
This commit is contained in:
Dunqing committed 2026-08-18 05:37:31 +00:00
1 parent bfb0832d24
commit 241c559b83
5 files changed
+130 -51

No files matched your search

+6 -6
View File
@@ -69,17 +69,17 @@ console.log(result.map);
### Property-name mangling
Property mangling is opt-in and independent from identifier mangling. `include` is a
[Rust regex](https://docs.rs/regex/latest/regex/#syntax) source string and is required when
`mangleProps` is present. JavaScript regex flags and look-around are not supported; Rust
character classes such as `\w` are Unicode-aware by default.
Property mangling is opt-in and independent from identifier mangling. `include` is a JavaScript
`RegExp` and is required when `mangleProps` is present. Its source and flags are compiled with
[Rust's regex engine](https://docs.rs/regex/latest/regex/#syntax). Flags `i`, `m`, `s`, and `u` are
supported. Other JavaScript flags and unsupported syntax are reported in `errors`.
```javascript
const result = minifySync("component.js", source, {
mangle: false,
mangleProps: {
include: "^_",
exclude: "^__public",
include: /^_/,
exclude: /^__public/,
reserved: ["_externalApi"],
quoted: false,
cache: previousResult?.mangleCache,
+5 -5
View File
@@ -169,12 +169,12 @@ export interface MangleOptionsKeepNames {
export interface ManglePropertiesOptions {
/**
* Rust-regex source selecting property names to mangle. Matching is unanchored unless the
* expression contains anchors. JavaScript regex flags and look-around are not supported.
* JavaScript `RegExp` selecting property names to mangle. The source and flags are compiled
* with Rust's regex engine. Flags `i`, `m`, `s`, and `u` are supported.
*/
include: string
/** Rust-regex source excluding property names selected by `include`. */
exclude?: string
include: RegExp
/** JavaScript `RegExp` excluding property names selected by `include`. */
exclude?: RegExp
/** Exact names that are neither mangled nor emitted as automatic output names. */
reserved?: Array<string>
/**
+5 -5
View File
@@ -169,12 +169,12 @@ export interface MangleOptionsKeepNames {
export interface ManglePropertiesOptions {
/**
* Rust-regex source selecting property names to mangle. Matching is unanchored unless the
* expression contains anchors. JavaScript regex flags and look-around are not supported.
* JavaScript `RegExp` selecting property names to mangle. The source and flags are compiled
* with Rust's regex engine. Flags `i`, `m`, `s`, and `u` are supported.
*/
include: string
/** Rust-regex source excluding property names selected by `include`. */
exclude?: string
include: RegExp
/** JavaScript `RegExp` excluding property names selected by `include`. */
exclude?: RegExp
/** Exact names that are neither mangled nor emitted as automatic output names. */
reserved?: Array<string>
/**
+76 -16
View File
@@ -1,4 +1,11 @@
use napi::Either;
use napi::{
Either, Env, Error, Status, Unknown,
bindgen_prelude::{
FnArgs, FromNapiValue, Function, JsObjectValue, JsValue, Object, ToNapiValue, TypeName,
ValidateNapiValue,
},
sys,
};
use napi_derive::napi;
use rustc_hash::FxHashMap;
@@ -275,14 +282,75 @@ impl From<&MangleOptionsKeepNames> for oxc_minifier::MangleOptionsKeepNames {
}
}
/// Owned source and flags from a JavaScript `RegExp`.
pub struct JsRegExp {
/// Pattern source without delimiters.
pub source: String,
/// JavaScript regular expression flags.
pub flags: String,
}
impl TypeName for JsRegExp {
fn type_name() -> &'static str {
"RegExp"
}
fn value_type() -> napi::ValueType {
napi::ValueType::Object
}
}
impl ValidateNapiValue for JsRegExp {}
impl FromNapiValue for JsRegExp {
unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> napi::Result<Self> {
let object = Object::from_raw(env, napi_val);
let env = Env::from(env);
let global = env.get_global()?;
let constructor = global.get_named_property::<Function<Unknown, ()>>("RegExp")?;
if !object.instanceof(constructor)? {
return Err(Error::new(Status::ObjectExpected, "Expected a RegExp object"));
}
Ok(Self {
source: object.get_named_property::<String>("source")?,
flags: object.get_named_property::<String>("flags")?,
})
}
}
impl ToNapiValue for JsRegExp {
unsafe fn to_napi_value(env: sys::napi_env, value: Self) -> napi::Result<sys::napi_value> {
let global = Env::from(env).get_global()?;
let constructor =
global.get_named_property::<Function<FnArgs<(String, String)>, Unknown>>("RegExp")?;
let object = constructor.new_instance(FnArgs::from((value.source, value.flags)))?;
Ok(object.raw())
}
}
impl JsRegExp {
fn compile(&self, option_name: &str) -> Result<lazy_regex::Regex, String> {
let error = |error| format!("Invalid mangleProps.{option_name} regex: {error}");
if self.flags.is_empty() {
lazy_regex::Regex::new(&self.source).map_err(error)
} else {
lazy_regex::Regex::new(&format!("(?{}){}", self.flags, self.source)).map_err(error)
}
}
}
#[napi(object)]
pub struct ManglePropertiesOptions {
/// Rust-regex source selecting property names to mangle. Matching is unanchored unless the
/// expression contains anchors. JavaScript regex flags and look-around are not supported.
pub include: String,
/// JavaScript `RegExp` selecting property names to mangle. The source and flags are compiled
/// with Rust's regex engine. Flags `i`, `m`, `s`, and `u` are supported.
#[napi(ts_type = "RegExp")]
pub include: JsRegExp,
/// Rust-regex source excluding property names selected by `include`.
pub exclude: Option<String>,
/// JavaScript `RegExp` excluding property names selected by `include`.
#[napi(ts_type = "RegExp")]
pub exclude: Option<JsRegExp>,
/// Exact names that are neither mangled nor emitted as automatic output names.
pub reserved: Option<Vec<String>>,
@@ -310,16 +378,8 @@ impl TryFrom<&ManglePropertiesOptions> for oxc_minifier::ManglePropertiesOptions
type Error = String;
fn try_from(options: &ManglePropertiesOptions) -> Result<Self, Self::Error> {
let include = lazy_regex::Regex::new(&options.include)
.map_err(|error| format!("Invalid mangleProps.include regex: {error}"))?;
let exclude = options
.exclude
.as_ref()
.map(|pattern| {
lazy_regex::Regex::new(pattern)
.map_err(|error| format!("Invalid mangleProps.exclude regex: {error}"))
})
.transpose()?;
let include = options.include.compile("include")?;
let exclude = options.exclude.as_ref().map(|regex| regex.compile("exclude")).transpose()?;
let mut cache = oxc_minifier::ManglePropertyCache::default();
if let Some(entries) = &options.cache {
for (original, value) in entries {
+38 -19
View File
@@ -145,7 +145,7 @@ describe("property mangling", () => {
const ret = minifySync("test.js", "let local; obj._often; obj._rare; obj._often;", {
compress: false,
mangle: false,
mangleProps: { include: "^_" },
mangleProps: { include: /^_/ },
});
expect(ret.errors).toHaveLength(0);
@@ -159,14 +159,14 @@ describe("property mangling", () => {
minifySync("test.js", source, {
compress: false,
mangle: false,
mangleProps: { include: "^_" },
mangleProps: { include: /^_/ },
}).code,
).toBe("obj.e;obj[`_field`];");
expect(
minifySync("test.js", source, {
compress: false,
mangle: false,
mangleProps: { include: "^_", quoted: true },
mangleProps: { include: /^_/, quoted: true },
}).code,
).toBe("obj.e;obj.e;");
});
@@ -176,7 +176,7 @@ describe("property mangling", () => {
compress: false,
mangle: false,
mangleProps: {
include: "^_",
include: /^_/,
cache: { _first: "A", _second: "A", _keep: false },
},
});
@@ -189,7 +189,7 @@ describe("property mangling", () => {
const first = minifySync("test.js", "obj.foo;", {
compress: false,
mangle: false,
mangleProps: { include: ".", cache: { e: false } },
mangleProps: { include: /./, cache: { e: false } },
});
expect(first.code).toBe("obj.t;");
expect(first.mangleCache).toEqual({ e: false, foo: "t" });
@@ -197,7 +197,7 @@ describe("property mangling", () => {
const second = minifySync("test.js", "obj.e; obj.foo;", {
compress: false,
mangle: false,
mangleProps: { include: ".", cache: first.mangleCache },
mangleProps: { include: /./, cache: first.mangleCache },
});
expect(second.code).toBe("obj.e;obj.t;");
expect(second.mangleCache).toEqual(first.mangleCache);
@@ -207,7 +207,7 @@ describe("property mangling", () => {
const ret = minifySync("test.js", "obj._alpha; obj._beta; obj._gamma;", {
compress: false,
mangle: false,
mangleProps: { include: "^_", debug: true },
mangleProps: { include: /^_/, debug: true },
});
expect(ret.code).toBe("obj._$_alpha$_;obj._$_beta$_;obj._$_gamma$_;");
});
@@ -217,8 +217,8 @@ describe("property mangling", () => {
compress: false,
mangle: false,
mangleProps: {
include: "^_",
exclude: "^_skip(?:One|Two)$",
include: /^_/,
exclude: /^_skip(?:One|Two)$/,
reserved: ["_reserved"],
},
});
@@ -226,11 +226,21 @@ describe("property mangling", () => {
expect(ret.code).toBe("obj._skipOne;obj._reserved;obj.e;");
});
it("preserves the case-insensitive RegExp flag", () => {
const ret = minifySync("test.js", "obj._FIELD; obj._other;", {
compress: false,
mangle: false,
mangleProps: { include: /^_field$/i },
});
expect(ret.errors).toHaveLength(0);
expect(ret.code).toBe("obj.e;obj._other;");
});
it("works through the async API", async () => {
const ret = await minify("test.js", "obj._field; obj._field;", {
compress: false,
mangle: false,
mangleProps: { include: "^_" },
mangleProps: { include: /^_/ },
});
expect(ret.errors).toHaveLength(0);
expect(ret.code).toBe("obj.e;obj.e;");
@@ -241,7 +251,7 @@ describe("property mangling", () => {
const ret = minifySync("test.js", "const invalid; obj._field;", {
compress: false,
mangle: false,
mangleProps: { include: "^_" },
mangleProps: { include: /^_/ },
});
expect(ret.errors).toHaveLength(1);
expect(ret.code).toBe("const invalid;obj._field;");
@@ -255,7 +265,7 @@ describe("property mangling", () => {
{
compress: false,
mangle: false,
mangleProps: { include: "^_" },
mangleProps: { include: /^_/ },
},
);
expect(Object.keys(ret.mangleCache!)).toEqual([
@@ -267,21 +277,30 @@ describe("property mangling", () => {
]);
});
it("requires RegExp filters", () => {
expect(() =>
minifySync("test.js", "obj._field;", {
mangleProps: { include: "^_" as any },
}),
).toThrow(/RegExp/);
});
it.each([
[{ include: "[" }, "Invalid mangleProps.include regex"],
[{ include: "^_", cache: { _field: true as any } }, "expected a string or false"],
[{ include: /^(?!_keep)/ }, "Invalid mangleProps.include regex"],
[{ include: /^_/g }, "Invalid mangleProps.include regex"],
[{ include: /^_/, cache: { _field: true as any } }, "expected a string or false"],
[
{ include: "^_", cache: { ["__proto__"]: false } },
{ include: /^_/, cache: { ["__proto__"]: false } },
"this original name cannot be used as a cache key",
],
[{ include: "^_", cache: { _field: "not-valid" } }, "must be an IdentifierName"],
[{ include: "^_", cache: { _field: "__proto__" } }, "must be an IdentifierName"],
[{ include: /^_/, cache: { _field: "not-valid" } }, "must be an IdentifierName"],
[{ include: /^_/, cache: { _field: "__proto__" } }, "must be an IdentifierName"],
[
{ include: "^_", cache: { _field: "constructor" } },
{ include: /^_/, cache: { _field: "constructor" } },
"other than '__proto__', 'constructor', or 'prototype'",
],
[
{ include: "^_", cache: { _field: "prototype" } },
{ include: /^_/, cache: { _field: "prototype" } },
"other than '__proto__', 'constructor', or 'prototype'",
],
])("rejects invalid options", (mangleProps, message) => {