Inline class serialization registration to fix 3rd-party package support#1480
Conversation
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
🦋 Changeset detectedLatest commit: cb61bee The changes in this PR will be included in the next version bump. 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❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (56 failed)mongodb (3 failed):
redis (2 failed):
turso (51 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
|
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | 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. |
There was a problem hiding this comment.
Pull request overview
This PR updates the SWC transform to inline class serialization registration so transformed code no longer imports workflow/internal/class-serialization, fixing usage when transforming 3rd-party packages that don’t depend on workflow (e.g., under pnpm/Yarn PnP).
Changes:
- Replaced generated
import { registerSerializationClass } ...+ call sites with a self-contained IIFE that registers classes viaglobalThis[Symbol.for("workflow-class-registry")]. - Updated the SWC plugin spec examples to reflect the new inline output.
- Updated test fixtures (expected outputs) across workflow/step/client modes to match the new emitted code.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/swc-plugin-workflow/transform/src/lib.rs | Generates inline, dependency-free class registration IIFE and removes import injection. |
| packages/swc-plugin-workflow/spec.md | Updates documentation examples to the new inlined registration pattern. |
| .changeset/inline-class-serialization.md | Publishes a patch changeset for the SWC plugin behavior change. |
| packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/static-method-step/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/static-method-step/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/static-method-step/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-local-const/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-local-const/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-local-const/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-imported/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-imported/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-imported/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-client.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js | Fixture updated to expect inline class registration (no import). |
| packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js | Fixture updated to expect inline class registration (no import). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ibility - Add "use step" to all public async methods on Sandbox (14), Command (5), and Snapshot (3) classes so the SWC plugin can strip method bodies in workflow mode, replacing them with durable step proxies. - Replace sync `client` getter with async `ensureClient()` method (also marked "use step") on Sandbox and Command. This ensures the APIClient import and all its transitive Node.js dependencies (undici, zlib, tar-stream, etc.) are only referenced inside step method bodies, which get stripped in workflow mode. The previous sync getter kept APIClient in the module scope, pulling Node.js deps into the workflow bundle. - Set `bundle: false` in tsdown config so each source file produces its own output file. This keeps Node.js imports local to the files that use them rather than hoisting them to a single entry point, allowing the workflow compiler to tree-shake unused imports after stripping step bodies. - Remove `serverExternalPackages` from workflow-code-runner next.config.ts since the package now works correctly with the workflow compiler. - Update workflow-code-runner to use workflow tarball with inline class serialization registration fix (PR vercel/workflow#1480).
There was a problem hiding this comment.
Overall this is a well-motivated, clean change. The inline IIFE approach correctly solves the 3rd-party package resolution issue and the generated code faithfully reproduces the registerSerializationClass behavior. Spec and test fixtures are all consistently updated. A few observations below.
Re: packages/core/src/class-serialization.ts (not in diff, so commenting here): Now that the SWC plugin no longer generates import { registerSerializationClass } from "workflow/internal/class-serialization", the registerSerializationClass export is only consumed by serialization.test.ts. The docstring on line 33 — "Called by the SWC plugin in both step mode and workflow mode" — is now inaccurate. Consider updating it to reflect that the SWC plugin now inlines this logic, and this function is retained for testing/manual use. Also worth considering: should the tests be updated to exercise the new inline IIFE pattern instead, to keep tests aligned with production behavior?
VaguelySerious
left a comment
There was a problem hiding this comment.
LGTM aside from nits that Pranay mentioned
There was a problem hiding this comment.
Happy to approve but I think the easier and better solution is to actually move class-serialization from workflow/internal/class-serialization to @workflow/serde/class-serialization so you still benefit from module level deduplication (now the SWC compiler is inlining the source into every use of it, rather than having them all import from a module).
Let me know if I missed something
- Fix inaccurate IIFE comment in lib.rs: the second arg is the generated class ID string, not the literal "classId" - Update registerSerializationClass docstring to reflect that the SWC plugin now inlines equivalent logic rather than importing it
|
Re: @pranaygp's review comments — Stale docstring in Moving to |
The revert brought back test fixtures using the old registerSerializationClass import, but #1480 changed the SWC plugin to emit inline IIFEs instead. Update the 6 fixture output files and restore the __builtin special case in lib.rs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…naygp-db9e68c1 * 'main' of https://github.com/vercel/workflow: (32 commits) chore: bump @nestjs/* to ^11.1.17 (#1497) chore: bump hono to ^4.12.8 (#1495) Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493) [world] Add stream pagination and metadata endpoints (#1470) [cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490) Remove NestJS Vercel integration while in experimental phase (#1485) feat: export semantic error types and add API reference docs (#1447) feat: enforce max queue deliveries in handlers with graceful failure (#1344) [world-postgres] Migrate client from `postgres.js` to `pg` (#1484) Inline class serialization registration to fix 3rd-party package support (#1480) [ai] Add experimental_context to DurableAgentOptions (#1489) [ai] Expose configured tools on DurableAgent instances (#1488) fix(builders): catch node builtin usage when entry fields diverge (#1455) [web-shared] Fix timeline duration format and precision (#1482) [cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467) [utils] Re-export parseName utilities and add workflow/observability module (#1453) [o11y] Polish display when run data has expired (#1438) Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144) fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468) [web] Support legacy newline-delimited stream format in `useStreamReader` (#1473) ...
…naygp-6fadd605 * 'main' of https://github.com/vercel/workflow: (73 commits) chore: bump next to 16.2.1 and fix deferred build (#1496) chore: bump nitropack to ^2.13.1 (#1501) chore: bump nuxt ecosystem dependencies (#1500) chore: bump sveltekit ecosystem (#1498) chore: bump express and fastify in workbenches (#1499) chore: bump @nestjs/* to ^11.1.17 (#1497) chore: bump hono to ^4.12.8 (#1495) Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493) [world] Add stream pagination and metadata endpoints (#1470) [cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490) Remove NestJS Vercel integration while in experimental phase (#1485) feat: export semantic error types and add API reference docs (#1447) feat: enforce max queue deliveries in handlers with graceful failure (#1344) [world-postgres] Migrate client from `postgres.js` to `pg` (#1484) Inline class serialization registration to fix 3rd-party package support (#1480) [ai] Add experimental_context to DurableAgentOptions (#1489) [ai] Expose configured tools on DurableAgent instances (#1488) fix(builders): catch node builtin usage when entry fields diverge (#1455) [web-shared] Fix timeline duration format and precision (#1482) [cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467) ... # Conflicts: # packages/core/src/runtime/start.ts
…ort (#1480) * Inline class serialization registration to fix 3rd-party package support The SWC plugin previously generated: import { registerSerializationClass } from "workflow/internal/class-serialization"; registerSerializationClass("class//...", ClassName); This broke for 3rd-party packages (e.g. @vercel/sandbox) that define serializable classes but don't depend on the 'workflow' package. The bare 'workflow' specifier is unresolvable from within node_modules of a package that doesn't list it as a dependency. Now the plugin generates a self-contained IIFE that uses Symbol.for('workflow-class-registry') on globalThis directly, with zero module dependencies: (function(__wf_cls, __wf_id) { var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); __wf_reg.set(__wf_id, __wf_cls); Object.defineProperty(__wf_cls, "classId", { ... }); })(ClassName, "class//..."); This is fully compatible with the existing deserialization side in @workflow/core which reads from the same globalThis registry. * Address review feedback: fix comment and update docstring - Fix inaccurate IIFE comment in lib.rs: the second arg is the generated class ID string, not the literal "classId" - Update registerSerializationClass docstring to reflect that the SWC plugin now inlines equivalent logic rather than importing it
The original PR #1480 was merged but reverted because it didn't include updated fixtures for the CJS require patterns added by PR #1144 (custom-serialization-require-destructured and custom-serialization-require-namespace). These fixtures still had the old 'import { registerSerializationClass }' pattern instead of the new inline IIFE.
…ort (v2) (#1503) * Inline class serialization registration to fix 3rd-party package support (#1480) * Inline class serialization registration to fix 3rd-party package support The SWC plugin previously generated: import { registerSerializationClass } from "workflow/internal/class-serialization"; registerSerializationClass("class//...", ClassName); This broke for 3rd-party packages (e.g. @vercel/sandbox) that define serializable classes but don't depend on the 'workflow' package. The bare 'workflow' specifier is unresolvable from within node_modules of a package that doesn't list it as a dependency. Now the plugin generates a self-contained IIFE that uses Symbol.for('workflow-class-registry') on globalThis directly, with zero module dependencies: (function(__wf_cls, __wf_id) { var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); __wf_reg.set(__wf_id, __wf_cls); Object.defineProperty(__wf_cls, "classId", { ... }); })(ClassName, "class//..."); This is fully compatible with the existing deserialization side in @workflow/core which reads from the same globalThis registry. * Address review feedback: fix comment and update docstring - Fix inaccurate IIFE comment in lib.rs: the second arg is the generated class ID string, not the literal "classId" - Update registerSerializationClass docstring to reflect that the SWC plugin now inlines equivalent logic rather than importing it * Update CJS require fixture outputs for inline class serialization The original PR #1480 was merged but reverted because it didn't include updated fixtures for the CJS require patterns added by PR #1144 (custom-serialization-require-destructured and custom-serialization-require-namespace). These fixtures still had the old 'import { registerSerializationClass }' pattern instead of the new inline IIFE.
Summary
registerSerializationClasslogic as a self-contained IIFE instead of importing fromworkflow/internal/class-serialization@vercel/sandbox) that define serializable classes could not have their code properly transformed, because the generatedimport ... from "workflow/internal/class-serialization"is unresolvable from withinnode_modulesof a package that doesn't depend onworkflowProblem
When the SWC plugin transformed a file containing a serializable class, it generated:
This works for project-local files (the project depends on
workflow), but fails for 3rd-party packages like@vercel/sandboxbecause:@vercel/sandboxdepends on@workflow/serde(standalone, zero deps) — not onworkflow"workflow"from withinnode_modules/@vercel/sandbox/, which fails under strict package managers (pnpm, yarn PnP)serverExternalPackagesinnext.config.ts, which shouldn't be necessarySolution
The generated code is now a self-contained IIFE with zero module dependencies:
This uses
Symbol.for("workflow-class-registry")— the same well-known global symbol that@workflow/core/class-serialization.tsuses — so it's fully compatible with the existing deserialization side.What changed
packages/swc-plugin-workflow/transform/src/lib.rs: Replacedcreate_class_serialization_import()+create_class_serialization_registration()with a singlecreate_class_serialization_registration()that generates the self-contained IIFEpackages/swc-plugin-workflow/spec.md: Updated all code examples to reflect the new inlined patternTesting
cargo test)pnpm testinpackages/core)pnpm testinpackages/builders)pnpm build)Context
Related to
@vercel/sandboxserde PR: vercel/sandbox#72