## Summary
Fix ES2018 object-rest lowering when one variable declaration contains multiple object-rest declarators.
### Before
Given:
```js
let { a, ...r } = x, { b, ...s } = y;
```
Oxc produced:
```js
let { a } = x, { b } = y,
s = babelHelpers.objectWithoutProperties(y, ["b"]),
{} = null;
```
The `r` binding was lost.
Each replacement was recorded using its original declarator index. Replacing the first declarator expanded it into multiple entries, shifting the second declarator. The subsequent replacement still used the old index and overwrote part of the first expansion.
### After
Oxc now produces:
```js
let { a } = x,
r = babelHelpers.objectWithoutProperties(x, ["a"]),
{ b } = y,
s = babelHelpers.objectWithoutProperties(y, ["b"]);
```
Queued replacements are applied from right to left. Replacing a later declarator cannot change the index of an earlier one, so every recorded index remains valid.
Regression coverage includes multiple `let`, `const`, and `var` declarators.
## Summary
- add transformer conformance coverage for named re-exports from `.mts` modules
- verify `export { x } from "./a.mts"` rewrites to the `.mjs` extension
## Summary
Ports facebook/react#35318 to the React Refresh plugin: hook calls through a nested member expression such as `FancyHook.property.useNestedThing()` are now collected into the `$RefreshSig$` custom-hooks list, so components using them no longer lose state on every fast-refresh update. As in the Babel plugin, the binding is resolved through one extra static member level, and a force reset is emitted when the root object has no binding in scope. Deeper nesting (`A.B.C.useHook()`) remains uncollected, same as upstream; note that Babel additionally emits `forceReset = true` in that case while oxc does not — a pre-existing, intentional divergence from #6143 that this PR does not change.
closes#16571
## Example
Input:
```jsx
import FancyHook from 'fancy';
export default function App() {
const foo = FancyHook.property.useNestedThing();
return <h1>{foo}</h1>;
}
```
Signature before:
```js
_s(App, "useNestedThing{foo}");
```
Signature after:
```js
_s(App, "useNestedThing{foo}", false, function() {
return [FancyHook.property.useNestedThing];
});
```
---
This PR was prepared with AI assistance; I reviewed, tested, and verified the changes.
## Summary
Closes#13398.
The styled-components transform's `transpileTemplateLiterals` option rewrites tagged template literals into the array-call form:
```js
// in
styled.div`width: 100%;`
// out — transpileTemplateLiterals: true
styled.div(["width: 100%;"])
```
This form is only more compact when template literals are *also* down-levelled to ES5 — which is why `babel-plugin-styled-components` enables it by default, to pre-empt Babel's verbose `_taggedTemplateLiteral` output. Oxc does not down-level template literals below ES6, so the array-call form is strictly *larger* than leaving the tagged template untouched.
This PR flips the default to `false`, as agreed with @Dunqing in #13398.
## Changes
- `StyledComponentsOptions::transpile_template_literals` now defaults to `false` (both the `serde` field default and the `Default` impl), with docs explaining why.
- napi: `transpileTemplateLiterals` is documented `@default false` (`transformer.rs` + `index.d.ts`), and the styled-components napi test snapshot now reflects the non-transpiled default output.
- Conformance fixtures that relied on the old default and whose reference output is transpiled now pin `transpileTemplateLiterals: true`, so they keep matching `babel-plugin-styled-components`' reference output. The conformance snapshot is unchanged — `plugin-styled-components` stays at 25/40.
## Breaking change
`transpileTemplateLiterals` now defaults to `false`. Pass `transpileTemplateLiterals: true` to restore the previous behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Adds a `strictNullChecks` field to `DecoratorOptions` (default `true`)
that
controls whether `null` and `undefined` are elided from union
`design:type`
metadata. The default preserves tsc strict semantics (`T | null` →
`Object`);
setting `strictNullChecks: false` matches
babel-plugin-transform-typescript-metadata
and tsc with `--strictNullChecks=false`, emitting the underlying
primitive
(`T | null` → `String` when `T = string`).
Originally this PR unconditionally elided null/undefined. Per @Dunqing's
review, it's now opt-in so the default-strict behaviour is preserved.
## Motivation
When `strictNullChecks` is off, tsc emits the constructor of the
underlying
primitive. babel-plugin-transform-typescript-metadata always elides null
and
undefined regardless of tsconfig. Downstream consumers (NestJS Swagger,
class-validator, TypeORM, AutoMapper) treat `Object` as "no metadata"
and
silently drop fields, so users with `strictNullChecks: false` (common in
older codebases) get worse behaviour under OXC than under tsc/babel.
## Implementation
```rust
pub struct DecoratorOptions {
pub legacy: bool,
pub emit_decorator_metadata: bool,
#[serde(default = "default_as_true")]
pub strict_null_checks: bool, // default true
}
```
Threaded through `Decorator::new(options) →
LegacyDecorator::new(&options) →
LegacyDecoratorMetadata::new(&options)` (matching the established
`TypeScript::new(&TypeScriptOptions)` precedent), and gated in
`serialize_union_or_intersection_constituents`:
```rust
TSType::TSNullKeyword(_) | TSType::TSUndefinedKeyword(_)
if !is_intersection && !self.strict_null_checks => continue,
```
Mirrored at the napi binding (`napi/transform/src/transformer.rs`) so
direct
Node consumers can flip it the day this lands. Consumers that wire
tsconfig
(rolldown, bundlers) can forward `compilerOptions.strictNullChecks` to
it.
## Behaviour matrix
| Source | `strictNullChecks: true` (default) | `strictNullChecks:
false` |
|---|---|---|
| `string \| null` | `Object` (matches tsc strict, current behaviour) |
`String` (matches babel, tsc non-strict) |
| `number \| undefined` | `Object` | `Number` |
| `boolean \| null \| undefined` | `Object` | `Boolean` |
| `null \| undefined` | `void 0` | `void 0` |
| `string \| number` | `Object` | `Object` (distinct primitives) |
| `string & null` | `Object` | `Object` (intersection unchanged) |
## Test plan
- [x] 7 unit tests in `tests/integrations/decorator_metadata.rs`
covering
`T | null` under both `strictNullChecks: true` (emits `Object`, current
behaviour) and `strictNullChecks: false` (emits primitive), plus
`T | undefined`, `T | null | undefined`, the `null | undefined`
void-only
case, the `string | number` distinct-primitive case (still `Object`),
and
the intersection-with-null regression
- [x] New conformance fixture `oxc/metadata/nullable-union/` with its
own
`options.json` setting `strictNullChecks: false`
- [x] `cargo test -p oxc_transformer` passes (31 unit + 11 integration)
- [x] `cargo run -p oxc_transform_conformance` shows 0 regressions;
baseline
fixtures still pass plus the new `nullable-union`
- [x] `cargo fmt -p oxc_transformer -p oxc_transform_napi` and
`cargo clippy --tests --no-deps` clean
AI assistance was used in writing this patch and tests; the contributor
has
reviewed and tested locally.
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
## Summary
`ReadonlyArray<T>` is a TypeScript-only utility type with no runtime
value. The legacy decorator metadata emit currently wraps it in the
runtime guard `typeof (_ref = typeof X !== "undefined" && X) ===
"function" ? _ref : Object`, which always falls through to `Object`
because there is no runtime `ReadonlyArray` binding. tsc resolves this
through the checker and emits `Array`; OXC's existing handling of the
short form `readonly T[]` already emits `Array`, so the generic form is
the only divergent shape.
## The bug
```ts
class Entity {
@D() shortForm!: readonly string[]; // tsc: Array, OXC: Array (already correct)
@D() genericForm!: ReadonlyArray<string>; // tsc: Array, OXC: Object (bug)
@D() nestedReadonly!: ReadonlyArray<ReadonlyArray<number>>; // same
}
```
The two surface forms describe the same type but produce different
runtime metadata. Downstream consumers (TypeORM `@Column` array
inference, NestJS Swagger schema, AutoMapper field detection) rely on
the `Array` constructor; `Object` causes silent degradation.
## Fix
In `serialize_type_reference_node`, recognize `ReadonlyArray` by name
when the symbol is unresolved or type-only. Emit `global_array(ctx)`
directly, bypassing the wrapper. A user-shadowed `class ReadonlyArray
{}` would have a value symbol and falls through to the normal
class-handling path, so the shadow case is preserved.
```rust
if ident.name == "ReadonlyArray"
&& symbol_id.is_none_or(|sid| ctx.scoping().symbol_flags(sid).is_type())
{
return Self::global_array(ctx);
}
```
## Scope
Only `ReadonlyArray` is handled in this PR. `ReadonlyMap`,
`ReadonlySet`, `WeakMap`, `WeakSet`, and `Readonly<Array<T>>` peel are
left for a follow-up because tsc's spec emit for those is less
unambiguous and warrants case-by-case verification. The same
predicate-style approach extends naturally if reviewers want them folded
in.
## Test plan
- [x] 4 unit tests in `decorator_metadata_readonly_array.rs`: bare
ReadonlyArray, nested ReadonlyArray, regression on `readonly T[]` short
form, and a user-shadowed `class ReadonlyArray {}` regression
- [x] New conformance fixture `oxc/metadata/readonly-array/`
- [x] `cargo test -p oxc_transformer` passes (31 unit + 9 integration)
- [x] `cargo run -p oxc_transform_conformance` shows 229 OXC fixtures
still passing, +1 new fixture (`readonly-array`); 0 regressions
- [x] `cargo fmt -p oxc_transformer` and `cargo clippy -p
oxc_transformer --tests --no-deps` clean
AI assistance was used in writing this patch and tests; the contributor
has reviewed and tested locally.
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Closes#22227.
Closes#22228.
## Summary
- Switch `design:type` / `design:paramtypes` / `design:returntype` emit from `typeof X === "function" ? X : Object` to `typeof X === "undefined" ? Object : X`, matching SWC and Babel. Cross-file imported enums and namespace-imported enums (`NS.E`) now return the actual enum object instead of silently degrading to `Object` — consumers like type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator can introspect via `Object.values()`.
- Cases where the runtime binding is genuinely missing still fall back to `Object` (stale ambient declarations, erased type imports, unresolved references), so existing behavior is preserved everywhere else.
- See #14740 for the design rationale: matching SWC/Babel rather than tsc's primitive-type emit (which requires cross-file type information OXC doesn't have).
- Conformance: 5 existing fixtures updated for the new emit shape, 6 new fixtures cover the bug paths and predicate boundaries; no `Output mismatch` regressions.
## Example
Input:
```ts
import { StringEnum } from './enums';
class Source {
@dec value!: StringEnum;
}
```
Before:
```js
// design:type === Object — StringEnum is an enum object, typeof !== "function"
babelHelpers.decorateMetadata("design:type",
typeof (_ref = typeof StringEnum !== "undefined" && StringEnum) === "function"
? _ref : Object)
```
After:
```js
// design:type === StringEnum (the enum object — introspectable via Object.values)
babelHelpers.decorateMetadata("design:type",
typeof StringEnum === "undefined" ? Object : StringEnum)
```
## Note on the SWC enum-metadata debate
[swc-project/swc#11033](https://github.com/swc-project/swc/issues/11033) argues SWC's emit is wrong for cross-file enums — the runtime returns the enum object (an `{...}`) rather than `Number` / `String` / `Object` as tsc would. This PR adopts the same emit shape SWC and Babel use, so it has the same property: cross-file enum metadata is the enum binding itself, not a primitive constructor.
OXC chose this side of the debate deliberately in #14740 — returning the enum object is what type-graphql, typeorm, NestJS Swagger, AutoMapper, and class-validator depend on to introspect enum members. Matching tsc's primitive emit would require cross-file type information OXC doesn't have, and would unblock no real consumer. If TypeScript publishes a position on what non-type-checker transformers should emit, this can be revisited.
AI disclosure: drafted with Claude Code, reviewed manually.
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).
Closes#22444.
`Phase.idle as Phase`, `Phase.idle satisfies Phase`, `Phase.idle!`, and `<Phase>Phase.idle` failed to inline under `optimizeConstEnums`. The const enum declaration was stripped while the member access was left as `Phase.idle`, producing a runtime `ReferenceError`.
## Cause
The enum inliner's `enter_expression` matched only bare `StaticMemberExpression`/`ComputedMemberExpression`. `annotations.rs` strips TS-only wrappers in its own `enter_expression`, but `walk_expression` only calls `enter_expression` once per node — after the strip the inner `Phase.idle` was never revisited.
## Fix
Peek through the wrappers via `Expression::get_inner_expression_mut()` (it already handles `TSAsExpression`, `TSSatisfiesExpression`, `TSNonNullExpression`, `TSTypeAssertion`, `TSInstantiationExpression`, `ParenthesizedExpression`). The outer `*expr = …` replacement then drops the wrapper along with the inlined value.
## Example
```ts
const enum Phase { idle, active }
class Foo {
#foo = Phase.idle as Phase;
}
```
Before:
```js
class Foo {
#foo = Phase.idle; // dangling reference
}
```
After:
```js
class Foo {
#foo = 0;
}
```
## Verification
New fixtures cover all wrapper shapes for const + regular enums:
- `babel-plugin-transform-typescript/const-enum-type-cast/`
- `babel-plugin-transform-typescript/optimize-enums/type-cast/`
Previously, given:
```js
class C {
async method(x = function y() {}) {}
}
```
The async-to-generator transform generates:
```js
class C {
method(x = function y() {}) {
return babelHelpers.asyncToGenerator(function* () {})();
}
}
```
The method parameter stays on the outer wrapper method, while only the original body moves into the inner generator function.
Before this change, `BindingMover` walked the full parameter AST while moving those parameter bindings. That meant it also visited default initializer expressions, so bindings created inside the initializer, like `y` above, could be moved out of their own function/class scope and into the wrapper method scope.
After transform, rebuilt semantics would disagree with the transformed scope tree because the parameter binding was correct, but nested initializer bindings had the wrong owning scope.
This PR makes `BindingMover` only visit the binding positions for formal parameters, rest parameters, and the left side of assignment patterns. It skips initializer expressions so nested function/class bindings stay in the scopes created for those expressions.
Fixes#22246, transformer panics with `const x = a[b?.c!]?.["d"];` under es2020 target.
(I wasn't sure where to put the test, but I've confirmed that it works on my end for now.)
Given some code like
```js
const {
[({ ...rest }) => {
let { ...b } = {};
}]: a,
[({ ...d } = {})]: c,
} = {};
export {}; // necessary so we are in strict mode
```
It would previously be transformed to:
```js
const { [(_ref) => {
let rest = _extends({}, (_objectDestructuringEmpty(_ref), _ref));
let _ref2 = {}, b = _extends({}, (_objectDestructuringEmpty(_ref2), _ref2));
}]: a, [(_ref3 = {}, {} = _ref3, d = _extends({}, (_objectDestructuringEmpty(_ref3), _ref3)), _ref3)]: c } = {};
export {};
```
This is a problem because there was no declaration to `_ref3`
This causes runtime errors.
After this PR, we now emit:
This new code includes `var _ref3`, fixing the unresolved reference issue.
```js
var _ref3;
const {
[_ref => {
let rest = babelHelpers.extends({}, (babelHelpers.objectDestructuringEmpty(_ref), _ref));
let _ref2 = {},
b = babelHelpers.extends({}, (babelHelpers.objectDestructuringEmpty(_ref2), _ref2));
}]: a,
[(_ref3 = {}, {} = _ref3, d = babelHelpers.extends({}, (babelHelpers.objectDestructuringEmpty(_ref3), _ref3)), _ref3)]: c
} = {};
```
This aligns with Babel.
----
This has been fixed by walking up to find the containing statement, and inserting the var declaration before it.
Previously, the `var` declaration would only be inserted if we were inside `ExpressionStatementExpression` which was not guaranteed.
Fixes bug 7 of https://github.com/oxc-project/oxc/issues/21922.
## The bug
`@dec accessor x: T` always emitted `design:type = Object` regardless of `T`:
```ts
class Entity {
@dec accessor name: string = ""; // tsc: design:type = String
@dec accessor count: number = 0; // tsc: design:type = Number
}
```
`Reflect.getMetadata("design:type", entity, "name")` returned `Object` instead of `String`. Frameworks reading `design:type` (NestJS Swagger, class-validator, TypeORM column metadata) silently degraded for every typed accessor field.
## Cause
`lower_accessor_properties` (in `enter_class`) rewrites
```ts
@dec accessor x: T = init;
```
into three members — a private storage field, a synthesized getter, and a synthesized setter — and transfers the decorators to the getter so the existing decorator pipeline picks them up. But it dropped the type annotation: the synth getter was constructed with `return_type: NONE`.
The metadata pass runs later and takes the getter branch:
```rust
if method.kind.is_get() {
(self.serialize_type_annotation(method.value.return_type.as_ref(), ctx), None)
}
```
`return_type` is `None` because lowering discarded it, so `serialize_type_annotation` correctly fell through to `Object` for what it sees as an untyped getter.
The metadata pass's `enter_accessor_property` dispatch exists, but it never fires because the `AccessorProperty` no longer exists by the time traversal reaches the class body.
## Fix
Stop dropping the type annotation. In `lower_accessor_properties`:
```rust
let decorators = std::mem::replace(&mut accessor.decorators, ctx.ast.vec());
let type_annotation = accessor.type_annotation.take();
```
Thread `type_annotation` through `create_accessor_method` to the getter (setter passes `None`). To avoid a post-construction patch on the synthesized `MethodDefinition`, `create_class_method` now takes a `return_type: Option<ArenaBox<TSTypeAnnotation>>` parameter and threads it directly into the function's return-type slot. The other caller (`create_class_constructor_with_params`) passes `None`.
The lowered AST is now self-consistent — the synth getter for `accessor x: T` reads as a getter that returns `T`, which is what it semantically is, and the existing getter-metadata path serializes it correctly.
## Out of scope (architectural limit)
`type Uuid = string; @dec accessor x: Uuid` still emits `Object`, same as for non-accessor properties. Resolving type-alias references to their primitive constituent requires a name-based alias-resolution pass (the same one that `tsc`'s checker provides via `getTypeReferenceSerializationKind`). That's bug 1 of #21922 and a separate change from this fix; flagged here so reviewers don't mistake it for an omission.
## Test plan
- [x] Conformance fixture `oxc/metadata/accessor` covers primitives, untyped (correctly stays `Object`), `string[]` → `Array`, `static accessor`, and computed-key (`@dec accessor ["computed"]: number`)
- [x] `cargo test -p oxc_transformer` — 31 + 5 passed
- [x] `cargo run -p oxc_transform_conformance -- --filter legacy-decorators` — 73 pass / 23 fail (baseline 72/24, +1 new fixture, **0 regressions**)
- [x] `cargo run -p oxc_transform_conformance -- --exec` — accessor exec test verifies `Reflect.getMetadata` returns the correct constructor at runtime
- [x] `cargo fmt -p oxc_transformer`
## Summary
Closes#21813.
Closes#21828.
`c_num?.x` and `c_num?.['x']` were not handled by the enum-inlining pass in `enter_expression`, which only matched `Expression::StaticMemberExpression` and `Expression::ComputedMemberExpression`. When the es2020 optional-chain plugin lowered the chain first, the member access became:
```js
c_num === null || c_num === void 0 ? void 0 : c_num.x
```
Then the inline pass saw the inner `c_num.x` and replaced it with the constant + deleted *that one* reference — but the two references in the test condition stayed live. With `optimizeConstEnums: true`, the const-enum declaration is then unconditionally removed, leaving dangling references to a binding that no longer exists.
The same shape underlies the rolldown report ([rolldown/rolldown#9181](https://github.com/rolldown/rolldown/issues/9181)) where rolldown's cross-module enum inlining drops the import that `c_num` was bound to.
## Fix
Match `Expression::ChainExpression` whose top element is an optional `StaticMemberExpression` or `ComputedMemberExpression` on an enum identifier, and inline the entire chain to the constant **before** es2020 sees it. Reuses the existing `try_inline_enum_member` / `try_inline_computed_enum_member` helpers — they don't care about the `optional` flag.
## Fixtures
- `optimize-enums/optional-chain-issue-21813/` — the bug case + an `exec.ts` that runs the transformed output to verify runtime semantics
- `optimize-enums/optional-chain-value-kept/` — IIFE preserved when a bare `Foo` ref also exists
- `optimize-enums/optional-chain-deeper-access/` — `Foo?.x.toString()` / `Bar?.['y']?.toString()` exercise nested chains where the inner inline still fires via the existing arms
- `const-enum-optional-chain/` — const-enum path with `optimizeConstEnums: true`
## Known limitation
Deeper chains under es2020 lowering — `c_num?.x.y` or `c_num?.x()` where the optional access is below the chain element — still produce dangling refs because the chain element is non-optional or a `CallExpression` and this fix's match arm doesn't trigger. Without es2020 lowering they work (the inner `?.x` gets visited as a regular `Expression::StaticMemberExpression`), so the conformance fixtures pass. A more general fix would walk the chain to find the leftmost optional-on-enum access; out of scope for this PR.
## Test plan
- [x] `cargo run -p oxc_transform_conformance -- --filter optimize-enums/optional-chain` — all three pass
- [x] `cargo run -p oxc_transform_conformance -- --filter const-enum-optional-chain` — passes
- [x] `cargo run -p oxc_transform_conformance -- --exec --filter optimize-enums/optional-chain-issue-21813` — runtime check passes
- [x] `cargo test -p oxc_transformer` — green
- [x] `cargo clippy -p oxc_transformer -- -D warnings` — clean
- [x] CI Conformance — green (snapshots updated for `oxc.snap.md` + `oxc_exec.snap.md`)
Before this change, fragment transforms omitted source location
metadata:
```js
_jsxDEV(_Fragment, { children: "test" }, void 0, false);
```
After this change, fragments receive the same trailing development
metadata as other JSX `jsxDEV` calls:
```js
_jsxDEV(_Fragment, { children: "test" }, void 0, false, {
fileName: _jsxFileName,
lineNumber: 2,
columnNumber: 10
}, this);
```
## Motivation
This came from
[#21042](https://github.com/oxc-project/oxc/issues/21042).
TypeScript's `react: reactJSXDev` transform also emits fragment source
locations and `this` for same case:
https://www.typescriptlang.org/play/?jsx=5#code/DwPgUABBwCYJYDcTAPTyZa7lseV4QA
## Changes
- allow fragment `jsxDEV` calls to receive source metadata in the JSX
transformer
- update the local transform conformance fixture for
`fragment-static-children`
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
fixes#21874
`transform_static_assignment_expression` took `assign_expr.right` once at the top of the function (correct for the `=` branch), then took it a second time inside the `else` branch that handles compound operators. The second `take_in` returned a `NullLiteral` dummy, so `??=` / `&&=` / `||=` (and the binary ops) on a static private field where the receiver isn't a direct class reference — e.g. `this.#shared ??= 1` inside a static method — emitted `null` in place of the RHS:
```js
// before
class Singleton {
static #shared;
static shared() {
this.#shared ??= 1;
return this.#shared;
}
}
// transformer output (broken):
_assertClassBrand(Singleton, this, _shared)._ ?? (_shared._ = _assertClassBrand(Singleton, this, null));
// ^^^^ should be 1
```
Removes the redundant inner `let class_ident = …; let value = assign_expr.right.take_in(…);` shadowing so the compound-operator branches use the already-captured outer `value` (the real RHS) when building the `_assertClassBrand(Class, object, value)` call. Output now matches Babel.
Adds a `private-static-logical-assignment` conformance fixture covering `??=`, `&&=`, and `||=`, plus an `exec.js` to verify runtime behavior.
## Summary
Fixes a long-standing inconsistency in the TypeScript transformer's handling of constructor parameter properties under the documented default (`useDefineForClassFields: true` per [`compiler_assumptions.rs:137-139`](https://github.com/oxc-project/oxc/blob/main/crates/oxc_transformer/src/compiler_assumptions.rs#L137-L139)).
The transformer already emitted TS-true-aligned output for **declared** fields (kept `boom: number;` as `boom;`) but silently dropped the field declaration for **parameter properties**, so downstream tools that re-analyze the transformed AST saw the property only as a `this.*` write, not as a declared class field.
```ts
// input
class Foo {
boom: number; // declared field
constructor(public foo) {} // parameter property
}
```
```js
// before — inconsistent
class Foo {
boom; // ✅ kept (TS-true)
constructor(foo) { // ❌ missing `foo;` (TS-false-style for params only)
this.foo = foo;
}
}
// after — consistent
class Foo {
foo; // ✅ now matches `boom;`
boom;
constructor(foo) {
this.foo = foo;
}
}
```
This is **not a policy change** — the documented default has always been `useDefineForClassFields: true` (set both `set_public_class_fields` and `remove_class_fields_without_initializer` to `true` to opt into `false` semantics). The fix just closes the missing case for parameter properties.
## Implementation
- Add `add_parameter_property_fields` to the `enter_class` path that runs when `set_public_class_fields` is `false`, mirroring the existing branch that calls `transform_class_fields` when it is `true`.
- Skip parameter properties whose name matches an existing instance field on the same class (matches tsc's dedup). Static, private, and `declare`-only fields are different bindings, so they don't suppress the new field.
- Early-bail before allocating the dedup set when the class has no parameter properties — keeps the transformer-allocations snapshot at baseline.
No new option is added; the existing `set_public_class_fields` assumption already maps cleanly to `useDefineForClassFields` (the playground's `useDefineForClassFields: true` already toggles `set_public_class_fields = false`).
Closes#21600Closes#21831
## Summary
When a TypeScript class used a computed-key static property together with a constructor that has no parameter-property assignments, the early return in `transform_class_fields` dropped the `computed_key_assignment_static_block` that initializes the temp var, leaving it declared but never assigned.
```ts
export class SampleClass {
static [Symbol.toPrimitive] = "test"
constructor() {}
}
```
previously emitted:
```js
let _Symbol$toPrimitive;
export class SampleClass {
static {
this[_Symbol$toPrimitive] = "test";
}
constructor() {}
}
```
Now emits (matching TypeScript's own output under `useDefineForClassFields: false`):
```js
let _Symbol$toPrimitive;
export class SampleClass {
static {
_Symbol$toPrimitive = Symbol.toPrimitive;
}
static {
this[_Symbol$toPrimitive] = "test";
}
constructor() {}
}
```
## Test plan
- Added fixtures `computed-static-property-with-constructor` (exact repro) and `computed-static-property-with-constructor-super` (super() + multiple computed keys) under `babel-plugin-transform-typescript`.
- Verified output against `tsc` 5.7 with `useDefineForClassFields: false`.
Closesrolldown/rolldown#9121
When the async-to-generator transform wraps an async arrow / anonymous async function expression whose name is inferred from its parent (e.g. `const foo = async () => {}`, `{ foo: async () => {} }`), it emits a named function expression as the caller:
```js
const foo = function () {
var _ref = asyncToGenerator(function* () { /* body */ });
return function foo() { return _ref.apply(this, arguments); };
}();
```
Per the JS spec, the inferred name `foo` binds only inside the named FE itself — not in the enclosing wrapper IIFE scope.
But the previous code had two bugs that together caused oxc to emit an AST that a fresh semantic analysis (e.g. Rolldown's minifier in Vite, or oxc's own minifier) would misread:
1. `create_function` set `FunctionType::FunctionDeclaration` whenever `id.is_some()`, even for callers using the result as a function expression. Any fresh semantic pass trusts `r#type` and treats the node as a declaration — which binds the name in the enclosing wrapper scope.
2. The inferred id's binding was registered in `wrapper_scope_id` rather than the function's own scope.
Combined, downstream minifiers resolve a body reference like `typeNext()` inside `const typeNext = async () => { typeNext(); }` to the inner caller instead of the outer `const typeNext`. The outer const then looks unused, gets inlined away, and the body reference fails at runtime with `ReferenceError: typeNext is not defined`.
## Fix
Pass `FunctionType` explicitly (via new `create_function_expression` and `create_function_declaration` helpers), and place the inferred id's binding in the caller function's own scope.
## Runtime verification
Ran input → oxc transform → oxc compress (same pipeline Rolldown/Vite uses):
**Pre-fix** output (matches the exact bug in the Vite issue — outer const inlined away):
```js
(function() {
var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
return function typeNext() { return _ref.apply(this, arguments); };
})()();
```
Runtime: `ReferenceError: typeNext is not defined` ✗
**Post-fix** output (outer const preserved; minifier correctly drops the now-unused inner FE name):
```js
const typeNext = function() {
var _ref = _asyncToGenerator(function* () { ..., typeNext(); });
return function() { return _ref.apply(this, arguments); };
}();
typeNext();
```
Runtime: works correctly ✓
Closes#21125Closes#21445
## Summary
When `useDefineForClassFields: false` (`setPublicClassFields` assumption), the TypeScript transform moves public/TS-private field initializers into the constructor. However, private field initializers (including `accessor` backing fields from the legacy decorator transform) were left as class field declarations, causing them to execute **before** the constructor body. This broke execution order when the initializer depended on other fields that were already moved to the constructor.
**Input:**
```ts
class Hello {
private input = { foo };
accessor util = this.input.foo();
}
```
**Before (broken):** `this.input.foo()` runs before `this.input = { foo }`
```js
class Hello {
constructor() { this.input = { foo }; }
#_util_accessor_storage = this.input.foo(); // ← runs BEFORE constructor
}
```
**After (fixed):** all instance initializers in the constructor, in source order
```js
class Hello {
constructor() {
this.input = { foo };
this.#_util_accessor_storage = this.input.foo();
}
#_util_accessor_storage;
}
```
### Approach
Matches TypeScript's [`WillHoistInitializersToConstructor`](https://github.com/microsoft/TypeScript/blob/7b8cb3bdf82f400642b73173f941335775d6f730/src/compiler/transformers/classFields.ts#L339) logic in [`shouldTransformAutoAccessorsInCurrentClass`](https://github.com/microsoft/TypeScript/blob/7b8cb3bdf82f400642b73173f941335775d6f730/src/compiler/transformers/classFields.ts#L1069-L1073):
- Pre-scan the class body to check if any non-private instance field has an initializer that will be hoisted
- When hoisting occurs, include **all** instance fields (including `#` private) in the hoisting to preserve execution order
- When no public fields are being hoisted, private fields stay as class field declarations
No changes needed to the decorator/legacy transform — the fix is entirely in `transform_class_fields`.
Fixes#21365
## Test plan
- [x] Added test fixture: `accessor-use-define-for-class-fields` (with hoisted + non-hoisted cases)
- [x] Updated `use-define-for-class-fields-without-class-properties` expected output to match TypeScript behavior
- [x] `cargo test -p oxc_transformer` — all pass
- [x] `cargo run -p oxc_transform_conformance` — no regressions, previously failing test now passes
🤖 Generated with [Claude Code](https://claude.ai/code)
## Summary
- Add a conformance test for string enum alias members (e.g. `Default = Theme.Light`) covering both const and non-const enum cases
- Verifies no incorrect reverse mapping is generated for string alias members (relates to https://github.com/rolldown/rolldown/issues/8866)
## Test plan
- `cargo run -p oxc_transform_conformance -- --filter enum-string-alias-member` passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This is required in Rolldown to align with `esbuild`, which could inline a regular enum as long as all its enum members can be evaluated. See https://github.com/rolldown/rolldown/pull/8796
## Summary
Depends on #20508.
- Add `optimize_enums` option to `TypeScriptOptions` that treats regular enums with all-evaluable members the same as const enums: inlines member accesses and removes non-exported declarations
- Regular enums are only removed when no runtime value references remain (e.g., `typeof Foo`, passing as argument)
- Includes 14 conformance test fixtures covering basic, string values, binary expressions, cross-member/cross-enum references, merged enums, exported (kept), non-evaluable (kept), template literals, unary expressions, typeof/value-usage/passed-as-argument (kept), and re-exported (kept)
## Test plan
- [x] 14 new conformance test fixtures under `optimize-enums/`
- [x] `cargo run -p oxc_transform_conformance` passes
- [x] `cargo test -p oxc_transformer` passes
## Summary
Fixes#21116 where TypeScript constructor parameter properties emit the
wrong property name when the parameter name clashes with a free variable
referenced in a class field initializer.
## Problem
When targeting pre-ES2022 (or with `transform-class-properties`
enabled), the class properties plugin detects that a constructor
parameter name shadows an outer variable referenced in a field
initializer, and renames the parameter binding (e.g. `x` → `_x`) to
preserve the initializer's semantics.
`convert_constructor_params` (TypeScript plugin) ran after this rename
and read `id.name` — which had already been mutated — to produce the
`this.X = X` assignment. This caused:
```ts
let x = 10;
class Foo {
field = x;
constructor(public x: number) {}
}
```
to incorrectly emit:
```js
constructor(_x) {
this._x = _x; // ❌ property name should be x
...
}
```
## Root Cause
Both plugins share a single traversal pass with interleaved hooks.
`enter_class_body` (class properties) fires before
`enter_method_definition` (TypeScript), so by the time
`convert_constructor_params` runs, `BindingIdentifier.name` has already
been mutated by `ConstructorSymbolRenamer`.
## Fix
Recover the property name from `id.span.source_text(source_text)`
instead of `id.name`. Spans are set by the parser and never mutated by
any transformer, so this always returns the original user-written
identifier regardless of renames.
```ts
constructor(_x) {
this.x = _x; // ✅
...
}
```
## AI Disclosure
This fix was developed with Claude Code assistance.
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Use raw property/identifier names instead of `get_var_name_from_node` (which strips leading underscores) when generating private backing field names for accessor properties
- For non-computed keys, use `PropertyKey::name()` which preserves leading underscores
- For computed keys, match on `Expression::Identifier` to get the raw name directly
- No counters, maps, or helper functions needed — collisions are avoided at the source
This prevents `accessor prop` and `accessor _prop` from producing the same `#_prop_accessor_storage` name, which caused a runtime `TypeError: Cannot initialize the same private elements twice on an object`.
Closes#20946
Supersedes #21023
## Test plan
- [x] Added test fixture covering both non-computed (`prop`/`_prop`) and computed (`[prop]`/`[_prop]`) accessor name collisions
- [x] All existing accessor tests pass unchanged
- [x] `cargo test -p oxc_transformer` passes
- [x] Full `cargo run -p oxc_transform_conformance` passes
- [x] `cargo clippy` clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Fix JSX pragma scanner incorrectly matching `@jsxImportSource` inside backtick code spans (e.g. `` `@jsxImportSource custom/source` ``) in doc comments
- Add validation that `@` must be preceded by whitespace, `*`, or be at the start of the comment
## Problem
The `memchr`-based `@` search found pragma directives anywhere in a comment, including inside inline code spans. This caused:
1. A `@jsxImportSource` inside a doc comment overriding the actual file-level pragma
2. The trailing backtick being included in the import specifier (`` custom/source` ``)
This broke Vite 8 dev server when `.tsx` files had JSDoc comments mentioning `@jsxImportSource`.
## Design
| Tool | `@` position check |
|------|-------------------|
| esbuild | None — matches `@jsx` anywhere in comment |
| Babel | Full start-of-line regex (`^\s*(?:\*\s*)?@jsx`) |
| **Oxc (this PR)** | Preceding byte must be whitespace, `*`, or start of comment |
We intentionally sit between esbuild and Babel — checking only the immediately preceding byte is sufficient for the backtick case without being as strict as Babel's full start-of-line regex.
Fixes#20669
## Test plan
- [x] Unit tests added for backtick-wrapped pragmas
- [x] Conformance test fixture added (`issue-20669`)
- [x] All existing transformer tests pass
- [x] Clippy clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Multiple JSX spread children like `{...[1,2,3]} {...[4,5,6]}` were each wrapped in individual arrays, producing `[[...[1,2,3]], [...[4,5,6]]]` instead of `[...[1,2,3], ...[4,5,6]]`
- Changed `transform_jsx_child_automatic` to return `ArrayExpressionElement` so spread children become direct spread elements in the final children array
- Added conformance tests for multiple spreads and mixed spread/non-spread children
Closes#20600
## Test plan
- [x] Existing `spread-children-automatic` test still passes
- [x] New `spread-children-multiple-automatic` test passes (the reported bug)
- [x] New `spread-children-mixed-automatic` test passes (spread + non-spread children)
- [x] Full transformer conformance suite passes
- [x] `cargo test -p oxc_transformer` passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Fix `design:type` metadata for getter/setter decorators without type annotations emitting `void 0` instead of `Object`
- Use `serialize_type_annotation` (which defaults to `Object`) instead of `serialize_return_type_of_node` (which defaults to `void 0`) for getter `design:type`
- Verified output matches TypeScript's behavior
Closes#20368
## Test plan
- [x] Added untyped getter/setter test cases to `getter-setter-method` fixture
- [x] Verified output matches TypeScript compiler output
- [x] All conformance tests pass with no regressions
- [x] Clippy clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Closes#20133
- Lower decorated `accessor` properties to private backing field + getter/setter pair when `experimentalDecorators` is enabled, matching TypeScript's `classFields.ts` `transformAutoAccessor`
- Only accessors with decorators are lowered — undecorated accessors are left as-is
- Support computed accessor keys (e.g. `@dec accessor [expr]`) via temp var to ensure the key expression is evaluated only once
- Accessor lowering runs in `enter_class` (Decorator plugin) so es2022 class-properties can further transform the generated private fields
- Decorators are transferred from the accessor to the generated getter, so the legacy decorator transform emits `_decorate()` calls
### Transform example
```ts
// Input
class C {
@dec accessor prop: string = "hello";
@dec accessor [expr]: any;
}
```
```js
// Output (without class-properties)
var _a = expr;
class C {
#_prop_accessor_storage = "hello";
get prop() { return this.#_prop_accessor_storage; }
set prop(value) { this.#_prop_accessor_storage = value; }
#_a;
get [_a]() { return this.#_a; }
set [_a](value) { this.#_a = value; }
}
babelHelpers.decorate([dec], C.prototype, "prop", null);
babelHelpers.decorate([dec], C.prototype, _a, null);
```
## Test plan
- [x] New test `oxc/accessor` — decorated accessors without class-properties plugin (instance, static, initialized, computed key)
- [x] New test `oxc/accessor-with-class-properties` — full pipeline with class-properties plugin (private fields transformed to WeakMap/brand patterns)
- [x] New test `decoratorOnClassAccessorProperty1` — instance, static, and initialized accessor properties with decorators
- [x] Existing `decoratorOnClassProperty13` output updated for accessor lowering
- [x] Undecorated accessors are not lowered (`oxc-13284.js` coverage test passes)
- [x] `cargo clippy`, `just fmt` clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This PR has two fixes. The second fix exposed a pre-existing bug that
needed to be fixed first.
- 24a72d7bc: `create_read_write_target` was used for a simple `=`
assignment when creating a temp var for decorated computed property
keys. Should be `create_write_target`.
- f0affc6f4: `transform_class_on_exit` was removing all no-initializer
properties unconditionally. But the legacy decorator embeds an
assignment in the computed key (`[FIELD_NAME]` → `[_a =
FIELD_NAME]`), so removing the property also removes the assignment,
leaving `_a` undefined in `_decorate()`. Keep properties whose computed
keys have side effects, matching TypeScript's
behavior.
Fixes#20418
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
related https://github.com/rolldown/rolldown/discussions/8684
Fix `design:paramtypes` being silently dropped in release builds when a
decorated class has static properties with anonymous class expression
initializers (`static Foo = class extends Error
{}`).
`pop_constructor_metadata()` was inside `debug_assert!`, which is a
no-op in release builds, causing stack imbalance and missing metadata.
---------
Co-authored-by: Dunqing <dengqing0821@gmail.com>
Closes#20120
`rewriteImportExtensions` was not handling dynamic `import()`
expressions, only static `import`/`export` declarations. TypeScript's
`rewriteRelativeImportExtensions` handles both (via
`visitImportOrRequireCall` in the ESNext module transformer).
The fix adds `enter_import_expression` visitors at three levels of the
transformer dispatch chain. When the `import()` source is a string
literal, it is rewritten the same way as static imports. Non-literal
sources (e.g., `import(variable)`) are left unchanged.
Test fixtures added for both `"rewrite"` and `"remove"` modes covering
dynamic imports.
> Disclosure: AI-assisted (Claude). All changes reviewed and tested by
the contributor.
---------
Co-authored-by: Sverre Johansen <sverre.johansen@microsoft.com>
## Summary
- Skip the trailing reference (e.g. `_c`) in the generated sequence expression when the destructuring assignment is directly inside an `ExpressionStatement`, since the expression result is not consumed
- Before: `_c = c2, {a2} = _c, b2 = objectWithoutProperties(_c, ["a2"]), _c;`
- After: `_c = c2, {a2} = _c, b2 = objectWithoutProperties(_c, ["a2"]);`
- The trailing reference is still kept when the result is consumed, e.g. `console.log((..., _c))`
Closes#7389 (issue 1)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- Adds a test case covering the interaction between `transform-class-properties` and `transform-async-to-generator` when `super` is accessed in an async arrow function within a static class property
- Verifies that `super` references are correctly hoisted before async transformation
## Test plan
- [x] Test passes with `cargo run -p oxc_transform_conformance -- --filter static-async-super`
- [x] Conformance tests pass with `just conformance`
Closes#8326🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implement a transformation plugin for tagged template expressions containing </script> tags to prevent script tag issues in browser environments.
This plugin transforms tagged template literals containing `</script` (case-insensitive) to use a helper function call with template caching, matching esbuild's behavior.
### Example
Transforms tagged templates to use cached template objects with a helper function:
Input:
```js
foo`</script>`
bar`<script>${content}</script>`
```
Output:
```js
var _templateObject, _templateObject2;
foo(_templateObject || (_templateObject = babelHelpers.taggedTemplateLiteral(["<\/script>"])));
bar(_templateObject2 || (_templateObject2 = babelHelpers.taggedTemplateLiteral(["<script>", "<\/script>"])), content);
```
Closes#15306
- [x] Explore repository structure and understand the issue
- [x] Identify the bug in enum.rs (lines 430-434) - missing
number-to-string conversion in template literals
- [x] Create test case for enum template literal with numbers
- [x] Fix the bug by converting numbers to strings in template literal
evaluation
- [x] Verify the fix with manual testing (regular numbers, mixed types,
negative numbers, floats, Infinity, NaN)
- [x] Run unit tests - all passing
- [x] Run code review - reviewed, minor comment about exhaustiveness
(already handled)
- [x] Run security check - no issues found
- [x] Update transform conformance snapshots per CI feedback
- [x] Fix template literal quasi bug (use enumerate instead of zip)
- [x] Add test case for trailing quasi edge case
- [x] Task completed
## Summary
Fixed a bug where numeric enum values in TypeScript were not converted
to strings when used in template literals. The fix converts
`ConstantValue::Number` to strings using `to_js_string()` method.
Fixed an additional bug where the last quasi (suffix) in template
literals was being dropped due to using `zip` instead of properly
iterating over all quasis with conditional expression access.
Updated the transform conformance snapshots to include both test cases.
## Security Summary
No security vulnerabilities were found in the changes.
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>transformer: computed enum with number in string template
is incorrect</issue_title>
>
<issue_description>[playground](https://playground.oxc.rs/#eNptVMFOGzEQ/ZXI6gGkgELggFL1UAGRKgFJC5RLpOLdnQSD197aswlotf/eZyfeTQWneN945s28eU4jcjER9FZZxwMydTm4rcuM3FU4NgszGNw+3Pw5GXwbnIxGo2ECxgDG+8ApgNN94AzA2RZoF2Zh9ikubFnVTEVPcjG7mT/cX11GpqcvTd/EceRvjz5g4/YpknWpoaePqaefpJ7F1NhWbo23mo61XR3MshfK+XgtdU3+YL/Jw8OvCyOGwopJI1xtwo9WhsVkKbWnoVhaV0pmch3CThofYDFhVwNQ4JGod0m5lk6yAnN3e/OsmHwlc+qgUpqV7j9ztOPI9yk+txXC2+L+vcysTl/5crW71g5FJZ0PfTXYMpPx4MXG2b9hHqm13fwirp2Z1exVQdPa5KG1VCpwklvTXDrkJjTm/T7/YdgpVMy7gCe0zSq/cs66HYoeglaxB5z3pGpE7eleZv1QLLNHVfCzmIyHgkwxW14rgymFXqLdCmy8i5+PwKaCRD9ry71OL/7t7hP4b/iaO1uBS0h/ZIgKKlATe1Job4V1lzLEtAaKOVQ3LGbZpPkR38h3jyuZk/kr8R2WhvR0OaGypG3nO34bvfXoZIUaSdRAFHv9zhAyg9vm5P5Lw6shp0oyLPUMR8nWza1X2xUFhQQk7by2FZWlWxG8Kcjj2TFYIPMlLVF4at2Flt5PFemi29o+C+xpI00fLRV36A2xLCRDqd1qP7d1IzwmqmAQckbCmMmNuS1oRfEBmWCEzrMvvrD4K9pJbozlWCshmlbpbqySHkNwVHopmNxW17SmrugrUXWLVSSDxVSY1uopDBwy1uQy65PgYR4TFhVC3akFXET5Atyd2oAHmdNM7T8OrNw+)
> Original report: https://github.com/vitejs/rolldown-vite/issues/472
>
> ### Input
> ```ts
> export enum NumberEnum {
> NUM_1 = 1000,
> NUM_2 = 2000,
> NUM_3 = 3000,
> NUM_4 = 4000,
> }
>
> export enum ComputedEnum {
> COMPUTED_1 = `${NumberEnum.NUM_1}-${NumberEnum.NUM_2}`,
> COMPUTED_2 = `${NumberEnum.NUM_3}-${NumberEnum.NUM_4}`,
> }
>
> console.log(Object.values(ComputedEnum));
> ```
> ### Actual
> ```js
> export let NumberEnum = /* @__PURE__ */ function(NumberEnum) {
> NumberEnum[NumberEnum["NUM_1"] = 1e3] = "NUM_1";
> NumberEnum[NumberEnum["NUM_2"] = 2e3] = "NUM_2";
> NumberEnum[NumberEnum["NUM_3"] = 3e3] = "NUM_3";
> NumberEnum[NumberEnum["NUM_4"] = 4e3] = "NUM_4";
> return NumberEnum;
> }({});
> export let ComputedEnum = /* @__PURE__ */ function(ComputedEnum) {
> ComputedEnum["COMPUTED_1"] = "-";
> ComputedEnum["COMPUTED_2"] = "-";
> return ComputedEnum;
> }({});
> console.log(Object.values(ComputedEnum));
> ```
>
> ### Expected
> ```js
> export let NumberEnum = /* @__PURE__ */ function(NumberEnum) {
> NumberEnum[NumberEnum["NUM_1"] = 1e3] = "NUM_1";
> NumberEnum[NumberEnum["NUM_2"] = 2e3] = "NUM_2";
> NumberEnum[NumberEnum["NUM_3"] = 3e3] = "NUM_3";
> NumberEnum[NumberEnum["NUM_4"] = 4e3] = "NUM_4";
> return NumberEnum;
> }({});
> export let ComputedEnum = /* @__PURE__ */ function(ComputedEnum) {
> ComputedEnum["COMPUTED_1"] = "1000-2000";
> ComputedEnum["COMPUTED_2"] = "3000-4000";
> return ComputedEnum;
> }({});
> console.log(Object.values(ComputedEnum));
> ```
> </issue_description>
>
> <agent_instructions>We should take a look at
./crates/oxc_transformer/src/typescript/enum.rs to check if there is a
bug</agent_instructions>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> </comments>
>
</details>
- Fixesoxc-project/oxc#15182
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Dunqing <29533304+Dunqing@users.noreply.github.com>
* Fixes: #14493
Getter and setter methods should not have `design:returntype` metadata according to TypeScript's `emitDecoratorMetadata` behavior. They should only have `design:type` and `design:paramtypes`.
**And `design:type` is different for the getter and setter:**
Getter: `design:type` is the same as the return type of method.
Setter: `design:type` is the same as the type of the first parameter of the method
* Fixes#13733
Fixes the issue where `declare` fields were incorrectly transformed instead of being removed, causing inheritance issues where child `declare` fields would interfere with parent class property initialization.
Only in the `enter_*` phase do we have to skip the `declare` field; in the exit phase, the `declare` fields have already been removed in the `TypeScript` plugin.
Fixes https://github.com/rolldown/rolldown/issues/6079. Regressed introduced by https://github.com/oxc-project/oxc/pull/13227.
The root cause is that the class plugin will insert a constructor method in some cases, but metadata handling is processed before the constructor is inserted, which causes the method count not to be exactly the same as the method metadata count. Thus, decorator metadata is inserted in the incorrect place.
This PR refactors the core transformation logic to ensure that the new methods and properties inserted by other plugins won't affect the decorator transformation.
### Transformation Flow Changes
Before: Complex upfront analysis with separate processing paths
- Used check_class_has_decorated() to analyze entire class tree
- Separate handling for class vs element decorators
- Multiple disconnected stacks for state tracking
After: Unified incremental processing
- Process decorators as we traverse (exit_method_definition,
exit_property_definition, etc.)
- Accumulate decoration statements in single stack during traversal
- Unified processing in transform_class() using consolidated state