fix(bundle): skip decorator pass when module has no decorators#34489
Merged
Conversation
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]>
deno bundle doesn't tree-shake classes with computed keysCI 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
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
deno bundle(and any other transpile consumer) currently emits a spuriousmodule-level hoist for every class that uses a computed member key, even when
the class has no decorators:
That
_computedKey = Symbol.iteratoris a top-level expression statement, soesbuild 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 andunconditionally calls
process_decorators_of_class_members, which rewrites anynon-trivial computed prop name into a hoisted
_computedKeyeven when neitherthe class nor its members carry any decorators.
Fix: pre-scan the parsed program for any
Decoratornode before callingparsed_source.transpile(...)indeno_resolver::emit::transpile. When themodule has none, force the transpile options'
decoratorsfield toDecoratorsTranspileOption::Noneso swc's decorator pass is skipped entirely.Modules that actually use decorators still take the full pass and behave
identically to before.
Verified locally:
The second reproducer in the issue (a plain
[""] = ""computed field) alsotree-shakes to
0B, and a class with an actual@decmethod still bundlesand runs correctly (decorator runtime is preserved).
Closes #30817.
Closes denoland/orchid#253
Test plan
tests/specs/bundle/tree_shake_computed_keyexercises the originalreproducer and asserts that the bundled output contains neither
_computedKeynorclass A(the IIFE has no other side effects, soesbuild can drop the whole thing).