Skip to content

Derive decorator options from tsconfig in scanImports and the REPL - #33384

Open
robobun wants to merge 2 commits into
mainfrom
farm/57746eca/scanimports-standard-decorators
Open

Derive decorator options from tsconfig in scanImports and the REPL#33384
robobun wants to merge 2 commits into
mainfrom
farm/57746eca/scanimports-standard-decorators

Conversation

@robobun

@robobun robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Repro

const t = new Bun.Transpiler({ loader: "ts" });

t.transformSync(`class C { accessor x = 1 }`);                 // fine
t.scanImports(`class C { accessor x = 1 }\nimport "./a"`);     // AggregateError: Failed to scan imports
                                                               //   Expected ";" but found "x"

It is wider than accessor. With the js/jsx loaders, scanImports also rejects plain standard decorators:

new Bun.Transpiler({ loader: "js" }).scanImports(`@dec class C {}\nimport "./a"`);
// AggregateError: Failed to scan imports — Unexpected @

And for TypeScript it silently used the legacy decorator grammar even with no experimentalDecorators, so @dec()() class C {} scanned fine but failed to transpile.

bun repl has the same bug from the same cause: class C { accessor x = 1 } fails the internal parse, falls back to raw evaluation, and JavaScriptCore reports SyntaxError: Unexpected identifier 'x'.

Cause

JSTranspiler::scan_imports and the REPL's transform_for_repl both build their parser options with ParserOptions::init(jsx, loader) and never touch features. standard_decorators defaults to false, and the parser gates the accessor keyword and JS-side @decorators on it (parse_property.rs, parse_stmt.rs, parse_typescript.rs, parse_prefix.rs).

transformSync / scan go through Transpiler::parse, which derives the flag from the tsconfig. The fast import scanner never did, so it accepted and rejected a different language than the transpiler it fronts.

Fix

Add RuntimeFeatures::standard_decorators_for(loader, experimental_decorators, emit_decorator_metadata) as the single source of truth for which decorator dialect to parse, and call it from every site that builds parser options:

  • src/bundler/transpiler.rs, src/bundler/ParseTask.rs — the two existing copies of the expression, unchanged behavior.
  • src/runtime/api/JSTranspiler.rsscanImports now reads the same config.tsconfig that get_parse_result does.
  • src/runtime/cli/repl.rs — the REPL reads vm.transpiler.options, and also wires up emit_decorator_metadata, which it was dropping.

scanImports now accepts exactly what transformSync accepts, and the tsconfig still selects the dialect: under experimentalDecorators the legacy grammar stays in force.

Verification

test/bundler/transpiler/transpiler.test.jsaccessor fields, decorators on classes, and decorators on class members across the ts/tsx/js/jsx loaders, each asserted against scan() as well. Plus @dec()() (legal in the legacy grammar, illegal in the standard one) to pin down which dialect is selected with and without experimentalDecorators.

test/js/bun/repl/repl.test.tsaccessor evaluates in the REPL, and a tsconfig with experimentalDecorators + emitDecoratorMetadata still gets the legacy lowering with design-time metadata.

11 new transpiler tests and 2 new REPL tests fail on main and pass with this change.

Related

#29201 proposes removing the standard_decorators gate on accessor entirely (esbuild and tsc accept the keyword under experimentalDecorators). That is a separate parser-level fix; this PR is about the option plumbing, and the two do not overlap in the files they touch. The tests here deliberately use @dec()() rather than accessor to pin the decorator dialect, so they stay correct if #29201 lands.

… the REPL

scanImports() and the REPL built their parser options from scratch and left
features.standard_decorators at its false default, so both rejected `accessor`
fields and (outside TypeScript) standard decorators that transformSync() and
scan() accept. For TypeScript they also parsed the legacy decorator grammar
even when the tsconfig asked for the standard one.

Add RuntimeFeatures::standard_decorators_for() as the single source of truth
for the decorator dialect and use it at every option-building site.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 72aa5144-bbb8-4fbb-893d-8f870bfe4f05

📥 Commits

Reviewing files that changed from the base of the PR and between fb50cce and f9ce1f1.

📒 Files selected for processing (7)
  • src/bundler/ParseTask.rs
  • src/bundler/transpiler.rs
  • src/js_parser/parser.rs
  • src/runtime/api/JSTranspiler.rs
  • src/runtime/cli/repl.rs
  • test/bundler/transpiler/transpiler.test.js
  • test/js/bun/repl/repl.test.ts

Walkthrough

