fix(mangler): keep names of destructured exported bindings#24036
Merged
Conversation
Member
Author
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Merging this PR will not alter performance
Comparing Footnotes
|
Member
Author
Merge activity
|
`collect_exported_symbols` resolved each exported variable declarator with `get_binding_identifier`, which returns `None` for destructuring patterns — so the bindings of `export const { … } = …` were never marked as exported and got mangled, **renaming the module's named exports**. Collect the declarator's `bound_names` instead, which covers object / array patterns, nesting, defaults and rest elements.
Surfaced by the monitor-oxc mangler runtime test (previously masked by the idempotency failures fixed in #24033): mangled `css-tree` lost its `find` export and `@asamuzakjp/dom-selector` failed with `SyntaxError: The requested module 'css-tree' does not provide an export named 'find'` ([failing run](https://github.com/oxc-project/monitor-oxc/actions/runs/28562614557/job/84683456673)). The same export shape appears in commander, csso, colorette, ansis and `@actions/io`, so those were broken too.
## Example
```js
import syntax from "./syntax/index.js";
export const { tokenize, parse, find } = syntax;
```
Before:
```js
import e from "./syntax/index.js";
export const { tokenize: t, parse: n, find: s } = e; // exports `t`, `n`, `s`
```
After:
```js
import e from "./syntax/index.js";
export const { tokenize, parse, find } = e;
```
Verified end-to-end by mangling every file in `[email protected]` and exercising `import { find, parse, generate, walk } from "css-tree"` in node.
graphite-app
Bot
force-pushed
the
fix/mangler-destructured-exports
branch
from
July 2, 2026 05:51
c06a9a2 to
652fbaf
Compare
graphite-app Bot
pushed a commit
that referenced
this pull request
Jul 2, 2026
…ed (#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](swc-project/swc#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](evanw/esbuild#960), [cjs-module-lexer#44](nodejs/cjs-module-lexer#44)); verified that pattern also works appended to fully-mangled output. That could be a future zero-size-cost alternative at the minifier level.
camc314
pushed a commit
that referenced
this pull request
Jul 3, 2026
`collect_exported_symbols` resolved each exported variable declarator with `get_binding_identifier`, which returns `None` for destructuring patterns — so the bindings of `export const { … } = …` were never marked as exported and got mangled, **renaming the module's named exports**. Collect the declarator's `bound_names` instead, which covers object / array patterns, nesting, defaults and rest elements.
Surfaced by the monitor-oxc mangler runtime test (previously masked by the idempotency failures fixed in #24033): mangled `css-tree` lost its `find` export and `@asamuzakjp/dom-selector` failed with `SyntaxError: The requested module 'css-tree' does not provide an export named 'find'` ([failing run](https://github.com/oxc-project/monitor-oxc/actions/runs/28562614557/job/84683456673)). The same export shape appears in commander, csso, colorette, ansis and `@actions/io`, so those were broken too.
## Example
```js
import syntax from "./syntax/index.js";
export const { tokenize, parse, find } = syntax;
```
Before:
```js
import e from "./syntax/index.js";
export const { tokenize: t, parse: n, find: s } = e; // exports `t`, `n`, `s`
```
After:
```js
import e from "./syntax/index.js";
export const { tokenize, parse, find } = e;
```
Verified end-to-end by mangling every file in `[email protected]` and exercising `import { find, parse, generate, walk } from "css-tree"` in node.
camc314
pushed a commit
that referenced
this pull request
Jul 3, 2026
…ed (#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](swc-project/swc#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](evanw/esbuild#960), [cjs-module-lexer#44](nodejs/cjs-module-lexer#44)); verified that pattern also works appended to fully-mangled output. That could be a future zero-size-cost alternative at the minifier level.
Boshen
added a commit
that referenced
this pull request
Jul 6, 2026
### 🚀 Features - 260425f semantic/examples: Include unresolved references (#24214) (camc314) - 2d9b0b3 minifier: Fold boolean-literal ternary branches in value contexts (#24110) (Dunqing) - 61fbf10 ast: Implement `ReplaceWith` on all AST types (#24013) (overlookmotel) - 7db7a29 allocator: Add `ReplaceWith` trait (#24012) (overlookmotel) - 4eb074e mangler: Add `reserved` option for names that must not be mangled (#24041) (Dunqing) - 2e62012 data_structures: Add `StringExt` trait (#24006) (overlookmotel) - 60e7160 minifier: Drop side-effect-free IIFEs whose result is unused (#23967) (Dunqing) - 26dd9e2 ast: Add method to widen inherited enum ref to parent ref (#23961) (overlookmotel) ### 🐛 Bug Fixes - e8b50ee transformer: Clean up semantics for stripped TypeScript syntax (#24180) (camc314) - d966d0b react_compiler: Remove clippy allows (#24168) (Boshen) - 854ef8d react_compiler: Compile generic functions instead of over-bailing on type-param hoisting (#24158) (Boshen) - 093586c react_compiler: Align memoization cache-slot allocation with Babel (#24157) (Boshen) - 09c8f59 react_compiler: Normalize snapshot fixture paths (#24142) (camc314) - f13df97 react_compiler: Drop stray empty statement from catch bindings (#24133) (Boshen) - cb2a505 react_compiler: Codegen destructuring reassignment targets (#24131) (Boshen) - b82c394 react_compiler: Propagate codegen invariants instead of emitting empty bodies (#24128) (Boshen) - 5771982 react_compiler: Render unchanged programs as source in fixture snapshots (#24129) (Boshen) - 4b16e1a transformer/async-to-generator: Preserve direct eval scope flags (#24136) (camc314) - 4e9194f react_compiler: Lower `delete obj.prop` to Property/ComputedDelete (#24123) (Boshen) - 0b25582 ast: Type binding node `typeAnnotation` as `TSTypeAnnotation | null` (#23113) (Boshen) - 018c0e5 transformer: Hoist lowered async declarations (#22770) (camc314) - 652fbaf mangler: Keep names of destructured exported bindings (#24036) (Dunqing) - e274415 minifier: Don't drop global calls that throw despite pure arguments (#23917) (Dunqing) - 59abb30 minifier: Only merge string literals in `try_fold_add` when the inner operator is `+` (#23622) (Jerry Zhao) ### ⚡ Performance - c5ca77b transformer: Avoid cloning refresh options (#24191) (camc314) - bf1a151 react_compiler: Compile out debug printers (#24184) (Boshen) - abb44a0 transformer: Build fixed object-rest arguments (#24190) (camc314) - a4db731 isolated_declarations: Use `ReplaceWith` instead of `TakeIn` (#24016) (overlookmotel) - ff10855 transformer: Use `ReplaceWith` instead of `TakeIn` (#24015) (overlookmotel) - bd49aff ecmascript: Avoid heap-allocating Math.min/max/imul operands (#23941) (Lawrence Lin) - e4b708b react_compiler: Skip compiled files before prefilters (#24171) (Boshen) - c59f2fe rust: Return impl ExactSizeIterator from slice-backed accessors (#24144) (Boshen) - 5d6d04a codegen: SWAR-skip boring byte runs in sourcemap line/column scan (#24023) (Boshen) - a55e0be traverse: Reduce string operations in `get_var_name_from_node` (#24007) (overlookmotel) - e6d48e1 transformer/nullish_coalescing: Move cold path into separate function (#23989) (overlookmotel) - c4e35b5 transformer/object_rest_spread: Pre-allocate capacity in `Vec` (#23988) (overlookmotel) - 527b8e5 transformer/decorators: Narrow type earlier (#23987) (overlookmotel) ### 📚 Documentation - 30d17f5 allocator: Clarify docs for `TakeIn::take_in_box` (#24093) (overlookmotel) - 675e6a8 ast: Correct doc comment for `PrivateFieldExpression` (#24008) (overlookmotel) - e4c30e6 minifier: Explain what `dce` mode means (#23994) (Dunqing) - 37cbf88 ast_macros: Document fields of `StructDetails` (#23959) (overlookmotel) - 4de3e54 ast: Correct doc comment (#23948) (overlookmotel) Co-authored-by: Boshen <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

collect_exported_symbolsresolved each exported variable declarator withget_binding_identifier, which returnsNonefor destructuring patterns — so the bindings ofexport const { … } = …were never marked as exported and got mangled, renaming the module's named exports. Collect the declarator'sbound_namesinstead, which covers object / array patterns, nesting, defaults and rest elements.Surfaced by the monitor-oxc mangler runtime test (previously masked by the idempotency failures fixed in #24033): mangled
css-treelost itsfindexport and@asamuzakjp/dom-selectorfailed withSyntaxError: The requested module 'css-tree' does not provide an export named 'find'(failing run). The same export shape appears in commander, csso, colorette, ansis and@actions/io, so those were broken too.Example
Before:
After:
Verified end-to-end by mangling every file in
[email protected]and exercisingimport { find, parse, generate, walk } from "css-tree"in node.