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.
128 lines
4.5 KiB
Rust
128 lines
4.5 KiB
Rust
use std::path::Path;
|
|
|
|
use oxc::{
|
|
CompilerInterface,
|
|
codegen::CodegenOptions,
|
|
mangler::MangleOptions,
|
|
minifier::CompressOptions,
|
|
transformer::TransformOptions,
|
|
transformer_plugins::{InjectGlobalVariablesConfig, InjectImport, ReplaceGlobalDefinesConfig},
|
|
};
|
|
use oxc_benchmark::{BenchmarkId, Criterion, criterion_group, criterion_main};
|
|
use oxc_tasks_common::TestFiles;
|
|
|
|
/// A representative Vite-style define config. Unlike a single dot define, this exercises every
|
|
/// `ReplaceGlobalDefines` dispatch path — identifier, dot, specific `import.meta.env.*`, and
|
|
/// trailing wildcards — so the benchmark reflects the per-node lookup cost of a real config.
|
|
const DEFINES: &[(&str, &str)] = &[
|
|
// identifier defines
|
|
("__DEV__", "false"),
|
|
("__PROD__", "true"),
|
|
("__VERSION__", "'1.2.3'"),
|
|
("__TEST__", "false"),
|
|
("DEBUG", "false"),
|
|
// dot defines
|
|
("process.env.NODE_ENV", "'production'"),
|
|
("process.env.PLATFORM", "'browser'"),
|
|
("process.env.VERSION", "'1.2.3'"),
|
|
("globalThis.__DEV__", "false"),
|
|
// specific `import.meta.env` entries
|
|
("import.meta.env.MODE", "'production'"),
|
|
("import.meta.env.BASE_URL", "'/'"),
|
|
("import.meta.env.PROD", "true"),
|
|
("import.meta.env.DEV", "false"),
|
|
("import.meta.env.SSR", "false"),
|
|
("import.meta.env.VITE_API_URL", "'https://example.com'"),
|
|
("import.meta.env.VITE_APP_TITLE", "'app'"),
|
|
// trailing wildcards
|
|
("import.meta.env.*", "undefined"),
|
|
("import.meta.*", "undefined"),
|
|
];
|
|
|
|
/// A [`CompilerInterface`] that runs the complete compilation pipeline:
|
|
/// parse -> semantic -> transform -> define -> inject -> minify -> mangle -> codegen.
|
|
struct PipelineCompiler {
|
|
transform: TransformOptions,
|
|
define: ReplaceGlobalDefinesConfig,
|
|
inject: InjectGlobalVariablesConfig,
|
|
compress: CompressOptions,
|
|
mangle: MangleOptions,
|
|
codegen: CodegenOptions,
|
|
}
|
|
|
|
impl CompilerInterface for PipelineCompiler {
|
|
fn transform_options(&self) -> Option<&TransformOptions> {
|
|
Some(&self.transform)
|
|
}
|
|
|
|
fn define_options(&self) -> Option<ReplaceGlobalDefinesConfig> {
|
|
Some(self.define.clone())
|
|
}
|
|
|
|
fn inject_options(&self) -> Option<InjectGlobalVariablesConfig> {
|
|
Some(self.inject.clone())
|
|
}
|
|
|
|
fn compress_options(&self) -> Option<CompressOptions> {
|
|
Some(self.compress.clone())
|
|
}
|
|
|
|
fn mangle_options(&self) -> Option<MangleOptions> {
|
|
Some(self.mangle.clone())
|
|
}
|
|
|
|
fn codegen_options(&self) -> Option<CodegenOptions> {
|
|
Some(self.codegen.clone())
|
|
}
|
|
|
|
fn check_semantic_error(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn bench_pipeline(criterion: &mut Criterion) {
|
|
let mut group = criterion.benchmark_group("pipeline");
|
|
|
|
for file in TestFiles::minimal().files() {
|
|
let id = BenchmarkId::from_parameter(&file.file_name);
|
|
let source_text = &file.source_text;
|
|
let source_type = file.source_type;
|
|
let source_path = Path::new(&file.file_name);
|
|
|
|
// `compile` creates its own `Allocator` and keeps no state between runs,
|
|
// so the compiler and its options can be built once and reused.
|
|
let mut compiler = PipelineCompiler {
|
|
transform: TransformOptions::from_target("esnext").unwrap(),
|
|
define: ReplaceGlobalDefinesConfig::new(DEFINES).unwrap(),
|
|
// Common global-variable injections: Node.js polyfills (when bundling for the
|
|
// browser) plus a classic-runtime JSX auto-import. Like the defines, these exercise
|
|
// the per-reference inject matching rather than a single specifier.
|
|
inject: InjectGlobalVariablesConfig::new(vec![
|
|
InjectImport::named_specifier("node:buffer", Some("Buffer"), "Buffer"),
|
|
InjectImport::default_specifier("node:process", "process"),
|
|
InjectImport::named_specifier("node:timers", Some("setImmediate"), "setImmediate"),
|
|
InjectImport::named_specifier(
|
|
"node:timers",
|
|
Some("clearImmediate"),
|
|
"clearImmediate",
|
|
),
|
|
InjectImport::default_specifier("react", "React"),
|
|
]),
|
|
compress: CompressOptions::smallest(),
|
|
mangle: MangleOptions::default(),
|
|
codegen: CodegenOptions::default(),
|
|
};
|
|
|
|
group.bench_function(id, |b| {
|
|
b.iter(|| {
|
|
compiler.compile(source_text, source_type, source_path);
|
|
});
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
criterion_group!(pipeline, bench_pipeline);
|
|
criterion_main!(pipeline);
|