A new helper function standard_decorators_for centralizes the logic determining whether TC39 standard decorator parsing should be enabled based on loader type and decorator-related flags. This helper is applied consistently in the bundler's ParseTask, Transpiler, JSTranspiler's scanImports fast path, and REPL transpilation, replacing previously inconsistent inline logic. Tests are added covering scanImports and REPL decorator behavior.

Changes

Standard decorators consistency

Layer / File(s) Summary
Add shared decorator predicate
src/js_parser/parser.rs
New Runtime::Features::standard_decorators_for(loader, experimental_decorators, emit_decorator_metadata) returns whether standard decorator parsing should be enabled.
Apply helper in bundler parse paths
src/bundler/ParseTask.rs, src/bundler/transpiler.rs
Replaces inline boolean logic for opts.features.standard_decorators with calls to the new helper.
Apply helper in scanImports fast path
src/runtime/api/JSTranspiler.rs
scan_imports now derives tsconfig flags and calls the helper so fast-scan decorator grammar matches full parsing.
Apply helper in REPL parsing
src/runtime/cli/repl.rs
transform_for_repl now forwards emit_decorator_metadata and computes standard_decorators via the helper using the Tsx loader.
Test coverage
test/bundler/transpiler/transpiler.test.js, test/js/bun/repl/repl.test.ts
Adds scanImports tests across loaders for auto-accessors and standard/legacy decorators, plus REPL tests for auto-accessor lowering and decorator metadata output, and extends runRepl to accept a cwd option.

Sequence Diagram(s)

sequenceDiagram
  participant ParseEntryPoint as ParseTask/Transpiler/scanImports/REPL
  participant RuntimeFeatures as Runtime::Features
  ParseEntryPoint->>RuntimeFeatures: standard_decorators_for(loader, experimental_decorators, emit_decorator_metadata)
  RuntimeFeatures-->>ParseEntryPoint: enabled: bool
  ParseEntryPoint->>ParseEntryPoint: set opts.features.standard_decorators = enabled
Loading

Compact Metadata
Type: Refactor / consistency fix
Estimated review effort: Medium
Related issues: None provided
Related PRs: None provided
Suggested labels: parser, decorators, bug-fix, tests
Suggested reviewers: None provided

Poem
A rabbit hopped through parser trees,
Found decorators scattered, ill at ease.
One helper now to rule them all,
ParseTask, REPL, transpiler — heed the call.
Standard or legacy, the checks align,
Consistent grammar, one clean line. 🐇✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the main change: deriving decorator options from tsconfig for scanImports and the REPL.
Description check ✅ Passed The PR description covers the change and verification steps, though it uses different headings than the template.
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.

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

@github-actions github-actions Bot added the claude label Jul 5, 2026
@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:31 PM PT - Jul 5th, 2026

@robobun, your commit f9ce1f1e2ca4964d39033914ad57a9ad8c96bfc2 passed in Build #68611! 🎉


🧪   To try this PR locally:

bunx bun-pr 33384

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

bun-33384 --bun

@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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.

LGTM — clean extraction of the decorator-dialect predicate into one helper, wired into the two call sites that were missing it, with thorough test coverage.

Extended reasoning...

Overview

This PR fixes Bun.Transpiler#scanImports and bun repl rejecting standard decorators and accessor fields that transformSync accepts. Root cause: both build ParserOptions directly and never set features.standard_decorators (defaults to false), while the full transpile path derives it from tsconfig. The fix extracts the existing inline predicate into RuntimeFeatures::standard_decorators_for(loader, experimental_decorators, emit_decorator_metadata) and calls it from all four sites: the two existing bundler paths (behavior-preserving refactor — the function body is byte-identical to the inline expression it replaces) plus JSTranspiler::scan_imports and Repl::transform_for_repl (the actual bug fixes). The REPL also now propagates emit_decorator_metadata, which it was previously dropping.

Security risks

None. This is pure parser-option plumbing — a boolean derived from already-loaded tsconfig/CLI state and assigned to a features struct. No new I/O, no untrusted input parsing, no unsafe blocks, no memory management changes.

Level of scrutiny

Low-to-moderate. The refactor portion is mechanically verifiable (same expression, moved). The fix portion adds one field assignment at each of two well-understood option-construction sites, reading from the same config sources the sibling code paths (get_parse_result, vm.transpiler.options) already use. I confirmed js_ast in transpiler.rs aliases bun_js_parser so the helper path resolves, and that vm.transpiler.options.{experimental_decorators,emit_decorator_metadata} exist on BundleOptions.

