fix(transformer/typescript): keep enum IIFE when a non-inlinable value reference remains#22501
Conversation
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
|
da9fe2a to
ae6439a
Compare
b7fe1e2 to
62376dd
Compare
ae6439a to
c723603
Compare
Merge activity
|
… remains (#22501) Refs vitejs/vite#22452 (comment). The optimize pass dropped the enum declaration whenever the members were all evaluable — regardless of whether some non-inlinable value reference still named the enum at runtime. The result referenced a binding that no longer existed: ```ts const enum Phase { one = 'one' } export default Phase; // -> export default Phase; // ReferenceError at runtime enum Direction { Up } Direction.Up.toString(); // -> Direction.Up.toString(); // ReferenceError at runtime ``` ## Fix Decide based on the remaining resolved references: drop only when none of them is a value reference (`Read`/`Write`). Type references (`as E`, `: E`) are kept here and stripped later by `annotations.rs`, so they don't block removal. `typeof E` in JS expression position is `Read` and correctly keeps the IIFE; `typeof E` in TS type position is `ValueAsType` (not in `Value`) and doesn't. Unifies the rule for regular and const enums — the previous asymmetry (const always dropped, regular dropped only when no refs at all) had no principled basis. ## Compatibility Diverges from Babel on two `optimize-const-enums/*` fixtures where the input has a bare value reference to the enum that we can't inline. Three engines disagree on what to do: | Engine | Inline `E.X` member accesses | Preserve `E` binding (IIFE) | Result for `E.unknown` / bare `E` | |---|---|---|---| | Babel `transform-typescript` (optimize-const-enums) | yes | **no** | runtime ReferenceError / TypeError | | tsc `--isolatedModules` | no | yes | `undefined` / resolves | | oxc after this PR | yes | yes | `undefined` / resolves | We now hybridize: inline what's inlinable (Babel) **and** keep the binding (tsc). Local overrides under `tasks/transform_conformance/overrides/` record the new expected output for the two affected Babel fixtures. ### `optimize-const-enums/local` Input: ```ts const enum A { x, y } A.x; A["y"]; A.z; A; ``` Babel (and oxc before this PR): ```js 0; 1; A.z; A; ``` `A.z` is `TypeError: cannot read properties of undefined`; `A;` is `ReferenceError`. tsc (`--isolatedModules`): ```js var A; (function (A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; })(A || (A = {})); A.x; A["y"]; A.z; A; ``` Keeps the IIFE binding, no member-access inlining. `A.z` evaluates to `undefined`; `A;` is a no-op. oxc after this PR (`overrides/.../optimize-const-enums/local/output.mjs`): ```js var A = /* @__PURE__ */ function(A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; return A; }(A || {}); 0; 1; A.z; A; ``` Hybrid: inlines `A.x` / `A["y"]` like Babel, **and** keeps the IIFE binding like tsc — so `A.z` evaluates to `undefined` (not crash) and `A;` resolves. ### `optimize-const-enums/merged` Input: ```ts const enum A { x, y } const enum A { z } A.x; A["y"]; A.z; A.w; A; ``` Babel (and oxc before this PR): ```js 0; 1; 0; A.w; A; ``` `A.w` is `TypeError: cannot read properties of undefined`; `A;` is `ReferenceError`. tsc (`--isolatedModules`): ```js var A; (function (A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; })(A || (A = {})); (function (A) { A[A["z"] = 0] = "z"; })(A || (A = {})); A.x; A["y"]; A.z; A.w; A; ``` Both IIFEs emitted, no inlining. `A.w` is `undefined`; `A;` resolves. oxc after this PR (`overrides/.../optimize-const-enums/merged/output.mjs`): ```js var A = /* @__PURE__ */ function(A) { A[A["x"] = 0] = "x"; A[A["y"] = 1] = "y"; return A; }(A || {}); A = /* @__PURE__ */ function(A) { A[A["z"] = 0] = "z"; return A; }(A || {}); 0; 1; 0; A.w; A; ``` Hybrid: both enum bodies lower (the second reuses the first's binding via `A = (function(A) { … })(A || {})` — assignment, not `var`) **and** members are inlined like Babel. `A.w` evaluates to `undefined`; `A;` resolves. ## Verification New fixtures cover the bug shapes: - `babel-plugin-transform-typescript/const-enum-value-ref-kept/` — `export default Phase` - `babel-plugin-transform-typescript/const-enum-mixed-refs/` — member access + bare ref combined Existing `optimize-enums/type-cast` updated to drop the IIFE for the all-type-refs case (the regular-enum side of the unified rule).
62376dd to
931b7d6
Compare
c723603 to
e6090e7
Compare
### 🐛 Bug Fixes - 0f26de6 ecmascript: Resolve identifier value type via tracked constants (#22234) (Alexander Lichter) - c27a8cf minifier: Normalize `{ x: x }` shorthand so adjacent-if merge is idempotent (#22401) (Dunqing) - e431a0e parser: Break extends clause loop on fatal error (#22517) (Boshen) - e9ec7c6 minifier: Fold optional chains by base nullishness (#22236) (Alexander Lichter) - e6090e7 transformer: Keep enum IIFE when a non-inlinable value reference remains (#22501) (Dunqing) - 931b7d6 transformer: Inline const enum members through type-cast wrappers (#22500) (Dunqing) - b9615b2 codegen: Preserve string quotes in require() calls during minification (#22475) (zennnnnnn11) - c73c159 transformer/async-to-generator: Reparent parameter initializer scopes (#22507) (camc314) - ecfd3ca transformer/async-to-generator: Move only parameter bindings (#22503) (camc314) - 3ce3431 transformer/explicit-resource-managment: Preserve shadowed for-head block (#22451) (camc314) ### ⚡ Performance - ce92c6c semantic: `#[inline]` `Scoping::get_binding` (#22414) (Dunqing) - 98be95c regular_expression: Track regex flags via bitflags (#22427) (Boshen) - dbbc059 jsdoc: Skip should_attach_jsdoc when no remaining comments (#22409) (Boshen) - 217d7d8 minifier: Index `SymbolValues` by `SymbolId` (#22441) (Dunqing) - d782b78 minifier: Use BitSet for LiveUsageCollector live references (#22425) (Boshen)

Refs vitejs/vite#22452 (comment).
The optimize pass dropped the enum declaration whenever the members were all evaluable — regardless of whether some non-inlinable value reference still named the enum at runtime. The result referenced a binding that no longer existed:
Fix
Decide based on the remaining resolved references: drop only when none of them is a value reference (
Read/Write). Type references (as E,: E) are kept here and stripped later byannotations.rs, so they don't block removal.typeof Ein JS expression position isReadand correctly keeps the IIFE;typeof Ein TS type position isValueAsType(not inValue) and doesn't.Unifies the rule for regular and const enums — the previous asymmetry (const always dropped, regular dropped only when no refs at all) had no principled basis.
Compatibility
Diverges from Babel on two
optimize-const-enums/*fixtures where the input has a bare value reference to the enum that we can't inline. Three engines disagree on what to do:E.Xmember accessesEbinding (IIFE)E.unknown/ bareEtransform-typescript(optimize-const-enums)--isolatedModulesundefined/ resolvesundefined/ resolvesWe now hybridize: inline what's inlinable (Babel) and keep the binding (tsc). Local overrides under
tasks/transform_conformance/overrides/record the new expected output for the two affected Babel fixtures.optimize-const-enums/localInput:
Babel (and oxc before this PR):
A.zisTypeError: cannot read properties of undefined;A;isReferenceError.tsc (
--isolatedModules):Keeps the IIFE binding, no member-access inlining.
A.zevaluates toundefined;A;is a no-op.oxc after this PR (
overrides/.../optimize-const-enums/local/output.mjs):Hybrid: inlines
A.x/A["y"]like Babel, and keeps the IIFE binding like tsc — soA.zevaluates toundefined(not crash) andA;resolves.optimize-const-enums/mergedInput:
Babel (and oxc before this PR):
A.wisTypeError: cannot read properties of undefined;A;isReferenceError.tsc (
--isolatedModules):Both IIFEs emitted, no inlining.
A.wisundefined;A;resolves.oxc after this PR (
overrides/.../optimize-const-enums/merged/output.mjs):Hybrid: both enum bodies lower (the second reuses the first's binding via
A = (function(A) { … })(A || {})— assignment, notvar) and members are inlined like Babel.A.wevaluates toundefined;A;resolves.Verification
New fixtures cover the bug shapes:
babel-plugin-transform-typescript/const-enum-value-ref-kept/—export default Phasebabel-plugin-transform-typescript/const-enum-mixed-refs/— member access + bare ref combinedExisting
optimize-enums/type-castupdated to drop the IIFE for the all-type-refs case (the regular-enum side of the unified rule).