fix(swc-plugin): count destructuring-default references in DCE usage analysis#2398
fix(swc-plugin): count destructuring-default references in DCE usage analysis#2398pranaygp wants to merge 2 commits into
Conversation
…analysis
The DCE usage collector skipped the entire variable name pattern when
visiting a `VarDeclarator` (to avoid marking the binding name as "used").
But default-value initializers inside destructuring patterns live in that
pattern — e.g. the `TTL` in `const { ttl = TTL } = options;` — so those
references were invisible to the collector. A module-scope `const`
referenced only through such a default was treated as unused and stripped,
while the surviving code kept reading it, producing a runtime
`ReferenceError` when the default fired.
Traverse the default-value initializer expressions (and computed keys)
within destructuring patterns while still not marking the binding names
themselves, so the referenced declaration is preserved. Function-parameter
defaults were already covered (params are visited in full).
Fixes #2396.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 5f05ea6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Benchmark Results
⏳ Benchmarks are running... _Started at: _ 📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
❌ Some benchmark jobs failed:
Check the workflow run for details. |
🧪 E2E Test Results
⏳ Tests are running... _Started at: _ ❌ Some tests failed Summary
❌ Failed Tests🐘 Local Postgres (1 failed)nextjs-turbopack-canary (1 failed):
📋 Other (1 failed)e2e-vercel-prod-tanstack-start (1 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
❌ 🐘 Local Postgres
✅ 🪟 Windows
❌ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
There was a problem hiding this comment.
Pull request overview
Fixes the SWC plugin’s DCE usage analysis so module-scope declarations referenced only via destructuring default initializers (e.g. const { ttl = TTL } = options) are correctly counted as “used”, preventing runtime ReferenceErrors when defaults fire.
Changes:
- Extend
ComprehensiveUsageCollectorto traverse destructuring patterns and visit only default-value initializers (and computed keys), without marking binding identifiers as used. - Add a regression fixture covering module-scope const preservation in both step and workflow modes (and confirming truly-unused consts are still removed).
- Update the SWC plugin spec and add a changeset for the patch release.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
packages/swc-plugin-workflow/transform/src/lib.rs |
Adds visit_pat_default_initializers and wires it into visit_mut_var_declarator so destructuring-default references are counted for DCE. |
packages/swc-plugin-workflow/transform/tests/fixture/destructuring-default-references-module-const/input.js |
New fixture input reproducing the destructuring-default-only reference pattern and an unused const. |
packages/swc-plugin-workflow/transform/tests/fixture/destructuring-default-references-module-const/output-step.js |
Step-mode snapshot asserting referenced consts survive and unused const is removed. |
packages/swc-plugin-workflow/transform/tests/fixture/destructuring-default-references-module-const/output-workflow.js |
Workflow-mode snapshot asserting referenced consts survive and unused const is removed. |
packages/swc-plugin-workflow/spec.md |
Documents that destructuring-default initializers count as references for DCE in both modes. |
.changeset/swc-destructuring-default-dce.md |
Patch changeset describing the DCE fix and the prevented runtime error. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
* origin/main: docs: document run idempotency (#2011) Render attr_set events and run attributes in observability UI (#2393) [ci] Fix backport job model slug (#2403) [ci] Comment on PR when backport fails, revert to use opus 4.8 (#2400) Update queue client to 0.3.1 (#2399) fix(deps): upgrade esbuild to 0.28.1 (GHSA-gv7w-rqvm-qjhr) (#2395)
Fixes #2396.
Problem
The SWC plugin's dead-code-elimination (DCE) usage analysis did not count identifier references that appear as default values inside a destructuring pattern (e.g.
const { ttl = TTL } = options;). When a module-scopeconstwas referenced only through such a destructuring default, the DCE pass treated it as unused and stripped it from the emitted bundle — while keeping the code that referenced it. The result was a runtimeReferenceError: <const> is not definedwhen the default fired.This is mode-independent (reproduces in both step and workflow mode) and class-independent (a plain top-level function exhibits it too). It shipped as a worked-around bug in the kill-switch pattern (#1858) and remains latent in
workbench/vitest/workflows/cookbook/distributed-abort-controller.ts.Root cause
In
ComprehensiveUsageCollector::visit_mut_var_declarator, the collector visited only the initializer and deliberately skipped the entirevar_decl.namepattern (to avoid marking the binding name as "used"). For destructuring patterns, the default-value initializer expressions live insidevar_decl.name— so references like theTTLin{ ttl = TTL }were never added toused_identifiers, andremove_dead_codethen pruned the still-referenced declaration.Fix
Added
visit_pat_default_initializers, which walks a binding pattern and visits only the default-value initializer expressions (and computed keys) —ObjectPatProp::KeyValuevalues,ObjectPatProp::Assign.value,AssignPat.right, array element defaults, rest args — while still not marking the binding names themselves as used.visit_mut_var_declaratornow calls it onvar_decl.name.Function-parameter destructuring defaults were already covered:
visit_mut_fn_declvisits params in full (confirmed by the existingdefault-parameter-usagefixture), so no change was needed there.Tests
destructuring-default-references-module-const/asserts that two module-scope consts referenced only via destructuring defaults — one inside a class static method, one inside a plain exported function — survive in both step and workflow mode. A third, genuinely-unused const is still stripped, confirming DCE is not over-broadened.main(consts stripped) and passes with the fix.cargo test -p swc_workflowsuite green (128 fixture tests + error tests).Spec
Updated
packages/swc-plugin-workflow/spec.md(both the step-mode and workflow-mode DCE sections) to note that a reference counts even when it appears only inside a destructuring-default initializer.Relation to prior fixes
Same symptom class as #1944 (DCE removing a declaration that surviving code references) but a distinct root cause: #1944 was about DCE ordering relative to step hoisting; this is about the usage collector never traversing destructuring-default initializers at all, and reproduces with no nested/hoisted steps involved.
🤖 Generated with Claude Code