mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
4eb074e69d
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.
81 lines
2.5 KiB
Rust
81 lines
2.5 KiB
Rust
#![expect(clippy::print_stdout)]
|
|
//! # Variable Name Mangling Example
|
|
//!
|
|
//! This example demonstrates variable name mangling using the Oxc mangler.
|
|
//! It shortens variable names to reduce file size.
|
|
//!
|
|
//! ## Usage
|
|
//!
|
|
//! Create a `test.js` file and run:
|
|
//! ```bash
|
|
//! cargo run -p oxc_minifier --example mangler [filename] [options]
|
|
//! ```
|
|
//!
|
|
//! ## 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
|
|
|
|
use std::path::Path;
|
|
|
|
use oxc_allocator::Allocator;
|
|
use oxc_codegen::Codegen;
|
|
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`,
|
|
// run `cargo run -p oxc_minifier --example mangler`
|
|
|
|
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());
|
|
|
|
let path = Path::new(&name);
|
|
let source_text = std::fs::read_to_string(path)?;
|
|
let source_type = SourceType::from_path(path).unwrap();
|
|
|
|
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.clone());
|
|
println!("{printed}");
|
|
|
|
if twice {
|
|
let printed2 = mangler(&printed, source_type, options);
|
|
println!("{printed2}");
|
|
println!("same = {}", printed == printed2);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn mangler(source_text: &str, source_type: SourceType, options: MangleOptions) -> String {
|
|
let allocator = Allocator::default();
|
|
let ret = Parser::new(&allocator, source_text, source_type).parse();
|
|
assert!(ret.diagnostics.is_empty());
|
|
let mangler_return = Mangler::new().with_options(options).build(&ret.program);
|
|
Codegen::new()
|
|
.with_scoping(Some(mangler_return.scoping))
|
|
.with_private_member_mappings(Some(mangler_return.class_private_mappings))
|
|
.build(&ret.program)
|
|
.code
|
|
}
|