fix(swc-plugin): register class expressions via an IIFE instead of by name - #3971
fix(swc-plugin): register class expressions via an IIFE instead of by name#3971TooTallNate wants to merge 2 commits into
Conversation
…nameable classes Class expressions with "use step" methods or custom serialization were registered by module-level statements referencing the class by name. When no module-scope binding could be resolved the plugin fell back to a placeholder `AnonymousClass` identifier, which is a guaranteed ReferenceError at module evaluation (#3929). Other shapes were silently wrong as well: `var A = class {}, B = class {}` registered A's steps under B, `X = class {}` assignments and classes nested inside functions emitted unresolvable references. Class expressions are now wrapped in a single IIFE that receives the class, performs every registration recorded for it, and returns it, so the registration no longer depends on a name being in scope. The class name is still needed for step/class IDs and is derived from the assigned variable, the class's own identifier, or the property key it is assigned to (`exports.Foo = class {}`, `{ Foo: class {} }`). When none is available, or the class is declared inside a function, the plugin emits a compile error instead of broken code. Class declarations keep their existing module-level output; the emitters were factored so both paths share the same statement builders.
🦋 Changeset detectedLatest commit: 8f107f9 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 |
🧪 E2E Test Results✅ All tests passed
|
| Passed | Failed | Skipped | Total | |
|---|---|---|---|---|
| ✅ ▲ Vercel Production | 3636 | 0 | 684 | 4320 |
| ✅ 💻 Local Development | 3922 | 0 | 558 | 4480 |
| ✅ 📦 Local Production | 3922 | 0 | 558 | 4480 |
| ✅ 🐘 Local Postgres | 3922 | 0 | 558 | 4480 |
| ✅ 🪟 Windows | 320 | 0 | 0 | 320 |
| ✅ 🌐 Cross-language Conformance | 68 | 0 | 73 | 141 |
| ✅ vercel-http-transport | 817 | 0 | 143 | 960 |
| ✅ vercel-multi-region | 27 | 0 | 0 | 27 |
| ✅ vercel-ws-transport | 553 | 0 | 87 | 640 |
| Total | 17187 | 0 | 2661 | 19848 |
Details by Category
✅ ▲ Vercel Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-node | 132 | 0 | 28 |
| ✅ astro-quickjs | 132 | 0 | 28 |
| ✅ example-node | 132 | 0 | 28 |
| ✅ example-quickjs | 132 | 0 | 28 |
| ✅ express-node | 132 | 0 | 28 |
| ✅ express-quickjs | 132 | 0 | 28 |
| ✅ fastify-node | 132 | 0 | 28 |
| ✅ fastify-quickjs | 132 | 0 | 28 |
| ✅ hono-node | 132 | 0 | 28 |
| ✅ hono-quickjs | 132 | 0 | 28 |
| ✅ nest-node | 132 | 0 | 28 |
| ✅ nest-quickjs | 132 | 0 | 28 |
| ✅ nextjs-turbopack-node | 157 | 0 | 3 |
| ✅ nextjs-turbopack-quickjs | 157 | 0 | 3 |
| ✅ nextjs-webpack-node | 157 | 0 | 3 |
| ✅ nextjs-webpack-quickjs | 157 | 0 | 3 |
| ✅ nitro-node | 132 | 0 | 28 |
| ✅ nitro-quickjs | 132 | 0 | 28 |
| ✅ nuxt-node | 132 | 0 | 28 |
| ✅ nuxt-quickjs | 132 | 0 | 28 |
| ✅ python-node | 66 | 0 | 94 |
| ✅ sveltekit-node | 151 | 0 | 9 |
| ✅ sveltekit-quickjs | 151 | 0 | 9 |
| ✅ tanstack-start-node | 132 | 0 | 28 |
| ✅ tanstack-start-quickjs | 132 | 0 | 28 |
| ✅ vite-node | 132 | 0 | 28 |
| ✅ vite-quickjs | 132 | 0 | 28 |
✅ 💻 Local Development
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 134 | 0 | 26 |
| ✅ astro-stable-quickjs | 134 | 0 | 26 |
| ✅ express-stable-node | 134 | 0 | 26 |
| ✅ express-stable-quickjs | 134 | 0 | 26 |
| ✅ fastify-stable-node | 134 | 0 | 26 |
| ✅ fastify-stable-quickjs | 134 | 0 | 26 |
| ✅ hono-stable-node | 134 | 0 | 26 |
| ✅ hono-stable-quickjs | 134 | 0 | 26 |
| ✅ nest-stable-node | 134 | 0 | 26 |
| ✅ nest-stable-quickjs | 134 | 0 | 26 |
| ✅ nextjs-turbopack-canary-node | 141 | 0 | 19 |
| ✅ nextjs-turbopack-canary-quickjs | 141 | 0 | 19 |
| ✅ nextjs-turbopack-stable-node | 160 | 0 | 0 |
| ✅ nextjs-turbopack-stable-quickjs | 160 | 0 | 0 |
| ✅ nextjs-webpack-canary-node | 141 | 0 | 19 |
| ✅ nextjs-webpack-canary-quickjs | 141 | 0 | 19 |
| ✅ nextjs-webpack-stable-node | 160 | 0 | 0 |
| ✅ nextjs-webpack-stable-quickjs | 160 | 0 | 0 |
| ✅ nitro-stable-node | 134 | 0 | 26 |
| ✅ nitro-stable-quickjs | 134 | 0 | 26 |
| ✅ nuxt-stable-node | 134 | 0 | 26 |
| ✅ nuxt-stable-quickjs | 134 | 0 | 26 |
| ✅ sveltekit-stable-node | 153 | 0 | 7 |
| ✅ sveltekit-stable-quickjs | 153 | 0 | 7 |
| ✅ tanstack-start-node | 134 | 0 | 26 |
| ✅ tanstack-start-quickjs | 134 | 0 | 26 |
| ✅ vite-stable-node | 134 | 0 | 26 |
| ✅ vite-stable-quickjs | 134 | 0 | 26 |
✅ 📦 Local Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 134 | 0 | 26 |
| ✅ astro-stable-quickjs | 134 | 0 | 26 |
| ✅ express-stable-node | 134 | 0 | 26 |
| ✅ express-stable-quickjs | 134 | 0 | 26 |
| ✅ fastify-stable-node | 134 | 0 | 26 |
| ✅ fastify-stable-quickjs | 134 | 0 | 26 |
| ✅ hono-stable-node | 134 | 0 | 26 |
| ✅ hono-stable-quickjs | 134 | 0 | 26 |
| ✅ nest-stable-node | 134 | 0 | 26 |
| ✅ nest-stable-quickjs | 134 | 0 | 26 |
| ✅ nextjs-turbopack-canary-node | 141 | 0 | 19 |
| ✅ nextjs-turbopack-canary-quickjs | 141 | 0 | 19 |
| ✅ nextjs-turbopack-stable-node | 160 | 0 | 0 |
| ✅ nextjs-turbopack-stable-quickjs | 160 | 0 | 0 |
| ✅ nextjs-webpack-canary-node | 141 | 0 | 19 |
| ✅ nextjs-webpack-canary-quickjs | 141 | 0 | 19 |
| ✅ nextjs-webpack-stable-node | 160 | 0 | 0 |
| ✅ nextjs-webpack-stable-quickjs | 160 | 0 | 0 |
| ✅ nitro-stable-node | 134 | 0 | 26 |
| ✅ nitro-stable-quickjs | 134 | 0 | 26 |
| ✅ nuxt-stable-node | 134 | 0 | 26 |
| ✅ nuxt-stable-quickjs | 134 | 0 | 26 |
| ✅ sveltekit-stable-node | 153 | 0 | 7 |
| ✅ sveltekit-stable-quickjs | 153 | 0 | 7 |
| ✅ tanstack-start-node | 134 | 0 | 26 |
| ✅ tanstack-start-quickjs | 134 | 0 | 26 |
| ✅ vite-stable-node | 134 | 0 | 26 |
| ✅ vite-stable-quickjs | 134 | 0 | 26 |
✅ 🐘 Local Postgres
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 134 | 0 | 26 |
| ✅ astro-stable-quickjs | 134 | 0 | 26 |
| ✅ express-stable-node | 134 | 0 | 26 |
| ✅ express-stable-quickjs | 134 | 0 | 26 |
| ✅ fastify-stable-node | 134 | 0 | 26 |
| ✅ fastify-stable-quickjs | 134 | 0 | 26 |
| ✅ hono-stable-node | 134 | 0 | 26 |
| ✅ hono-stable-quickjs | 134 | 0 | 26 |
| ✅ nest-stable-node | 134 | 0 | 26 |
| ✅ nest-stable-quickjs | 134 | 0 | 26 |
| ✅ nextjs-turbopack-canary-node | 141 | 0 | 19 |
| ✅ nextjs-turbopack-canary-quickjs | 141 | 0 | 19 |
| ✅ nextjs-turbopack-stable-node | 160 | 0 | 0 |
| ✅ nextjs-turbopack-stable-quickjs | 160 | 0 | 0 |
| ✅ nextjs-webpack-canary-node | 141 | 0 | 19 |
| ✅ nextjs-webpack-canary-quickjs | 141 | 0 | 19 |
| ✅ nextjs-webpack-stable-node | 160 | 0 | 0 |
| ✅ nextjs-webpack-stable-quickjs | 160 | 0 | 0 |
| ✅ nitro-stable-node | 134 | 0 | 26 |
| ✅ nitro-stable-quickjs | 134 | 0 | 26 |
| ✅ nuxt-stable-node | 134 | 0 | 26 |
| ✅ nuxt-stable-quickjs | 134 | 0 | 26 |
| ✅ sveltekit-stable-node | 153 | 0 | 7 |
| ✅ sveltekit-stable-quickjs | 153 | 0 | 7 |
| ✅ tanstack-start-node | 134 | 0 | 26 |
| ✅ tanstack-start-quickjs | 134 | 0 | 26 |
| ✅ vite-stable-node | 134 | 0 | 26 |
| ✅ vite-stable-quickjs | 134 | 0 | 26 |
✅ 🪟 Windows
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 160 | 0 | 0 |
| ✅ nextjs-turbopack-quickjs | 160 | 0 | 0 |
✅ 🌐 Cross-language Conformance
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ python | 68 | 0 | 73 |
✅ vercel-http-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 132 | 0 | 28 |
| ✅ express | 132 | 0 | 28 |
| ✅ hono | 132 | 0 | 28 |
| ✅ nextjs-turbopack | 157 | 0 | 3 |
| ✅ nitro | 132 | 0 | 28 |
| ✅ vite | 132 | 0 | 28 |
✅ vercel-multi-region
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack | 27 | 0 | 0 |
✅ vercel-ws-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 132 | 0 | 28 |
| ✅ express | 132 | 0 | 28 |
| ✅ nextjs-turbopack | 157 | 0 | 3 |
| ✅ vite | 132 | 0 | 28 |
📊 Workflow Benchmarkscommit Backend:
Streams
📈 STSO distribution vs main (inline / queue-hop histograms)1020 steps (inline) Cumulative STSO time: main 131526ms → this run 130396ms (Δ -1130ms, -1%) 📈 CRTT drill-down vs main (RTT distributions & profiles)RTT over stream progress (avg per tenth of stream, bars scaled min→max): RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max): Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max): ℹ️ Metric definitions & methodologyStreams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach. The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, Best/P75/P90/P99 deltas compare against the most recent benchmark run on Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it) Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000t 🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor. |
There was a problem hiding this comment.
🔵 Needs a closer look
It substantially refactors SWC transform code generation in a high-impact area (class/step registration semantics), so it warrants final maintainer review despite strong fixture/spec coverage.
Pull request overview
This PR fixes a SWC transform correctness issue where class expressions containing "use step" methods or custom serialization could produce module-level registrations that reference a non-existent identifier (e.g. AnonymousClass.prototype[...]), causing guaranteed ReferenceError during module evaluation (per #3929). It does so by changing class-expression registration to run inside an IIFE that closes over the class value, and by emitting compile-time errors for classes that cannot be safely referenced/registered from module scope.
Changes:
- Wrap class expressions that require registration (steps/workflows/getters/serde) in a single registration IIFE that receives the class, performs all recorded registrations, and returns it.
- Add explicit compile errors for (a) unnameable anonymous class expressions using step/workflow/serde features and (b) classes using those features declared inside functions.
- Expand/refresh fixtures + update the plugin spec to document the new class-expression handling and new diagnostics.
File summaries
| File | Description |
|---|---|
| packages/swc-plugin-workflow/transform/src/lib.rs | Implements class-expression registration via IIFE, name-resolution for IDs, and new compile-time errors for unreferenceable classes; refactors registration builders to share logic. |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/input.js | Adds a comprehensive fixture covering multiple bundler-emitted class-expression shapes that must remain referenceable and correctly named. |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-step.js | Updates expected step-mode output to validate IIFE-based registration, hoisted registry lookups, and stable IDs/names. |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-shapes/output-workflow.js | Updates expected workflow-mode output to validate proxy reattachment + class registration inside the IIFE. |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-step.js | Updates expected output to reflect class-expression serde registration happening inline via IIFE rather than module-end emissions. |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-workflow.js | Same as above for workflow mode. |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-step.js | Updates expected output so step registrations for class expressions occur within the class-expression IIFE. |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name-step-methods/output-workflow.js | Same as above for workflow mode. |
| packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/input.js | Adds error fixtures for anonymous/unnameable class expressions using step/workflow/serde features. |
| packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.js | Expected transformed JS output for the error fixture (showing unaffected valid cases). |
| packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-step.stderr | Expected diagnostics for anonymous/unnameable class expressions in step mode. |
| packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.js | Expected transformed JS output for the error fixture in workflow mode. |
| packages/swc-plugin-workflow/transform/tests/errors/anonymous-class-step-methods/output-workflow.stderr | Expected diagnostics for anonymous/unnameable class expressions in workflow mode. |
| packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/input.js | Adds error fixtures for nested (in-function) classes using step/workflow/serde features. |
| packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.js | Expected transformed JS output for the nested-class error fixture (showing unaffected valid cases). |
| packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-step.stderr | Expected diagnostics for nested classes in step mode. |
| packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.js | Expected transformed JS output for the nested-class error fixture in workflow mode. |
| packages/swc-plugin-workflow/transform/tests/errors/nested-class-step-methods/output-workflow.stderr | Expected diagnostics for nested classes in workflow mode. |
| packages/swc-plugin-workflow/spec.md | Updates the spec to explain the new IIFE-based class-expression strategy, name derivation order, and the new error cases. |
| .changeset/class-expression-registration-iife.md | Adds a patch changeset describing the behavior change and new diagnostics. |
Review details
- Files reviewed: 19/20 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Sim WorldSimulated world deterministic testing for races. Traces 🟠 world-sim scenario book — 1 fail of 41 total
Full trace: |
About these numbersSizes are gzip; parentheses show the change against
|
…ad of erroring
With registration happening inside the IIFE, an anonymous class expression
in a position that provides no name (`foo(class { ... })`, an array element,
a conditional branch) only needs a name for its step/class IDs. Generate a
deterministic `AnonymousClass<N>`, counting only anonymous classes that have
something to register, instead of rejecting them. Classes declared inside a
function remain an error.
Dead-code elimination now keeps module-level declarations whose initializer
contains a wrapped class expression: evaluating the initializer is what
registers the class, and the binding may be otherwise unreferenced.
Fixes #3929
Problem
Class expressions with
"use step"methods or custom serialization were registered by module-level statements that reference the class by name. When the plugin could not resolve a module-scope binding it fell back to a placeholderAnonymousClassidentifier, producing code likewhich is a guaranteed
ReferenceErroras soon as the module is evaluated. That is the failure in #3929.While investigating, I found the same class of bug live on
mainin other shapes, even with the #1599 binding-name fix in place:var A = class {…}, B = class {…}registeredA's steps underB's name andB's underAnonymousClasslet X; X = class {…},exports.X = class {…},var X = (class {…}),{ X: class {…} },foo(class {…})→AnonymousClassInner.prototype[...]at module scope →ReferenceErrorWhat actually happened in #3929
I could not reproduce the report with its declared versions. Every published
@vercel/sandbox(2.8.0 → 3.2.1) shipsvar FileSystem = class { … }, every published@workflow/swc-plugin >= 4.1.0-beta.22(everything any@workflow/next >= 4.0.2pins) handles that shape, and a fresh Next 16.3.1 + Turbopack +@workflow/next@4.1.2+@vercel/sandbox@3.1.0app builds cleanly (static and dynamic import). The exact reported output is produced only by plugins<= 4.1.0-beta.21, which still load under@swc/core@1.15.3, so a stale plugin resolving in the reporter's monorepo is the most likely cause. Independently of that, the plugin should never have been able to emit this code, which is what this PR fixes.Fix
Class expressions are wrapped in a single IIFE that receives the class, performs every registration recorded for it, and returns it:
The registration closes over the class value, so it no longer depends on any name being in scope, and every position a class expression can appear in produces the same shape. Registry lookups are hoisted once per registry inside the IIFE rather than repeated per registration.
Names are still needed for IDs (
step//…//Foo#run). They are derived, in order, from:var Foo = class _Foo {}→Foo; coversX = class {}, parenthesized initializers,var X = exports.X = class {}, and per-declarator for multi-declarator statements),foo(class Plugin {})),exports.Foo = class {},{ Foo: class {} }),AnonymousClass<N>(foo(class {…}), array element, conditional branch).Ncounts only anonymous classes that have something to register, and is de-duplicated against declared identifiers. Like_anonymousStep<N>, these are positional; the spec calls that out..nameis preserved by inserting the identifier for binding-derived names (as the existing serde path already did), viaObject.defineProperty(__wf_cls, "name", …)for key-derived names (which must not be introduced as a binding), and left as-is for generated names.Classes declared inside a function are a compile error when they use steps/workflows/serde: their registration would only run when (and each time) that function runs, not at module load, so their steps could not be resolved by ID. One error per class, none in
detectmode. Nested classes without steps or serialization are unaffected.Dead-code elimination keeps declarations whose initializer contains a wrapped class. The plugin's DCE removes any unreferenced module-level
var/const; registered class expressions used to survive it only because the module-end registration referenced their binding. Evaluating the initializer is now what registers the class, so such declarations are kept explicitly (const registry = new Map([["point", class { …serde… }]])).Class declarations keep their existing module-level output. The two inline-registration IIFE builders and the module-end emission loops were factored into shared statement builders parameterized by the class reference; the 123 fixtures not involving class expressions are byte-identical.
Testing
cargo test: 133 fixtures (newclass-expression-binding-shapesandclass-expression-anonymous-generated-namescovering every shape above, including an unreferenced binding surviving DCE) + 24 error cases (newnested-class-step-methods)@vercel/sandbox@3.1.0dist/*.jsin both modes, then evaluated the transformed modules in Node: 33 steps and 5 classes registered,FileSystem.name === "FileSystem", workflow-mode proxies attached@workflow/builders(502) and@workflow/next(71) vitest suitesworkbench/nextjs-turbopackproduction build; e2e serde subset (-t "Serde|serde|serializ", 10 tests) against the dev serverspec.mdupdated; changeset included