Skip to content

transpiler: key decorator flavor on experimentalDecorators only - #36142

Closed
robobun wants to merge 6 commits into
mainfrom
farm/2f80fee5/fix-decorator-flavor-keyed-on-metadata
Closed

robobun wants to merge 6 commits into
mainfrom
farm/2f80fee5/fix-decorator-flavor-keyed-on-metadata

Conversation

@robobun

@robobun robobun commented Jul 27, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

const src = "function d(...a){} class C { @d m() {} }";
const flavor = (o: string) =>
  /__legacyDecorateClassTS/.test(o) ? "LEGACY" : /__decorateElement/.test(o) ? "std" : "?";
for (const [k, c] of Object.entries({
  "no flags": {},
  "experimentalDecorators:true": { experimentalDecorators: true },
  "emitDecoratorMetadata:true ONLY": { emitDecoratorMetadata: true },
  "experimentalDecorators:false + emitDecoratorMetadata:true": { experimentalDecorators: false, emitDecoratorMetadata: true },
}))
  console.log(k, "->", flavor(new Bun.Transpiler({ loader: "ts", tsconfig: JSON.stringify({ compilerOptions: c }) }).transformSync(src)));

Before:

no flags -> std
experimentalDecorators:true -> LEGACY
emitDecoratorMetadata:true ONLY -> LEGACY
experimentalDecorators:false + emitDecoratorMetadata:true -> LEGACY

After (matches tsc):

no flags -> std
experimentalDecorators:true -> LEGACY
emitDecoratorMetadata:true ONLY -> std
experimentalDecorators:false + emitDecoratorMetadata:true -> std

At runtime, with tsconfig { experimentalDecorators: false, emitDecoratorMetadata: true } a method decorator was being called as (target, key, descriptor) (legacy) instead of (value, context) (standard), so stage-3 decorators silently received the wrong arguments with no diagnostic.

Cause

src/bundler/transpiler.rs and src/bundler/ParseTask.rs computed standard_decorators = !loader.is_typescript() || !(experimental_decorators || emit_decorator_metadata), originally added in #27527 as a heuristic for NestJS projects that set emitDecoratorMetadata without experimentalDecorators. tsc does not do this: emitDecoratorMetadata is a no-op unless experimentalDecorators is also set (TS5052), and standard decorators are emitted.

Fix

Key standard_decorators on experimentalDecorators alone, and gate emit_decorator_metadata on it as well so the metadata flag remains a no-op in the standard path.

This unmasked a pre-existing gap in the tsconfig extends merge: resolver.rs OR-merged emit_decorator_metadata across the chain but never merged experimental_decorators, so a child tsconfig setting experimentalDecorators: true over a base that does not was dropped. That was previously hidden by the || emit_decorator_metadata fallback. The merge now copies experimental_decorators alongside emit_decorator_metadata.

The #27526 regression test is updated to set experimentalDecorators: true explicitly, which is what tsc (and the NestJS docs) require. Projects that relied on the old heuristic will need to add that flag to their tsconfig.

Verification

  • test/bundler/transpiler/es-decorators.test.ts: bun run and Bun.Transpiler for { emitDecoratorMetadata: true } alone and { experimentalDecorators: false, emitDecoratorMetadata: true }; a guard that both flags together still emit __legacyDecorateClassTS with design:* metadata; and two extends-chain tests (flag in child, flag in base) pinning legacy semantics.
  • test/bundler/bundler_decorator_metadata.test.ts: two itBundled cases covering the Bun.build path for the same tsconfigs.

Six of the nine new cases fail on main.


[review] gate passed · iteration 0 · 6 files touched

fails on main (without fix)
ASAN without fix: 6 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_decorator_metadata.test.ts test/bundler/transpiler/es-decorators.test.ts "test/regression/issue/27526.test.ts"
bun test v1.4.0 (ac4df3440)

