mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(mangler): add reserved option for names that must not be mangled (#24041)
Adds `MangleOptions::reserved` — equivalent to terser / uglify-js / swc `mangle.reserved`: names in the set are never used as mangled names, and bindings that already carry them keep them. Default empty; default output is byte-identical to main (minsize snapshot unchanged). ## Motivation Node's cjs-module-lexer detects a CommonJS module's named exports by **lexically** scanning for `exports.<name> =` / `module.exports` token patterns — no scope analysis ([its README documents renamed identifiers as "DETECTS: NO EXPORTS"](https://github.com/nodejs/cjs-module-lexer)). UMD / CommonJS wrappers bind `exports` / `module` as ordinary function parameters, and the mangler renames them, erasing every named export the lexer can see. Surfaced by the monitor-oxc mangler runtime test ([failing run](https://github.com/oxc-project/monitor-oxc/actions/runs/28569108563/job/84702922472), previously masked by #24033 / #24036): `archiver` does `import { queue } from "async"`, and `async`'s dist is a UMD bundle — mangled, the factory parameter `exports` became `e` and node fails with `Named export 'queue' not found`. With `reserved: ["exports", "module"]` the output imports correctly (verified end-to-end in node, including running a queue task). ## Why an opt-in list instead of always keeping these names Renaming them is ecosystem-standard. Verified empirically — minifying the same UMD sample and importing it in node: | minifier | renames `exports` param? | `import { queue }` from its output | escape hatch | | --- | --- | --- | --- | | terser 5.48 | yes | fails | `mangle.reserved` (verified working) | | uglify-js 3.19 | yes | fails | `reserved` (verified working) | | esbuild 0.28 `--minify` | yes | fails | none for identifier mangling | | swc | yes | fails | terser-style `mangle.reserved` ([swc#3424](https://github.com/swc-project/swc/issues/3424)) | An earlier revision of this PR kept `exports` / `module` unconditionally; the Minification Size check showed the real cost: ~1% minified on UMD-heavy files (d3: +3.3 kB from ~590 `exports` references, flipping it to *larger than esbuild*), because the point is keeping the literal 7-char token at every export site. No other minifier pays this by default — and rolldown bundles bind `exports` / `module` in every CJS interop wrapper, where node's lexer never looks. So oxc gets the same escape hatch as terser/swc, with the same name. `MangleOptions` loses `Copy` (it now holds the set); construction sites switch to `..MangleOptions::default()` / `clone()`. Exposed in `oxc-minify` NAPI options (`mangle.reserved: string[]`) and the mangler example (`--reserve-exports`). monitor-oxc will pass `["exports", "module"]` for its mangler runtime test (its corpus imports prebuilt CJS/UMD dists directly). For reference, esbuild solves this for its *own* node-platform CJS bundles differently — a dead-code annotation `0 && (module.exports = { a, b })` that cjs-module-lexer recognizes ([esbuild#960](https://github.com/evanw/esbuild/issues/960), [cjs-module-lexer#44](https://github.com/nodejs/cjs-module-lexer/issues/44)); verified that pattern also works appended to fully-mangled output. That could be a future zero-size-cost alternative at the minifier level.
This commit is contained in:
1 parent
01c8775a24
commit
4eb074e69d
10 files changed
+172
-38
No files matched your search
Generated
+1
@@ -2180,6 +2180,7 @@ dependencies = [
|
||||
"oxc_parser",
|
||||
"oxc_sourcemap",
|
||||
"oxc_span",
|
||||
"oxc_str",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -20,7 +20,7 @@ mod keep_names;
|
||||
|
||||
pub use keep_names::MangleOptionsKeepNames;
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct MangleOptions {
|
||||
/// Pass true to mangle names declared in the top level scope.
|
||||
///
|
||||
@@ -33,6 +33,19 @@ pub struct MangleOptions {
|
||||
/// Keep function / class names
|
||||
pub keep_names: MangleOptionsKeepNames,
|
||||
|
||||
/// Names that bindings must not be renamed to, and that bindings already
|
||||
/// carrying them keep. Equivalent to terser / swc `mangle.reserved`.
|
||||
///
|
||||
/// The main use case is `["exports", "module"]` when minifying prebuilt
|
||||
/// CommonJS / UMD files that Node consumers `import` directly: Node's
|
||||
/// cjs-module-lexer detects a CommonJS module's named exports by lexically
|
||||
/// scanning for `exports.<name> =` / `module.exports` token patterns with no
|
||||
/// scope analysis, so renaming an `exports` / `module` wrapper parameter
|
||||
/// erases every named export it can see.
|
||||
///
|
||||
/// Default: empty.
|
||||
pub reserved: FxHashSet<CompactStr>,
|
||||
|
||||
/// Use more readable mangled names
|
||||
/// (e.g. `slot_0`, `slot_1`, `slot_2`, ...) for debugging.
|
||||
///
|
||||
@@ -41,7 +54,7 @@ pub struct MangleOptions {
|
||||
}
|
||||
|
||||
impl MangleOptions {
|
||||
fn top_level(self, source_type: SourceType) -> bool {
|
||||
fn top_level(&self, source_type: SourceType) -> bool {
|
||||
self.top_level.unwrap_or(source_type.is_module() || source_type.is_commonjs())
|
||||
}
|
||||
}
|
||||
@@ -376,7 +389,7 @@ impl<'t> Mangler<'t> {
|
||||
|
||||
// ── Phase 1: collect constraints — names we must not reuse or shadow. ──
|
||||
let constraints =
|
||||
Constraints::collect(allocator, scoping, ast_nodes, program, self.options);
|
||||
Constraints::collect(allocator, scoping, ast_nodes, program, &self.options);
|
||||
// ── Phase 2: assign slots — give bindings that can share a name the same slot. ──
|
||||
let slots = SlotAssignment::compute(allocator, scoping, ast_nodes, &constraints);
|
||||
// ── Phase 3: rank slots by reference frequency (hottest first). ──
|
||||
@@ -477,6 +490,9 @@ impl<'t> SlotFrequency<'t> {
|
||||
struct Constraints<'a, 's> {
|
||||
/// Whether top-level (module / CommonJS) bindings may be mangled at all.
|
||||
top_level: bool,
|
||||
/// User-reserved names — never used as mangled names, and bindings carrying
|
||||
/// them keep them (see [`MangleOptions::reserved`]).
|
||||
reserved: &'s FxHashSet<CompactStr>,
|
||||
/// Names of top-level exports — kept when `top_level` so importers still resolve.
|
||||
exported_names: ArenaHashSet<'a, Str<'a>>,
|
||||
exported_symbols: Option<BitSet<'a>>,
|
||||
@@ -511,7 +527,7 @@ impl<'a, 's> Constraints<'a, 's> {
|
||||
scoping: &'s Scoping,
|
||||
ast_nodes: &AstNodes,
|
||||
program: &'a Program<'a>,
|
||||
options: MangleOptions,
|
||||
options: &'s MangleOptions,
|
||||
) -> Self {
|
||||
let top_level = options.top_level(program.source_type);
|
||||
let (exported_names, exported_symbols) = if top_level && program.source_type.is_module() {
|
||||
@@ -521,7 +537,25 @@ impl<'a, 's> Constraints<'a, 's> {
|
||||
};
|
||||
let (keep_name_names, keep_name_symbols) =
|
||||
collect_keep_name_symbols(options.keep_names, allocator, scoping, ast_nodes);
|
||||
Self { top_level, exported_names, exported_symbols, keep_name_names, keep_name_symbols }
|
||||
Self {
|
||||
top_level,
|
||||
reserved: &options.reserved,
|
||||
exported_names,
|
||||
exported_symbols,
|
||||
keep_name_names,
|
||||
keep_name_symbols,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a binding with this name must keep it.
|
||||
///
|
||||
/// `inline(always)`: called per symbol in `SlotRanking::tally`'s hot loop — the
|
||||
/// empty-`reserved` fast path must compile down to the plain `is_special_name`
|
||||
/// check plus one predictable branch.
|
||||
#[expect(clippy::inline_always, reason = "hot path")]
|
||||
#[inline(always)]
|
||||
fn is_kept_name(&self, name: &str) -> bool {
|
||||
is_special_name(name) || (!self.reserved.is_empty() && self.reserved.contains(name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +758,7 @@ impl<'a> SlotRanking<'a> {
|
||||
if scoping.scope_flags(symbol_scope_id).contains_direct_eval() {
|
||||
continue;
|
||||
}
|
||||
if is_special_name(scoping.symbol_name(symbol_id)) {
|
||||
if constraints.is_kept_name(scoping.symbol_name(symbol_id)) {
|
||||
continue;
|
||||
}
|
||||
if keep_name_symbols.is_some_and(|keep| keep.has_bit(symbol_id.index())) {
|
||||
@@ -762,7 +796,7 @@ impl<'a, const CAPACITY: usize> NameTable<'a, CAPACITY> {
|
||||
let root_bindings = scoping.get_bindings(scoping.root_scope_id());
|
||||
let is_reserved = |name: &str| {
|
||||
oxc_syntax::keyword::is_reserved_keyword(name)
|
||||
|| is_special_name(name)
|
||||
|| constraints.is_kept_name(name)
|
||||
|| root_unresolved_references.contains_key(name)
|
||||
|| (root_bindings.contains_key(name)
|
||||
&& (!constraints.top_level || constraints.exported_names.contains(name)))
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
//! ## Options
|
||||
//!
|
||||
//! - `--keep-names`: Preserve function and class names
|
||||
//! - `--reserve-exports`: Reserve `exports` / `module` binding names (cjs-module-lexer)
|
||||
//! - `--debug`: Enable debug output
|
||||
//! - `--twice`: Test idempotency by running twice
|
||||
|
||||
@@ -25,6 +26,7 @@ use oxc_mangler::{MangleOptions, MangleOptionsKeepNames, Mangler};
|
||||
use oxc_parser::Parser;
|
||||
use oxc_span::SourceType;
|
||||
use pico_args::Arguments;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
// Instruction:
|
||||
// create a `test.js`,
|
||||
@@ -34,6 +36,7 @@ fn main() -> std::io::Result<()> {
|
||||
let mut args = Arguments::from_env();
|
||||
|
||||
let keep_names = args.contains("--keep-names");
|
||||
let reserve_exports = args.contains("--reserve-exports");
|
||||
let debug = args.contains("--debug");
|
||||
let twice = args.contains("--twice");
|
||||
let name = args.free_from_str().unwrap_or_else(|_| "test.js".to_string());
|
||||
@@ -45,9 +48,14 @@ fn main() -> std::io::Result<()> {
|
||||
let options = MangleOptions {
|
||||
top_level: None,
|
||||
keep_names: MangleOptionsKeepNames { function: keep_names, class: keep_names },
|
||||
reserved: if reserve_exports {
|
||||
["exports", "module"].into_iter().map(Into::into).collect()
|
||||
} else {
|
||||
FxHashSet::default()
|
||||
},
|
||||
debug,
|
||||
};
|
||||
let printed = mangler(&source_text, source_type, options);
|
||||
let printed = mangler(&source_text, source_type, options.clone());
|
||||
println!("{printed}");
|
||||
|
||||
if twice {
|
||||
|
||||
@@ -8,14 +8,14 @@ use oxc_span::SourceType;
|
||||
|
||||
fn mangle_with_source_type(
|
||||
source_text: &str,
|
||||
options: MangleOptions,
|
||||
options: &MangleOptions,
|
||||
source_type: SourceType,
|
||||
) -> String {
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, source_text, source_type).parse();
|
||||
assert!(ret.diagnostics.is_empty(), "Parser errors: {:?}", ret.diagnostics);
|
||||
let program = ret.program;
|
||||
let mangler_return = Mangler::new().with_options(options).build(&program);
|
||||
let mangler_return = Mangler::new().with_options(options.clone()).build(&program);
|
||||
Codegen::new()
|
||||
.with_scoping(Some(mangler_return.scoping))
|
||||
.with_private_member_mappings(Some(mangler_return.class_private_mappings))
|
||||
@@ -23,15 +23,15 @@ fn mangle_with_source_type(
|
||||
.code
|
||||
}
|
||||
|
||||
fn mangle(source_text: &str, options: MangleOptions) -> String {
|
||||
fn mangle(source_text: &str, options: &MangleOptions) -> String {
|
||||
mangle_with_source_type(source_text, options, SourceType::mjs().with_unambiguous(true))
|
||||
}
|
||||
|
||||
fn mangle_script(source_text: &str, options: MangleOptions) -> String {
|
||||
fn mangle_script(source_text: &str, options: &MangleOptions) -> String {
|
||||
mangle_with_source_type(source_text, options, SourceType::script())
|
||||
}
|
||||
|
||||
fn test(source_text: &str, expected: &str, options: MangleOptions) {
|
||||
fn test(source_text: &str, expected: &str, options: &MangleOptions) {
|
||||
let mangled = mangle(source_text, options);
|
||||
let expected = {
|
||||
let allocator = Allocator::default();
|
||||
@@ -52,12 +52,12 @@ fn direct_eval() {
|
||||
|
||||
// Symbols in scopes with direct eval should NOT be mangled
|
||||
let source_text = "function foo() { let NO_MANGLE; eval('') }";
|
||||
let mangled = mangle(source_text, options);
|
||||
let mangled = mangle(source_text, &options);
|
||||
assert_eq!(mangled, "function foo() {\n\tlet NO_MANGLE;\n\teval(\"\");\n}\n");
|
||||
|
||||
// Nested direct eval: parent scope also should not mangle
|
||||
let source_text = "function foo() { let NO_MANGLE; function bar() { eval('') } }";
|
||||
let mangled = mangle(source_text, options);
|
||||
let mangled = mangle(source_text, &options);
|
||||
assert_eq!(
|
||||
mangled,
|
||||
"function foo() {\n\tlet NO_MANGLE;\n\tfunction bar() {\n\t\teval(\"\");\n\t}\n}\n"
|
||||
@@ -66,43 +66,43 @@ fn direct_eval() {
|
||||
// Sibling scope without direct eval should be mangled
|
||||
let source_text =
|
||||
"function foo() { let NO_MANGLE; eval('') } function bar() { let SHOULD_MANGLE; }";
|
||||
let mangled = mangle(source_text, options);
|
||||
let mangled = mangle(source_text, &options);
|
||||
// SHOULD_MANGLE gets mangled (to some short name), NO_MANGLE stays as is
|
||||
assert!(mangled.contains("NO_MANGLE"));
|
||||
assert!(!mangled.contains("SHOULD_MANGLE"));
|
||||
|
||||
// Child function scope without direct eval CAN be mangled (eval in parent cannot access child function locals)
|
||||
let source_text = "function foo() { eval(''); function bar() { let CAN_MANGLE; } }";
|
||||
let mangled = mangle(source_text, options);
|
||||
let mangled = mangle(source_text, &options);
|
||||
assert!(!mangled.contains("CAN_MANGLE"));
|
||||
|
||||
// Indirect eval should still allow mangling
|
||||
let source_text = "function foo() { let SHOULD_MANGLE; (0, eval)('') }";
|
||||
let mangled = mangle(source_text, options);
|
||||
let mangled = mangle(source_text, &options);
|
||||
assert!(!mangled.contains("SHOULD_MANGLE"));
|
||||
|
||||
test(
|
||||
r#"var e = () => {}; var foo = (bar) => e(bar); var pt = (() => { eval("") })();"#,
|
||||
r#"var e = () => {}; var foo = (t) => e(t); var pt = (() => { eval(""); })();"#,
|
||||
MangleOptions::default(),
|
||||
&MangleOptions::default(),
|
||||
);
|
||||
|
||||
test(
|
||||
r#"var e = () => {}; var foo = (bar) => e(bar); var pt = (() => { eval("") })();"#,
|
||||
r#"var e = () => {}; var foo = (t) => e(t); var pt = (() => { eval(""); })();"#,
|
||||
MangleOptions { top_level: Some(true), ..MangleOptions::default() },
|
||||
&MangleOptions { top_level: Some(true), ..MangleOptions::default() },
|
||||
);
|
||||
|
||||
test(
|
||||
r"function outer() { let e = 1; eval(''); function inner() { let longNameToMangle = 2; console.log(e); } }",
|
||||
r#"function outer() { let e = 1; eval(""); function inner() { let t = 2; console.log(e); } }"#,
|
||||
MangleOptions::default(),
|
||||
&MangleOptions::default(),
|
||||
);
|
||||
|
||||
test(
|
||||
r"function evalScope() { let x = 1; eval(''); } function siblingScope() { let longName = 2; console.log(longName); }",
|
||||
r#"function evalScope() {let x = 1; eval(""); } function siblingScope() { let e = 2; console.log(e); }"#,
|
||||
MangleOptions::default(),
|
||||
&MangleOptions::default(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,12 +165,12 @@ fn mangler() {
|
||||
let mut snapshot = String::new();
|
||||
cases.into_iter().fold(&mut snapshot, |w, case| {
|
||||
let options = MangleOptions::default();
|
||||
write!(w, "{case}\n{}\n", mangle(case, options)).unwrap();
|
||||
write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap();
|
||||
w
|
||||
});
|
||||
top_level_cases.into_iter().fold(&mut snapshot, |w, case| {
|
||||
let options = MangleOptions { top_level: Some(true), ..MangleOptions::default() };
|
||||
write!(w, "{case}\n{}\n", mangle(case, options)).unwrap();
|
||||
write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap();
|
||||
w
|
||||
});
|
||||
keep_name_cases.into_iter().fold(&mut snapshot, |w, case| {
|
||||
@@ -178,7 +178,7 @@ fn mangler() {
|
||||
keep_names: MangleOptionsKeepNames::all_true(),
|
||||
..MangleOptions::default()
|
||||
};
|
||||
write!(w, "{case}\n{}\n", mangle(case, options)).unwrap();
|
||||
write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap();
|
||||
w
|
||||
});
|
||||
|
||||
@@ -210,7 +210,7 @@ fn private_member_mangling() {
|
||||
let mut snapshot = String::new();
|
||||
cases.into_iter().fold(&mut snapshot, |w, case| {
|
||||
let options = MangleOptions::default();
|
||||
write!(w, "{case}\n{}\n", mangle(case, options)).unwrap();
|
||||
write!(w, "{case}\n{}\n", mangle(case, &options)).unwrap();
|
||||
w
|
||||
});
|
||||
|
||||
@@ -230,21 +230,21 @@ fn function_expression_name_shadowed() {
|
||||
test(
|
||||
"function _() { var x; var f = function foo() { var foo = x; } }",
|
||||
"function _() { var e; var t = function t() { var t = e; } }",
|
||||
options,
|
||||
&options,
|
||||
);
|
||||
|
||||
// Parameter shadow.
|
||||
test(
|
||||
"function _() { var x; (function foo(foo) { foo + x })() }",
|
||||
"function _() { var e; (function t(t) { t + e; })(); }",
|
||||
options,
|
||||
&options,
|
||||
);
|
||||
|
||||
// `var` inside an `else` block — still hoists through the block scope to the fn-expr scope.
|
||||
test(
|
||||
"function _() { var x; var f = function foo() { if (x) {} else { var foo = x; } } }",
|
||||
"function _() { var e; var t = function t() { if (e) {} else { var t = e; } } }",
|
||||
options,
|
||||
&options,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,8 +267,8 @@ fn shadowed_fn_expr_mangle_is_idempotent() {
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let pass1 = mangle(case, options);
|
||||
let pass2 = mangle(&pass1, options);
|
||||
let pass1 = mangle(case, &options);
|
||||
let pass2 = mangle(&pass1, &options);
|
||||
assert_eq!(
|
||||
pass1, pass2,
|
||||
"\nIdempotency failure for:\n{case}\nPass 1:\n{pass1}\nPass 2:\n{pass2}"
|
||||
@@ -289,7 +289,7 @@ fn destructured_exports_keep_names() {
|
||||
export const { tokenize, parse, find } = syntax;",
|
||||
"import e from 's';
|
||||
export const { tokenize, parse, find } = e;",
|
||||
options,
|
||||
&options,
|
||||
);
|
||||
|
||||
test(
|
||||
@@ -297,7 +297,7 @@ fn destructured_exports_keep_names() {
|
||||
export const [first, second, ...rest] = syntax;",
|
||||
"import e from 's';
|
||||
export const [first, second, ...rest] = e;",
|
||||
options,
|
||||
&options,
|
||||
);
|
||||
|
||||
// Renamed destructuring and nested patterns: only the bound names are exported.
|
||||
@@ -306,7 +306,72 @@ fn destructured_exports_keep_names() {
|
||||
export const { a: renamed, b: { nested = 1 } } = syntax;",
|
||||
"import e from 's';
|
||||
export const { a: renamed, b: { nested = 1 } } = e;",
|
||||
options,
|
||||
&options,
|
||||
);
|
||||
}
|
||||
|
||||
/// With `reserved: ["exports", "module"]`, those bindings keep their names. Node's
|
||||
/// cjs-module-lexer detects a CommonJS module's named exports by lexically scanning
|
||||
/// for `exports.<name> =` / `module.exports` token patterns — it does no scope
|
||||
/// analysis. UMD bundles bind `exports` / `module` as wrapper-function parameters
|
||||
/// (e.g. `async`'s `factory(exports)`); renaming the parameter erases every named
|
||||
/// export, breaking `import { queue } from "async"` at runtime in Node. `reserved` is
|
||||
/// empty by default, matching esbuild / terser / swc, which also rename these.
|
||||
#[test]
|
||||
fn reserved_names() {
|
||||
let options = MangleOptions {
|
||||
reserved: ["exports", "module"].into_iter().map(Into::into).collect(),
|
||||
..MangleOptions::default()
|
||||
};
|
||||
|
||||
// UMD factory parameter named `exports`.
|
||||
test(
|
||||
"(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined'
|
||||
? factory(exports)
|
||||
: factory((global.lib = {}));
|
||||
})(this, function (exports) {
|
||||
function queue(worker) { return worker; }
|
||||
exports.queue = queue;
|
||||
});",
|
||||
"(function (e, t) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined'
|
||||
? t(exports)
|
||||
: t((e.lib = {}));
|
||||
})(this, function (exports) {
|
||||
function t(e) { return e; }
|
||||
exports.queue = t;
|
||||
});",
|
||||
&options,
|
||||
);
|
||||
|
||||
// CommonJS wrapper parameters named `module` and `exports`.
|
||||
test(
|
||||
"(function (module, exports) {
|
||||
function helper(value) { return value; }
|
||||
module.exports.helper = helper;
|
||||
exports.other = helper;
|
||||
})(m, m.exports);",
|
||||
"(function (module, exports) {
|
||||
function t(e) { return e; }
|
||||
module.exports.helper = t;
|
||||
exports.other = t;
|
||||
})(m, m.exports);",
|
||||
&options,
|
||||
);
|
||||
|
||||
// Off by default: `exports` is renamed like any other binding (matching
|
||||
// esbuild / terser / swc), trading Node named-import detection for size.
|
||||
test(
|
||||
"(function (factory) { factory(exports); })(function (exports) {
|
||||
function queue(worker) { return worker; }
|
||||
exports.queue = queue;
|
||||
});",
|
||||
"(function (e) { e(exports); })(function (e) {
|
||||
function t(e) { return e; }
|
||||
e.queue = t;
|
||||
});",
|
||||
&MangleOptions::default(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -338,7 +403,7 @@ fn annex_b_block_scoped_function() {
|
||||
let mut snapshot = String::new();
|
||||
cases.into_iter().fold(&mut snapshot, |w, case| {
|
||||
let options = MangleOptions::default();
|
||||
write!(w, "{case}\n{}\n", mangle_script(case, options)).unwrap();
|
||||
write!(w, "{case}\n{}\n", mangle_script(case, &options)).unwrap();
|
||||
w
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ doctest = false
|
||||
oxc_allocator = { workspace = true }
|
||||
oxc_codegen = { workspace = true, features = ["sourcemap"] }
|
||||
oxc_compat = { workspace = true }
|
||||
oxc_str = { workspace = true }
|
||||
oxc_diagnostics = { workspace = true }
|
||||
oxc_minifier = { workspace = true }
|
||||
oxc_napi = { workspace = true }
|
||||
|
||||
Vendored
+11
@@ -137,6 +137,17 @@ export interface MangleOptions {
|
||||
* @default false
|
||||
*/
|
||||
keepNames?: boolean | MangleOptionsKeepNames
|
||||
/**
|
||||
* Names that bindings must not be renamed to, and that bindings already
|
||||
* carrying them keep. Equivalent to terser's `mangle.reserved`.
|
||||
*
|
||||
* Pass `['exports', 'module']` when minifying prebuilt CommonJS / UMD files
|
||||
* that Node consumers `import` directly, so Node's cjs-module-lexer can still
|
||||
* detect the mangled module's named exports.
|
||||
*
|
||||
* @default []
|
||||
*/
|
||||
reserved?: Array<string>
|
||||
/** Debug mangled names. */
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use napi::Either;
|
||||
use napi_derive::napi;
|
||||
|
||||
use oxc_compat::EngineTargets;
|
||||
use oxc_str::CompactStr;
|
||||
|
||||
#[napi(object)]
|
||||
pub struct TreeShakeOptions {
|
||||
@@ -221,6 +222,16 @@ pub struct MangleOptions {
|
||||
/// @default false
|
||||
pub keep_names: Option<Either<bool, MangleOptionsKeepNames>>,
|
||||
|
||||
/// Names that bindings must not be renamed to, and that bindings already
|
||||
/// carrying them keep. Equivalent to terser's `mangle.reserved`.
|
||||
///
|
||||
/// Pass `['exports', 'module']` when minifying prebuilt CommonJS / UMD files
|
||||
/// that Node consumers `import` directly, so Node's cjs-module-lexer can still
|
||||
/// detect the mangled module's named exports.
|
||||
///
|
||||
/// @default []
|
||||
pub reserved: Option<Vec<String>>,
|
||||
|
||||
/// Debug mangled names.
|
||||
pub debug: Option<bool>,
|
||||
}
|
||||
@@ -236,6 +247,9 @@ impl From<&MangleOptions> for oxc_minifier::MangleOptions {
|
||||
Some(Either::B(o)) => oxc_minifier::MangleOptionsKeepNames::from(o),
|
||||
None => default.keep_names,
|
||||
},
|
||||
reserved: o.reserved.as_ref().map_or(default.reserved, |names| {
|
||||
names.iter().map(|name| CompactStr::from(name.as_str())).collect()
|
||||
}),
|
||||
debug: o.debug.unwrap_or(default.debug),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ impl Oxc {
|
||||
options.mangle.map(|o| MangleOptions {
|
||||
top_level: Some(o.top_level),
|
||||
keep_names: MangleOptionsKeepNames { function: o.keep_names, class: o.keep_names },
|
||||
debug: false,
|
||||
..MangleOptions::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -120,7 +120,7 @@ fn bench_mangler(criterion: &mut Criterion) {
|
||||
.with_options(MangleOptions {
|
||||
top_level: None,
|
||||
keep_names: MangleOptionsKeepNames::all_true(),
|
||||
debug: false,
|
||||
..MangleOptions::default()
|
||||
})
|
||||
.build_with_semantic(&mut semantic, &program);
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ impl CompilerInterface for PipelineCompiler {
|
||||
}
|
||||
|
||||
fn mangle_options(&self) -> Option<MangleOptions> {
|
||||
Some(self.mangle)
|
||||
Some(self.mangle.clone())
|
||||
}
|
||||
|
||||
fn codegen_options(&self) -> Option<CodegenOptions> {
|
||||
|
||||
Reference in new issue
Block a user