feat(minifier): add property name mangling - #24740
Conversation
Merging this PR will not alter performance
Performance Changes
Comparing Footnotes
|
e9070cb to
75c9e72
Compare
6a6957b to
abf6ec8
Compare
|
I think this PR is ready to review; this new implementation resolved all reviews of prior attempted in #23772. |
Boshen
left a comment
There was a problem hiding this comment.
Did you verify against test262 runtime tests?
Boshen
left a comment
There was a problem hiding this comment.
PR description, comments, and documentation require work to enable maintainers to understand how this feature works.
Boshen
left a comment
There was a problem hiding this comment.
Should we enable this in cargo minsize?
|
Would you add the reason why this lives in |
|
Added this rationale to the PR description. Property mangling must inspect and rewrite property-bearing AST syntax before compression erases quote and key context. |
|
Enabled property mangling in both test262 runtime and minsize and all look good. |
c0edad4 to
599c9be
Compare
sapphi-red
left a comment
There was a problem hiding this comment.
Does the transformer keep /* #__KEY__ */? If not, I guess that would be needed.
599c9be to
5f08776
Compare
Good catch. The transformer won't touch comments, but currently, codegen will miss printing comments in some places, especially for some inline comments. I will check and fill the gap after the merge. |
|
Merging it, the comments issue was worked on #25766 |
Merge activity
|
Oxc can shorten variable names and private class names, but it cannot shorten property names such as `obj._field`. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files. ### Feature - `include` selects names to mangle. `exclude` and `reserved` keep names unchanged. - The cache can give a property a fixed output name or keep it unchanged. - The mangler supports member access, object and class keys, destructuring, optional chains, the left side of `in`, and JSX property names. - `mangle_quoted` also handles strings and template literals used directly as property keys. `/* @__KEY__ */` and `/* #__KEY__ */` can mark one other string or template literal as a property key. - Short names are assigned in a stable order. More common properties get shorter names. Existing and reserved names are avoided. - Bundlers can collect names from several programs, create one shared map, and apply it to every program. - Numeric keys, `__proto__`, `constructor`, `prototype`, TypeScript-only keys, and private names are never changed by this pass. ### Design Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in `oxc_minifier`. It only reuses the short-name generator from `oxc_mangler`. For one program, the order is: ```text collect names → assign names → rewrite once → compress → mangle variables and private names ``` Property names are rewritten before compression because compression can remove information about quotes and comments. For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options. The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls. ### Example With `include: "^_"` and `mangle_quoted: true`: ```js obj[`_field`] = 1; obj._field = 2; ``` becomes: ```js obj[`e`] = 1; obj.e = 2; ``` ### Limitation - Property mangling is unsafe when other code still uses the old property name. Public APIs and names shared with external code, host APIs, imported namespace objects, or separate builds must be excluded or reserved. - Quote handling is per use. Without `mangle_quoted`, `obj._field` may change while `obj["_field"]` does not. - The mangler cannot find names built at runtime or stored in arbitrary strings. Code that uses `eval`, `Function`, or `with` must reserve any affected names. - Compression may create a matching property after this pass has finished. For example, `'f' + 'oo_'` may later become `foo_` and will not be mangled. - TypeScript enum reverse mappings are not supported because the transform creates quoted strings. - Multi-program support is a caller API. The caller must collect every program that shares mangled properties before assigning names. The public `oxc-minify` API is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374. Supersedes #23772. AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.
5f08776 to
2f5cdb1
Compare
oxc-project/oxc#24740 Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
The property mangler added in #24740 is not available through `oxc-minify`. This adds `mangleProps` for configuring it and `mangleCache` for reusing chosen property names. ### Feature - `include` selects property names to mangle. `exclude` and `reserved` keep names unchanged. - `quoted` also mangles quoted property names. `debug` produces readable output names. - `cache` fixes a property to a chosen name. A `false` value keeps the original name. - Property mangling works in both sync and async APIs. It is separate from identifier mangling. - When property mangling is enabled and the call succeeds, the result includes the updated `mangleCache`. - Invalid `mangleProps` regexes and cache entries are returned in `result.errors`. ### Design The binding converts JavaScript values into core minifier options and results. The core minifier still chooses names and rewrites properties, so both APIs follow the same rules. A parser error may leave an incomplete AST. In that case, property mangling is skipped and no partial cache is returned. ### Example ```js const result = minifySync( "test.js", "let local; obj._often; obj._rare; obj._often;", { compress: false, mangle: false, mangleProps: { include: "^_" }, }, ); result.code; // "let local;obj.e;obj.t;obj.e;" result.mangleCache; // { _often: "e", _rare: "t" } ``` ### Limitation - `include` and `exclude` use Rust regex syntax. They match anywhere unless the pattern includes anchors. - Do not reuse the cache on already-mangled output. A mapping is not always safe to apply twice. - The cache only remembers recorded mappings. It cannot find unchanged names in another input or in external code. Reserve, exclude, or pin those names before the first call. - String cache targets must be valid JavaScript `IdentifierName` values and cannot be `__proto__`, `constructor`, or `prototype`. The original name `__proto__` cannot be a cache key. AI assistance: OpenAI Codex and Claude were used for research, implementation, adversarial review, and verification. The contributor remains responsible for reviewing, understanding, and submitting the changes under the repository AI usage policy.
oxc-project/oxc#24740 Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
### 💥 BREAKING CHANGES - 365274e packages/codegen: [**BREAKING**] `printSync` return an object (#25720) (overlookmotel) ### 🚀 Features - 6a7eb60 packages/codegen: Alter capitalization of `sourceFilename` option (#25854) (overlookmotel) - 300763f packages/codegen: Add full source map support (#25585) (camc314) - a169e4a napi/minify: Expose property name mangling options (#24741) (Dunqing) - 2f5cdb1 minifier: Add property name mangling (#24740) (Dunqing) - 4922141 mangler: Deduplicate private accessor names (#25601) (camc314) - 1c4f519 napi: Add Relay transform plugin (#25503) (Boshen) - a4478e9 codegen: Add `oxc-codegen` package (#25488) (overlookmotel) ### 🐛 Bug Fixes - 0fbcf64 codegen: Validate sourcemap options (#25860) (camc314) - 345f981 react_compiler: Skip node_modules by default (#25859) (Boshen) - ab81f3f minifier: Drop side-effect-free additions (#25639) (Dunqing) - 241c559 napi/minify: Accept RegExp for property filters (#25827) (Dunqing) - 4cc7ea4 codegen: Reject invalid indent options (#25807) (camc314) - 53f9295 isolated-declarations: Preserve undefined for defaulted any (#25292) (camc314) - 8ab883a codegen: Preserve property key annotations (#25766) (Dunqing) - b13fd48 minifier: Model uninitialized module vars as undefined (#25497) (Dunqing) - f6000ac ecmascript: Fold `**` with `Number::exponentiate`, not IEEE `pow` (#25644) (Kotaro Chikuba) - b846abc isolated-declarations: Handle ambient expando properties (#25655) (camc314) - dec0a86 react-compiler: Standardize diagnostics (#25702) (Boshen) - fca2e0c parser: Reject initialized lexical declarations in for-in (#25700) (Boshen) - ae9be8f parser: Forbid type parameters on quoted constructors (#25696) (Boshen) - c5f188a react-compiler: Use Babel v1 validation defaults (#25676) (Boshen) - db44651 napi: Disable reuseWorker in browser bindings (#25640) (leaysgur) - ce35d47 react_compiler: Preserve JSX import source pragmas (#25592) (Boshen) - 059784d semantic: Respect shadowed `Infinity` and `NaN` in enum evaluation (#25604) (camc314) - bb5a232 minifier: Keep variable declaration initilized with class when keepNames is enabled (#25584) (sapphi-red) - f49229d minifier: Keep side effects when rotating bitwise operands (#25596) (Kotaro Chikuba) - e82495b ecmascript: Derive `ToNumber` of `!x` from `ToBoolean` (#25595) (Kotaro Chikuba) - 509931b semantic: Classify global references per identifier (#25608) (camc314) - 80484ce mangler: Correct base54 safety comment (#25606) (camc314) - c1369a7 codegen: Resolve private names in class heritage (#25588) (camc314) - c002f29 codegen: Escape sources for empty import specifiers (#25586) (camc314) - 0c68b7f estree: Emit `decorators` on `FormalParameterRest` (#25582) (camc314) - cdf1846 semantic: Allow legacy escapes in JSX attributes (#25576) (Boshen) - e75e102 minifier: Preserve block statement in labeled iteration statements (#25162) (Armano) - 771d79a mangler: Exclude non-manglable symbols from slot assignment (#25539) (sapphi-red) - 76b19f5 minifier: Avoid duplicating large folded strings (#25532) (Dunqing) - 59e8895 codegen: Validate starting indent level (#25550) (camc314) - 88b34f2 clippy: Remove unneeded `unsafe` (#25551) (camc314) - 23a7ad0 parser: Stop delimited lists at end of file (#25542) (Boshen) - 73acba9 parser: Preserve fatal errors during await reparse (#25541) (Boshen) - c3e99d1 minifier: Avoid invalid octal escapes in template folds (#25495) (Dunqing) - b4e6a9e codegen: Output newline after `export default interface` (#25487) (overlookmotel) - 5fcf683 minifier: Correct issue with try finally termination (#25185) (Armano) - 1645d93 react_compiler: Preserve source spans (#25462) (Boshen) - 8d7f9cf react_compiler: Honor eslint suppressions (#25394) (Boshen) ### ⚡ Performance - 568203e ecmascript: Use binary search for known globals (#25817) (Boshen) - c84ede3 estree_tokens: Share JS token update entry point (#25826) (Boshen) - 9a6e862 minifier: Move owned statements directly (#25835) (Dunqing) - 61b2aef minifier: Move owned AST nodes directly (#25837) (Dunqing) - 673b04b minifier: Replace expressions without take_in dummies (#25836) (Dunqing) - 63ff8ef linter: Outline diagnostic construction (#25762) (Boshen) - 621808e diagnostics: Measure graphemes lazily (#25723) (Boshen) - f62ed0e diagnostics: Optimize graphical number rendering (#25715) (Boshen) - 7152834 diagnostics: Reduce graphical formatting overhead (#25714) (Boshen) - 10fc4b7 diagnostics: Reduce graphical rendering allocations (#25711) (Boshen) - 9c8abab diagnostics: Batch graphical reports (#25710) (Boshen) - 757f3d4 mangler: Share allocated names across reused slots (#25605) (camc314) - 5444cbf codegen: Skip escaping harmless `<` tokens (#25564) (camc314) ### 📚 Documentation - 58f7ab9 packages/codegen: Reformat docs and comments (#25848) (overlookmotel) - 627466e transform-react: Document all options (#25800) (Boshen) - ce03ac1 packages/codegen: Fix JSDoc comments for `printSync` (#25722) (overlookmotel) - 386a699 packages/codegen: Correct JSDoc comment (#25716) (overlookmotel) - 31e571d mangler: Update code example (#25599) (camc314) - fd62354 codegen: Revamp package readme (#25560) (camc314) - ffa3153 codegen: Clarify raw transfer Node requirement (#25565) (camc314) - c5d4063 codegen: Clarify binary walk allocations (#25562) (camc314) - 5703d7a codegen: Correct stale printer comments (#25561) (camc314) Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
oxc-project/oxc#24740 Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
oxc-project/oxc#24740 Oxc-Revision: 2f5cdb1231d217ba33d0c0ee777fa36095c939b4
|
I wonder... is there a way to have the minifier auto-mangle properties in classes that are never exposed outside a module? e.g.: // reader.ts
class FooReader {
read(text: string) {}
}
// The only method that gets exported from the module, the class is never exposed
export const readThing = (text: string) => {
return new FooReader().read(text);
}Having to configure properly mangling rules kind of feels like I now need to know the inner workings of library/package code instead of the bundler knowing that something can be mangled already 🤔 |
Oxc can shorten variable names and private class names, but it cannot shorten property names such as
obj._field. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files.Feature
includeselects names to mangle.excludeandreservedkeep names unchanged.in, and JSX property names.mangle_quotedalso handles strings and template literals used directly as property keys./* @__KEY__ */and/* #__KEY__ */can mark one other string or template literal as a property key.__proto__,constructor,prototype, TypeScript-only keys, and private names are never changed by this pass.Design
Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in
oxc_minifier. It only reuses the short-name generator fromoxc_mangler.For one program, the order is:
Property names are rewritten before compression because compression can remove information about quotes and comments.
For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options.
The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls.
Example
With
include: "^_"andmangle_quoted: true:becomes:
Limitation
mangle_quoted,obj._fieldmay change whileobj["_field"]does not.eval,Function, orwithmust reserve any affected names.'f' + 'oo_'may later becomefoo_and will not be mangled.The public
oxc-minifyAPI is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374.Supersedes #23772.
AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.