Other factors

  • 11 new transpiler tests (describe.each over all four loaders, plus @dec()() dialect-discriminator tests both with and without experimentalDecorators, each cross-checked against scan()) and 2 new REPL tests (accessor lowering; tsconfig-driven legacy metadata). Tests assert on evaluation output rather than echoed input, and the PR description states they fail on main.
  • No CODEOWNER-covered files touched.
  • No outstanding human review comments; only bot noise (CodeRabbit rate-limit notice, robobun build link).
  • The bug-hunting system found nothing.
  • The PR description explicitly addresses interaction with #29201 and designed the dialect tests to remain valid if that lands.

@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.

LGTM — clean option-plumbing fix that extracts the duplicated standard_decorators expression into one helper and wires it into the two sites that were missing it.

Extended reasoning...

Overview

This PR fixes Bun.Transpiler#scanImports and bun repl rejecting valid standard-decorator / accessor syntax that transformSync accepts. It adds RuntimeFeatures::standard_decorators_for(loader, experimental, emit_metadata) in src/js_parser/parser.rs as the single source of truth for the boolean, then calls it from four sites: two existing (bundler/transpiler.rs, bundler/ParseTask.rs — pure refactors of the identical inline expression) and two new (JSTranspiler::scan_imports, Repl::transform_for_repl). The REPL also picks up emit_decorator_metadata, which it was previously dropping. Tests are added for all four loaders in transpiler.test.js (cross-checking scanImports against scan) and two REPL tests covering both the accessor-lowering and the tsconfig-selects-legacy-grammar paths.

Security risks

None. The change only threads existing tsconfig-derived booleans into parser feature flags; no new I/O, no untrusted-input parsing, no auth/crypto/permissions surface.

Level of scrutiny

Low-to-medium. The two bundler-side edits are provably behavior-preserving (same expression, extracted to a function). The two new call sites only affect which decorator grammar the fast-path import scanner and the REPL parser accept — developer-facing surfaces with no correctness impact on emitted code beyond aligning with what transformSync already does. The helper's placement on Runtime::Features and its re-export via RuntimeFeatures follow the file's existing conventions; the js_ast:: vs js_parser:: path difference across call sites resolves to the same type via the crate's alias (use bun_js_parser as js_ast in transpiler.rs).

Other factors

Test coverage is thorough: describe.each over ts/tsx/js/jsx, both scanImports and scan asserted for consistency, and a @dec()() discriminator to prove which dialect is selected with and without experimentalDecorators. The REPL tests use tempDir + cwd to isolate tsconfig, and assert on evaluation output rather than echoed input. No CODEOWNERS cover the touched files. No prior human reviews or outstanding comments. The bug hunter found nothing.

@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

CI status

The diff is green. On f9ce1f1e, 15 of the 16 test lanes ran and all 15 passed: all three Windows lanes, all four Debian lanes (including debian-13-x64-asan), Alpine, Ubuntu, and both macOS lanes that executed (darwin-14-x64, darwin-26-aarch64).

The single red check, buildkite/bun/darwin-14-aarch64-test-bun, is reported by Buildkite as Expired: the job sat in the queue and no macOS 14 aarch64 agent picked it up, so it never executed. There is no test output for it. Buildkite has since re-queued the job. The same lane was canceled on the prior build, so it has not run on this PR at all.

The earlier build (68600) had two macOS timeouts, terminal.test.ts on darwin-14-x64 and bun-serve-file.test.ts on darwin-26-aarch64. Both lanes pass on the current build.

Neither is reachable from this change. The diff is five files of parser-option plumbing: a boolean derived from tsconfig, threaded into ParserOptions.features. Nothing in it touches Bun.spawn, PTYs, or HTTP. The clearest evidence is that test/js/bun/repl/repl.test.ts, which this PR modifies and which runs roughly ten Bun.Terminal PTY tests through its withTerminalRepl helper, passed on darwin-14-x64 in the very run where terminal.test.ts's PTY tests hit the 90s default timeout. PTY was working on that agent; one file's tests were starved.

I am not pushing another retrigger commit. Happy to rebase or re-run on request.

Recap for review

scanImports and bun repl each built ParserOptions from scratch and never set features.standard_decorators, which defaults to false. The parser gates the accessor keyword and JS-side @decorators on that flag, so the fast import scanner and the REPL rejected syntax the transpiler they front already accepts. RuntimeFeatures::standard_decorators_for() is now the single source of truth and is called from all four sites that build parser options; the two bundler sites are a refactor of the identical inline expression.

13 tests added (11 in transpiler.test.js, 2 in repl.test.ts). All 13 fail on main and pass with this change, verified by stashing src/ and re-running under bun bd test. cargo clippy, cargo fmt --check, Prettier, and oxlint are clean.

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.

1 participant