Skip to content

fix(bundle): skip decorator pass when module has no decorators#34489

Merged
littledivy merged 2 commits into
mainfrom
orch/divybot-253
May 29, 2026
Merged

fix(bundle): skip decorator pass when module has no decorators#34489
littledivy merged 2 commits into
mainfrom
orch/divybot-253

Conversation

@divybot

@divybot divybot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

deno bundle (and any other transpile consumer) currently emits a spurious
module-level hoist for every class that uses a computed member key, even when
the class has no decorators:

// main.ts
(() => {
  class A {
    *[Symbol.iterator]() {/* empty */}
  }
})();
// before
(() => {
  var _computedKey;
  _computedKey = Symbol.iterator;
  class A {
    *[_computedKey]() {}
  }
})();

That _computedKey = Symbol.iterator is a top-level expression statement, so
esbuild can't prove the IIFE is pure and refuses to tree-shake it. (esbuild
already has well-known-symbol handling for the un-hoisted form.)

Root cause: swc's Ecma decorator pass
(swc_ecma_transforms_proposal::decorator_2022_03) walks every class and
unconditionally calls process_decorators_of_class_members, which rewrites any
non-trivial computed prop name into a hoisted _computedKey even when neither
the class nor its members carry any decorators.

Fix: pre-scan the parsed program for any Decorator node before calling
parsed_source.transpile(...) in deno_resolver::emit::transpile. When the
module has none, force the transpile options' decorators field to
DecoratorsTranspileOption::None so swc's decorator pass is skipped entirely.
Modules that actually use decorators still take the full pass and behave
identically to before.

Verified locally:

$ deno bundle --output=out.js main.ts   # the issue's reproducer
Bundled 1 module in 29ms
  out.js 0B

The second reproducer in the issue (a plain [""] = "" computed field) also
tree-shakes to 0B, and a class with an actual @dec method still bundles
and runs correctly (decorator runtime is preserved).

Closes #30817.
Closes denoland/orchid#253

Test plan

  • tests/specs/bundle/tree_shake_computed_key exercises the original
    reproducer and asserts that the bundled output contains neither
    _computedKey nor class A (the IIFE has no other side effects, so
    esbuild can drop the whole thing).
  • Existing bundle/decorator specs unaffected.

The swc Ecma decorator transform unconditionally hoists every computed
class-member key into a `var _computedKey; _computedKey = ...;` pair at
module scope, even when no decorators are present. The resulting
module-level assignment looks like a side effect to esbuild and blocks
tree-shaking of otherwise-pure class declarations (e.g. classes whose
only computed key is `Symbol.iterator`).

Detect modules with no decorators before transpiling and force
`decorators: None` for them so the swc pass is skipped entirely. Modules
that actually use decorators are unaffected.

Fixes #30817.

Co-Authored-By: Divy Srivastava <[email protected]>
@divybot divybot changed the title [denoland/deno#30817] deno bundle doesn't tree-shake classes with computed keys fix(bundle): skip decorator pass when module has no decorators May 29, 2026
CI uses dprint 0.47.2 which formats the if condition slightly differently
than 0.54.0 — wrap the program_has_decorators call onto its own lines.

Co-Authored-By: Divy Srivastava <[email protected]>
@littledivy
littledivy merged commit 342c728 into main May 29, 2026
136 checks passed
@littledivy
littledivy deleted the orch/divybot-253 branch May 29, 2026 12:03
littledivy added a commit to crowlKats/deno that referenced this pull request Jun 10, 2026
…and#34489)

## Summary

`deno bundle` (and any other transpile consumer) currently emits a
spurious
module-level hoist for every class that uses a computed member key, even
when
the class has no decorators:

```ts
// main.ts
(() => {
  class A {
    *[Symbol.iterator]() {/* empty */}
  }
})();
```

```js
// before
(() => {
  var _computedKey;
  _computedKey = Symbol.iterator;
  class A {
    *[_computedKey]() {}
  }
})();
```

That `_computedKey = Symbol.iterator` is a top-level expression
statement, so
esbuild can't prove the IIFE is pure and refuses to tree-shake it.
(esbuild
already has well-known-symbol handling for the un-hoisted form.)

Root cause: swc's Ecma decorator pass
(`swc_ecma_transforms_proposal::decorator_2022_03`) walks every class
and
unconditionally calls `process_decorators_of_class_members`, which
rewrites any
non-trivial computed prop name into a hoisted `_computedKey` even when
neither
the class nor its members carry any decorators.

Fix: pre-scan the parsed program for any `Decorator` node before calling
`parsed_source.transpile(...)` in `deno_resolver::emit::transpile`. When
the
module has none, force the transpile options' `decorators` field to
`DecoratorsTranspileOption::None` so swc's decorator pass is skipped
entirely.
Modules that actually use decorators still take the full pass and behave
identically to before.

Verified locally:

```text
$ deno bundle --output=out.js main.ts   # the issue's reproducer
Bundled 1 module in 29ms
  out.js 0B
```

The second reproducer in the issue (a plain `[""] = ""` computed field)
also
tree-shakes to `0B`, and a class with an actual `@dec` method still
bundles
and runs correctly (decorator runtime is preserved).

Closes denoland#30817.
Closes denoland/orchid#253

## Test plan

- [ ] `tests/specs/bundle/tree_shake_computed_key` exercises the
original
      reproducer and asserts that the bundled output contains neither
`_computedKey` nor `class A` (the IIFE has no other side effects, so
      esbuild can drop the whole thing).
- [ ] Existing bundle/decorator specs unaffected.

---------

Co-authored-by: divybot <[email protected]>
Co-authored-by: Divy Srivastava <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deno bundle doesn't tree-shake classes with computed keys

2 participants