test/bundler/bundler_decorator_metadata.test.ts:
(pass) bundler > decorator_metadata/TypeSerialization [2545.91ms]
(pass) bundler > decorator_metadata/ImportIdentifiers [1275.87ms]
1280 |           "/tsconfig.json": JSON.stringify({ compilerOptions }),
1281 |         },
1282 |         bundling: true,
1283 |         run: { stdout: `[2,"method","function"]\n` },
1284 |         onAfterBundle(api) {
1285 |           api.expectFile("/out.js").not.toContain("__legacyDecorateClassTS");
                                               ^
error: expect(received).not.toContain(expected)

Expected to not contain: "__legacyDecorateClassTS"
Received: "var __legacyDecorateClassTS = function(decorators, target, key, desc) {\n  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n  if (typeof Reflect === \"object\" && typeo
... (truncated)

release without fix: 15 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/bundler/bundler_decorator_metadata.test.ts:
(pass) bundler > decorator_metadata/TypeSerialization [45.07ms]
(pass) bundler > decorator_metadata/ImportIdentifiers [45.60ms]
1280 |           "/tsconfig.json": JSON.stringify({ compilerOptions }),
1281 |         },
1282 |         bundling: true,
1283 |         run: { stdout: `[2,"method","function"]\n` },
1284 |         onAfterBundle(api) {
1285 |           api.expectFile("/out.js").not.toContain("__legacyDecorateClassTS");
                                               ^
error: expect(received).not.toContain(expected)

Expected to not contain: "__legacyDecorateClassTS"
Received: "var __legacyDecorateClassTS = function(decorators, target, key, desc) {\n  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n  if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\")\n    r = Reflect.decorate(decorators, target, key, desc);\n  else\n    for (var i = decorators.length - 1;i >= 0; i--)\n      if (d = decorators[i])\n        r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) |
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_decorator_metadata.test.ts test/bundler/transpiler/es-decorators.test.ts "test/regression/issue/27526.test.ts"
bun test v1.4.0 (ac4df3440)

test/bundler/bundler_decorator_metadata.test.ts:
(pass) bundler > decorator_metadata/TypeSerialization [2450.29ms]
(pass) bundler > decorator_metadata/ImportIdentifiers [1259.73ms]
(pass) bundler > decorator_metadata/StandardDecoratorsWhenExperimentalDecoratorsUnset [516.30ms]
(pass) bundler > decorator_metadata/StandardDecoratorsWhenExperimentalDecoratorsUnsetExplicitFalse [560.84ms]

test/regression/issue/27526.test.ts:
(pass) legacy decorators work when experimentalDecorators and emitDecoratorMetadata are set [386.35ms]
(pass) TC39 standard decorators work when neither emitDecoratorMetadata nor experimentalDecorators is set [377.65ms]

test/bundler/transpiler/es-decorators.test.ts:
(pass) ES Decorators > class decorators > basic class decorator [383.46ms]
(pass) ES Decorators > class decorators > class decorator receives correct context [448.33ms]
(pass) ES Decorators > clas
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 804ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output)
�[1m�[92m   Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap)
�[1m�[92m   Compiling�[0m bun_valkey v0
... (truncated)
diff hotspot
src/bundler/ParseTask.rs                        | 11 ++--
 src/bundler/transpiler.rs                       | 11 ++--
 src/resolver/resolver.rs                        |  2 +
 test/bundler/bundler_decorator_metadata.test.ts | 27 ++++++++
 test/bundler/transpiler/es-decorators.test.ts   | 85 +++++++++++++++++++++++++
 test/regression/issue/27526.test.ts             |  7 +-
 6 files changed, 127 insertions(+), 16 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                             reads  edits  tests
src/bundler/ParseTask.rs                             2      3      0
src/bundler/transpiler.rs                            2      3      0
src/resolver/resolver.rs                             3      1      0
test/bundler/bundler_decorator_metadata.test.ts      1      2      0
test/bundler/transpiler/es-decorators.test.ts        3      3      0
test/regression/issue/27526.test.ts                  2      2      0

emitDecoratorMetadata alone (or with experimentalDecorators:false) was
selecting the legacy decorator lowering. tsc treats emitDecoratorMetadata
as a no-op without experimentalDecorators and emits standard decorators,
so a stage-3 decorator would silently receive the legacy
(target, key, descriptor) calling convention under bun.

standard_decorators is now keyed on experimentalDecorators alone, and
emit_decorator_metadata is gated on it. Updates the #27526 regression
test to set experimentalDecorators explicitly, matching what tsc and
NestJS require.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The parser and transpiler now enable legacy decorator metadata only when both decorator options are set. Tests cover standard decorators without experimental mode and legacy decorators with metadata enabled.

Decorator mode selection

Layer / File(s) Summary
Align decorator feature flags
src/bundler/ParseTask.rs, src/bundler/transpiler.rs
Decorator metadata requires experimental decorators, while standard decorator selection no longer depends on metadata emission.
Validate decorator modes
test/bundler/transpiler/es-decorators.test.ts, test/regression/issue/27526.test.ts
Tests verify standard decorator lowering without experimental decorators and legacy metadata lowering when both options are enabled.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: decorator flavor now depends on experimentalDecorators.
Description check ✅ Passed The description clearly explains the bug, fix, and verification, and it covers the required behavior despite using custom headings.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 27, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:10 PM PT - Jul 27th, 2026

❌ @autofix-ci[bot], your commit ac4df34 has some failures in Build #83601 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 36142

That installs a local version of the PR into your bun-36142 executable, so you can run:

bun-36142 --bun

Comment thread src/bundler/ParseTask.rs Outdated
Comment thread src/bundler/transpiler.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/regression/issue/27526.test.ts`:
- Around line 4-6: Replace the explanatory comment in the regression test with
only the issue URL comment for Bun issue 27526, preserving the surrounding test
code unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a522b8a6-3df6-4674-a6bf-23c3386b1371

📥 Commits

Reviewing files that changed from the base of the PR and between 4eb6f99 and b7e4f7f.

📒 Files selected for processing (4)
  • src/bundler/ParseTask.rs
  • src/bundler/transpiler.rs
  • test/bundler/transpiler/es-decorators.test.ts
  • test/regression/issue/27526.test.ts

Comment thread test/regression/issue/27526.test.ts Outdated
Comment thread src/bundler/transpiler.rs Outdated
Comment thread src/bundler/ParseTask.rs Outdated
robobun and others added 2 commits July 27, 2026 21:06
… bundler path

The extends merge loop OR-merged emit_decorator_metadata but not
experimental_decorators, so a child tsconfig setting
experimentalDecorators: true over a base that does not was dropped.
Previously masked by the emit_decorator_metadata fallback in the
standard_decorators computation.

Also adds itBundled coverage for the ParseTask.rs path.
Comment thread src/resolver/resolver.rs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two earlier findings (resolver extends merge and bundler-path coverage) are addressed and the remaining OR-merge limitation is a pre-existing pattern acknowledged as follow-up. I'm deferring rather than approving because this deliberately reverses the #27527 heuristic — projects with emitDecoratorMetadata: true and no experimentalDecorators flip from legacy to standard decorators, and the #27526 regression test's contract is changed to require the flag. That's a defensible tsc-alignment call, but it's a user-facing breaking change a maintainer should sign off on.

What was reviewed:

  • standard_decorators/emit_decorator_metadata gating in both transpiler.rs and ParseTask.rs — kept in lockstep, both now covered by tests.
  • resolver.rs extends merge — experimental_decorators now OR-merged alongside emit_decorator_metadata; child-overrides-base=false limitation is pre-existing and unchanged.
  • Confirmed no other src/ sites derive standard_decorators from these flags.
Extended reasoning...

Overview

Changes the decorator-flavor selection in the runtime transpiler (src/bundler/transpiler.rs) and the bundler parse worker (src/bundler/ParseTask.rs) so that legacy TypeScript decorators are selected only when experimentalDecorators is set, and emitDecoratorMetadata is gated on the same flag (matching tsc's TS5052 semantics). Adds an OR-merge for experimental_decorators in the tsconfig extends loop in src/resolver/resolver.rs. Nine new tests across bun run, Bun.Transpiler, and Bun.build pin both directions plus the extends chain; the #27526 regression test is updated to set experimentalDecorators: true explicitly.

Security risks

None. Pure transpiler-option plumbing; no untrusted input parsing, auth, or crypto paths touched.

Level of scrutiny

Medium-high. The code change is small and mechanically correct against tsc semantics, and both prior review findings (missing experimental_decorators merge in the extends loop; missing itBundled coverage for ParseTask.rs) were fixed in f7c90da. The remaining 🟡 note — child experimentalDecorators: false cannot override a base's true because the field is a plain bool — is a pre-existing limitation shared with the adjacent emit_decorator_metadata merge and reasonably deferred.

Other factors

The reason to defer is not correctness but the product decision: the original || emit_decorator_metadata heuristic was added in #27527 specifically so NestJS-style projects that set only emitDecoratorMetadata would get legacy semantics. This PR reverses that and rewrites the #27526 regression test to require the flag, with the PR body noting affected projects "will need to add that flag to their tsconfig." REVIEW.md flags both "never silently weaken an existing test" and "never change a Bun-native default" as things a maintainer should explicitly agree to. The change is well-argued (tsc alignment, NestJS docs already require both flags), but whether to accept the ecosystem breakage is a maintainer call, not a bot call.

@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

CI build #83601: the gate (robobun/evidence) passed. All seven test failures are tagged [flaky] (also failing on main) and none touch the transpiler, bundler, resolver, or tsconfig paths this PR changes:

  • test/cli/install/bun-install-registry.test.ts (windows aarch64, hoisting)
  • test/js/bun/terminal/terminal-platform-gaps.test.ts (windows x64, ConPTY output)
  • test/js/node/test/parallel/test-http-server-connections-checking-leak.js (alpine x64)
  • test/cli/install/bun-add.test.ts (darwin x64, git ssh publickey)
  • test/cli/run/no-orphans.test.ts (darwin, perl daemon timeout)
  • test/js/bun/http/serve.test.ts (darwin x64)
  • test/regression/issue/20144/20144.test.ts (darwin, SIGINT vs SIGKILL)

The decorator tests added in this PR (es-decorators.test.ts, bundler_decorator_metadata.test.ts, 27526.test.ts) passed on every lane. Ready for review.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-27, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants