Skip to content

Inline class serialization registration to fix 3rd-party package support#1480

Merged
TooTallNate merged 2 commits intomainfrom
nrajlich/inline-class-serialization-registration
Mar 23, 2026
Merged

Inline class serialization registration to fix 3rd-party package support#1480
TooTallNate merged 2 commits intomainfrom
nrajlich/inline-class-serialization-registration

Conversation

@TooTallNate
Copy link
Member

Summary

  • The SWC plugin now inlines the registerSerializationClass logic as a self-contained IIFE instead of importing from workflow/internal/class-serialization
  • This fixes a fundamental issue where 3rd-party packages (like @vercel/sandbox) that define serializable classes could not have their code properly transformed, because the generated import ... from "workflow/internal/class-serialization" is unresolvable from within node_modules of a package that doesn't depend on workflow

Problem

When the SWC plugin transformed a file containing a serializable class, it generated:

import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//./path//ClassName", ClassName);

This works for project-local files (the project depends on workflow), but fails for 3rd-party packages like @vercel/sandbox because:

  1. @vercel/sandbox depends on @workflow/serde (standalone, zero deps) — not on workflow
  2. When Next.js/Turbopack bundles the transformed code, it tries to resolve "workflow" from within node_modules/@vercel/sandbox/, which fails under strict package managers (pnpm, yarn PnP)
  3. The workaround was adding packages to serverExternalPackages in next.config.ts, which shouldn't be necessary

Solution

The generated code is now a self-contained IIFE 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", {
        value: __wf_id, writable: false, enumerable: false, configurable: false
    });
})(ClassName, "class//./path//ClassName");

This uses Symbol.for("workflow-class-registry") — the same well-known global symbol that @workflow/core/class-serialization.ts uses — so it's fully compatible with the existing deserialization side.

What changed

  • packages/swc-plugin-workflow/transform/src/lib.rs: Replaced create_class_serialization_import() + create_class_serialization_registration() with a single create_class_serialization_registration() that generates the self-contained IIFE
  • packages/swc-plugin-workflow/spec.md: Updated all code examples to reflect the new inlined pattern
  • 27 test fixture files: Updated to match the new output format (all 129 SWC tests pass)

Testing

  • All 129 SWC plugin tests pass (cargo test)
  • All 475 core package tests pass (pnpm test in packages/core)
  • All 103 builder tests pass (pnpm test in packages/builders)
  • Full workspace build succeeds (pnpm build)

Context

Related to @vercel/sandbox serde PR: vercel/sandbox#72

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.
Copilot AI review requested due to automatic review settings March 22, 2026 20:00
@TooTallNate TooTallNate requested a review from a team as a code owner March 22, 2026 20:00
@vercel
Copy link
Contributor

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
example-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-astro-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-express-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-fastify-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-hono-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-nestjs-workflow Error Error Open in v0 Mar 23, 2026 10:56pm
workbench-nitro-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-nuxt-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-sveltekit-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workbench-vite-workflow Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm
workflow-swc-playground Ready Ready Preview, Comment, Open in v0 Mar 23, 2026 10:56pm

@changeset-bot
Copy link

changeset-bot bot commented Mar 22, 2026

🦋 Changeset detected

Latest 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

@github-actions
Copy link
Contributor

github-actions bot commented Mar 22, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 780 0 67 847
✅ 💻 Local Development 782 0 142 924
✅ 📦 Local Production 782 0 142 924
✅ 🐘 Local Postgres 782 0 142 924
✅ 🪟 Windows 72 0 5 77
❌ 🌍 Community Worlds 118 56 21 195
✅ 📋 Other 198 0 33 231
Total 3514 56 552 4122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMEEMMXXYDD4MB5D518TGSSP
  • webhookWorkflow | wrun_01KMEEMY9WJDF939SWZA390FC4
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMEEVC8VVKCRRDZ0VBW5JC2Y

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMEEMMXXYDD4MB5D518TGSSP
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMEEVC8VVKCRRDZ0VBW5JC2Y

turso (51 failed):

  • addTenWorkflow | wrun_01KMEEKC9RWJD1DC2DGZKQP8RC
  • addTenWorkflow | wrun_01KMEEKC9RWJD1DC2DGZKQP8RC
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMEEMJ5JNRZ8M1MRTW6CMZHA
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMEEKMERXTEWF689KQRR6DKN
  • promiseRaceWorkflow | wrun_01KMEEKTKBYW37J265X0QEKAQB
  • promiseAnyWorkflow | wrun_01KMEEKWY3V4S7VC1G7HJ0R8Z3
  • importedStepOnlyWorkflow | wrun_01KMEEMXM94PCQHK4CK3EP13B8
  • hookWorkflow | wrun_01KMEEMABNRPXSYHA1ZAXW2QNV
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMEEMMXXYDD4MB5D518TGSSP
  • webhookWorkflow | wrun_01KMEEMY9WJDF939SWZA390FC4
  • sleepingWorkflow | wrun_01KMEEN4QA54DXKE7S5ZPVDNBH
  • parallelSleepWorkflow | wrun_01KMEENGJCX88DJQEPK4F3QPNZ
  • nullByteWorkflow | wrun_01KMEENM03XJ9ZSMTHQP96FEXN
  • workflowAndStepMetadataWorkflow | wrun_01KMEENPE0WWNZ64R35NPKDG3D
  • fetchWorkflow | wrun_01KMEEQJ2SPNXX75VH427VKA44
  • promiseRaceStressTestWorkflow | wrun_01KMEEQPFD691KDK2VWS5XKQD7
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMEETPX4S1JCKMG5K6K08997
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMEEVC8VVKCRRDZ0VBW5JC2Y
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMEEW2H59G0AX1CX7JBA3C1F
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMEEWQ3QTGMXNF7HFHWTVGYP
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMEEX0DBP3KF8AA0DYMERQYR
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMEEX698DJPFDRJ8GDEF2W08
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMEEX8PWR1R3ZN9029T39GBD
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMEEXRB4A0HMRHEZ2FJ08XNT
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMEEXY7DW5MP7SSQVNV2KDNX
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMEEY4Z8CNK95N2MH0PYRY5Y
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMEEYC1S8DB9YF574WCE0JYW
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMEEYK8PBTAYDAMHRDEP21HM
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMEEYTDBAA7E13JAK5YRN4EW
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMEEZ1M9JSJ4AWHHX0H8NFA0
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMEEZCSZA3FAWC8TBZWPJJRG
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMEEZP1XY9NEYNDZRF55YRN9
  • cancelRun - cancelling a running workflow | wrun_01KMEEZX2FB51CBMHQ2NWHXMCJ
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMEF06SC9PNY993G3NWER0ZP
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMEF0KNJX5Z5G942DCX240T1
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMEF18RRS11VYAEKVEFK59TZ
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMEF1MHXJXJNZWD57EQGAVPG

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 70 0 7
✅ example 70 0 7
✅ express 70 0 7
✅ fastify 70 0 7
✅ hono 70 0 7
✅ nextjs-turbopack 75 0 2
✅ nextjs-webpack 75 0 2
✅ nitro 70 0 7
✅ nuxt 70 0 7
✅ sveltekit 70 0 7
✅ vite 70 0 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 66 0 11
✅ express-stable 66 0 11
✅ fastify-stable 66 0 11
✅ hono-stable 66 0 11
✅ nextjs-turbopack-canary 55 0 22
✅ nextjs-turbopack-stable 72 0 5
✅ nextjs-webpack-canary 55 0 22
✅ nextjs-webpack-stable 72 0 5
✅ nitro-stable 66 0 11
✅ nuxt-stable 66 0 11
✅ sveltekit-stable 66 0 11
✅ vite-stable 66 0 11
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 66 0 11
✅ express-stable 66 0 11
✅ fastify-stable 66 0 11
✅ hono-stable 66 0 11
✅ nextjs-turbopack-canary 55 0 22
✅ nextjs-turbopack-stable 72 0 5
✅ nextjs-webpack-canary 55 0 22
✅ nextjs-webpack-stable 72 0 5
✅ nitro-stable 66 0 11
✅ nuxt-stable 66 0 11
✅ sveltekit-stable 66 0 11
✅ vite-stable 66 0 11
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 66 0 11
✅ express-stable 66 0 11
✅ fastify-stable 66 0 11
✅ hono-stable 66 0 11
✅ nextjs-turbopack-canary 55 0 22
✅ nextjs-turbopack-stable 72 0 5
✅ nextjs-webpack-canary 55 0 22
✅ nextjs-webpack-stable 72 0 5
✅ nitro-stable 66 0 11
✅ nuxt-stable 66 0 11
✅ sveltekit-stable 66 0 11
✅ vite-stable 66 0 11
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 72 0 5
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 2
❌ mongodb 52 3 5
✅ redis-dev 3 0 2
❌ redis 53 2 5
✅ turso-dev 3 0 2
❌ turso 4 51 5
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 66 0 11
✅ e2e-local-postgres-nest-stable 66 0 11
✅ e2e-local-prod-nest-stable 66 0 11

📋 View full workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Mar 22, 2026

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.042s (-7.8% 🟢) 1.006s (~) 0.964s 10 1.00x
💻 Local Next.js (Turbopack) 0.049s 1.005s 0.957s 10 1.15x
🌐 Redis Next.js (Turbopack) 0.054s 1.007s 0.952s 10 1.29x
🐘 Postgres Next.js (Turbopack) 0.060s 1.011s 0.952s 10 1.41x
🐘 Postgres Nitro 0.060s (-13.4% 🟢) 1.012s (~) 0.952s 10 1.42x
🐘 Postgres Express 0.062s (+11.7% 🔺) 1.011s (~) 0.949s 10 1.46x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.445s (-23.7% 🟢) 2.437s (+14.4% 🔺) 1.991s 10 1.00x
▲ Vercel Next.js (Turbopack) 0.473s (-28.4% 🟢) 2.606s (+9.0% 🔺) 2.132s 10 1.06x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.120s 2.007s 0.887s 10 1.00x
💻 Local Express 1.124s (~) 2.006s (~) 0.882s 10 1.00x
💻 Local Next.js (Turbopack) 1.124s 2.005s 0.882s 10 1.00x
🐘 Postgres Next.js (Turbopack) 1.135s 2.013s 0.878s 10 1.01x
🐘 Postgres Express 1.151s (~) 2.012s (~) 0.861s 10 1.03x
🐘 Postgres Nitro 1.161s (~) 2.012s (~) 0.851s 10 1.04x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.031s (-2.6%) 3.660s (+8.6% 🔺) 1.629s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.080s (+0.9%) 3.705s (+1.1%) 1.625s 10 1.02x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 10.748s 11.022s 0.274s 3 1.00x
💻 Local Next.js (Turbopack) 10.754s 11.023s 0.270s 3 1.00x
🐘 Postgres Express 10.895s (~) 11.038s (~) 0.143s 3 1.01x
💻 Local Express 10.905s (-0.5%) 11.021s (~) 0.116s 3 1.01x
🐘 Postgres Nitro 10.929s (-1.9%) 11.043s (-8.4% 🟢) 0.114s 3 1.02x
🐘 Postgres Next.js (Turbopack) 10.940s 11.046s 0.106s 3 1.02x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 17.281s (~) 19.115s (~) 1.834s 2 1.00x
▲ Vercel Nitro 17.340s (-3.1%) 18.742s (-4.5%) 1.402s 2 1.00x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 14.247s 15.029s 0.781s 4 1.00x
🐘 Postgres Next.js (Turbopack) 14.563s 15.044s 0.481s 4 1.02x
🐘 Postgres Express 14.586s (-1.6%) 15.040s (~) 0.454s 4 1.02x
💻 Local Next.js (Turbopack) 14.614s 15.030s 0.416s 4 1.03x
🐘 Postgres Nitro 14.715s (-1.2%) 15.044s (~) 0.329s 4 1.03x
💻 Local Express 14.916s (-0.8%) 15.030s (-4.8%) 0.114s 4 1.05x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 34.234s (+1.8%) 36.289s (+4.2%) 2.055s 2 1.00x
▲ Vercel Nitro 34.818s (+6.5% 🔺) 36.646s (+7.6% 🔺) 1.828s 2 1.02x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 13.485s 14.027s 0.542s 7 1.00x
🐘 Postgres Next.js (Turbopack) 13.985s 14.324s 0.339s 7 1.04x
🐘 Postgres Express 14.218s (-1.5%) 15.037s (~) 0.819s 6 1.05x
🐘 Postgres Nitro 14.272s (-3.9%) 15.042s (-1.1%) 0.770s 6 1.06x
💻 Local Next.js (Turbopack) 16.071s 16.696s 0.625s 6 1.19x
💻 Local Express 16.568s (-2.7%) 17.029s (-1.9%) 0.461s 6 1.23x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 59.376s (-3.8%) 60.985s (-3.0%) 1.609s 2 1.00x
▲ Vercel Next.js (Turbopack) 64.849s (+5.9% 🔺) 67.087s (+7.3% 🔺) 2.239s 2 1.09x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.251s 2.011s 0.760s 15 1.00x
🐘 Postgres Nitro 1.285s (-1.2%) 2.011s (~) 0.726s 15 1.03x
🐘 Postgres Express 1.298s (+1.2%) 2.011s (~) 0.713s 15 1.04x
🌐 Redis Next.js (Turbopack) 1.323s 2.007s 0.683s 15 1.06x
💻 Local Express 1.491s (-2.6%) 2.005s (~) 0.514s 15 1.19x
💻 Local Next.js (Turbopack) 1.538s 2.006s 0.468s 15 1.23x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.535s (-2.2%) 3.958s (-3.4%) 1.423s 8 1.00x
▲ Vercel Nitro 2.864s (+13.4% 🔺) 4.299s (+12.0% 🔺) 1.436s 7 1.13x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.449s (~) 3.012s (~) 0.562s 10 1.00x
🐘 Postgres Next.js (Turbopack) 2.469s 3.011s 0.542s 10 1.01x
🐘 Postgres Nitro 2.480s (-0.5%) 3.012s (~) 0.532s 10 1.01x
🌐 Redis Next.js (Turbopack) 2.575s 3.008s 0.433s 10 1.05x
💻 Local Express 2.900s (-6.4% 🟢) 3.108s (-22.5% 🟢) 0.208s 10 1.18x
💻 Local Next.js (Turbopack) 2.952s 3.454s 0.502s 9 1.21x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.269s (+25.5% 🔺) 4.828s (+31.6% 🔺) 1.559s 7 1.00x
▲ Vercel Next.js (Turbopack) 3.523s (+20.0% 🔺) 5.320s (+24.1% 🔺) 1.797s 6 1.08x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.609s (+1.1%) 4.013s (~) 0.404s 8 1.00x
🐘 Postgres Nitro 3.637s (-0.8%) 4.014s (~) 0.377s 8 1.01x
🐘 Postgres Next.js (Turbopack) 3.855s 4.139s 0.285s 8 1.07x
🌐 Redis Next.js (Turbopack) 4.197s 5.011s 0.814s 6 1.16x
💻 Local Express 8.162s (-6.4% 🟢) 9.020s (-2.7%) 0.858s 4 2.26x
💻 Local Next.js (Turbopack) 8.210s 8.770s 0.561s 4 2.27x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.141s (-7.3% 🟢) 4.745s (-1.0%) 1.605s 7 1.00x
▲ Vercel Next.js (Turbopack) 4.035s (-3.1%) 5.706s (+2.7%) 1.672s 6 1.28x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.243s 2.011s 0.768s 15 1.00x
🐘 Postgres Nitro 1.264s (-2.1%) 2.011s (~) 0.747s 15 1.02x
🐘 Postgres Express 1.302s (+0.9%) 2.012s (~) 0.710s 15 1.05x
🌐 Redis Next.js (Turbopack) 1.327s 2.006s 0.679s 15 1.07x
💻 Local Express 1.503s (-2.6%) 2.006s (~) 0.502s 15 1.21x
💻 Local Next.js (Turbopack) 1.530s 2.006s 0.475s 15 1.23x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.545s (+10.3% 🔺) 4.135s (+10.0% 🔺) 1.590s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.877s (+11.3% 🔺) 4.500s (+19.0% 🔺) 1.623s 7 1.13x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 2.445s 3.011s 0.566s 10 1.00x
🐘 Postgres Express 2.450s (~) 3.012s (~) 0.561s 10 1.00x
🐘 Postgres Nitro 2.462s (-1.3%) 3.011s (~) 0.549s 10 1.01x
🌐 Redis Next.js (Turbopack) 2.566s 3.008s 0.442s 10 1.05x
💻 Local Express 2.934s (-7.5% 🟢) 3.759s (-6.3% 🟢) 0.824s 8 1.20x
💻 Local Next.js (Turbopack) 2.976s 3.563s 0.587s 9 1.22x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.533s (-11.9% 🟢) 4.008s (-6.9% 🟢) 1.475s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.780s (-7.2% 🟢) 4.222s (-1.6%) 1.442s 8 1.10x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.589s (-1.6%) 4.013s (~) 0.424s 8 1.00x
🐘 Postgres Express 3.610s (~) 4.013s (~) 0.403s 8 1.01x
🐘 Postgres Next.js (Turbopack) 3.751s 4.015s 0.263s 8 1.05x
🌐 Redis Next.js (Turbopack) 4.180s 4.725s 0.545s 7 1.16x
💻 Local Express 8.544s (-6.6% 🟢) 9.023s (-10.0% 🟢) 0.479s 4 2.38x
💻 Local Next.js (Turbopack) 8.722s 9.020s 0.298s 4 2.43x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.288s (+6.0% 🔺) 4.917s (+4.4%) 1.630s 7 1.00x
▲ Vercel Next.js (Turbopack) 3.522s (-1.8%) 5.385s (+9.3% 🔺) 1.863s 6 1.07x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 0.689s 1.005s 0.315s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.822s 1.009s 0.187s 60 1.19x
💻 Local Next.js (Turbopack) 0.863s 1.038s 0.176s 59 1.25x
🐘 Postgres Nitro 0.873s (-9.9% 🟢) 1.009s (-18.3% 🟢) 0.136s 60 1.27x
🐘 Postgres Express 0.888s (-2.9%) 1.100s (+1.9%) 0.211s 55 1.29x
💻 Local Express 0.974s (-3.7%) 1.113s (-27.9% 🟢) 0.140s 55 1.41x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 9.921s (~) 11.817s (-4.4%) 1.896s 6 1.00x
▲ Vercel Nitro 10.297s (+2.3%) 12.033s (+5.3% 🔺) 1.736s 5 1.04x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.700s 2.028s 0.328s 45 1.00x
🐘 Postgres Next.js (Turbopack) 1.996s 2.285s 0.290s 40 1.17x
🐘 Postgres Nitro 2.128s (-7.9% 🟢) 3.012s (~) 0.883s 30 1.25x
🐘 Postgres Express 2.149s (-3.4%) 3.011s (~) 0.862s 30 1.26x
💻 Local Next.js (Turbopack) 2.634s 3.007s 0.373s 30 1.55x
💻 Local Express 2.972s (-2.0%) 3.258s (-11.7% 🟢) 0.286s 28 1.75x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 32.733s (+9.0% 🔺) 34.644s (+8.6% 🔺) 1.911s 3 1.00x
▲ Vercel Next.js (Turbopack) 34.015s (+4.9%) 35.574s (+5.7% 🔺) 1.559s 3 1.04x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 3.403s 4.009s 0.606s 30 1.00x
🐘 Postgres Next.js (Turbopack) 4.080s 4.706s 0.626s 26 1.20x
🐘 Postgres Nitro 4.300s (-10.2% 🟢) 5.014s (-1.7%) 0.714s 24 1.26x
🐘 Postgres Express 4.354s (-3.1%) 5.055s (+0.8%) 0.701s 24 1.28x
💻 Local Next.js (Turbopack) 8.435s 9.017s 0.582s 14 2.48x
💻 Local Express 9.069s (+3.0%) 9.633s (+5.2% 🔺) 0.564s 13 2.67x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 86.802s (+0.7%) 88.199s (+1.1%) 1.397s 2 1.00x
▲ Vercel Next.js (Turbopack) 91.081s (+5.8% 🔺) 92.971s (+5.1% 🔺) 1.891s 2 1.05x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.269s 1.009s 0.740s 60 1.00x
🐘 Postgres Nitro 0.294s (-7.3% 🟢) 1.009s (~) 0.715s 60 1.09x
🐘 Postgres Express 0.311s (+4.8%) 1.009s (~) 0.699s 60 1.15x
🌐 Redis Next.js (Turbopack) 0.430s 1.021s 0.591s 59 1.60x
💻 Local Next.js (Turbopack) 0.562s 1.021s 0.459s 59 2.09x
💻 Local Express 0.607s (+6.7% 🔺) 1.005s (~) 0.398s 60 2.26x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.594s (-5.1% 🟢) 3.351s (+1.5%) 1.757s 18 1.00x
▲ Vercel Next.js (Turbopack) 2.095s (+10.7% 🔺) 3.941s (+5.1% 🔺) 1.847s 16 1.31x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.530s (-9.2% 🟢) 1.010s (~) 0.480s 90 1.00x
🐘 Postgres Next.js (Turbopack) 0.550s 1.010s 0.459s 90 1.04x
🐘 Postgres Express 0.554s (+1.2%) 1.010s (~) 0.455s 90 1.05x
🌐 Redis Next.js (Turbopack) 1.209s 2.006s 0.797s 45 2.28x
💻 Local Next.js (Turbopack) 2.484s 3.008s 0.524s 30 4.69x
💻 Local Express 2.503s (+2.1%) 3.008s (~) 0.505s 30 4.73x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.237s (+7.8% 🔺) 4.812s (+8.2% 🔺) 1.575s 19 1.00x
▲ Vercel Next.js (Turbopack) 3.512s (+8.3% 🔺) 5.160s (+9.2% 🔺) 1.647s 18 1.08x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.912s 1.125s 0.212s 107 1.00x
🐘 Postgres Nitro 0.921s (-4.7%) 1.203s (-14.0% 🟢) 0.282s 100 1.01x
🐘 Postgres Express 0.947s (+3.0%) 1.306s (+5.3% 🔺) 0.359s 93 1.04x
🌐 Redis Next.js (Turbopack) 2.876s 3.137s 0.260s 39 3.15x
💻 Local Next.js (Turbopack) 10.349s 10.931s 0.582s 11 11.34x
💻 Local Express 11.134s (+1.1%) 11.844s (+1.5%) 0.710s 11 12.21x
💻 Local Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 7.896s (+9.2% 🔺) 9.588s (+10.5% 🔺) 1.692s 13 1.00x
▲ Vercel Next.js (Turbopack) 37.668s (+17.3% 🔺) 39.241s (+16.2% 🔺) 1.572s 10 4.77x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 0.179s 1.001s 0.012s 1.018s 0.839s 10 1.00x
🌐 Redis Next.js (Turbopack) 0.189s 1.000s 0.002s 1.008s 0.819s 10 1.06x
🐘 Postgres Next.js (Turbopack) 0.200s 1.001s 0.001s 1.012s 0.811s 10 1.12x
💻 Local Express 0.206s (~) 1.003s (~) 0.012s (-2.5%) 1.017s (~) 0.812s 10 1.15x
🐘 Postgres Nitro 0.217s (-9.7% 🟢) 0.996s (~) 0.002s (-5.6% 🟢) 1.013s (~) 0.796s 10 1.21x
🐘 Postgres Express 0.219s (-1.8%) 0.995s (~) 0.002s (+38.5% 🔺) 1.013s (~) 0.794s 10 1.22x
💻 Local Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.723s (+11.4% 🔺) 3.078s (+17.8% 🔺) 0.333s (-27.2% 🟢) 4.065s (+13.1% 🔺) 2.343s 10 1.00x
▲ Vercel Next.js (Turbopack) 1.907s (+16.6% 🔺) 3.027s (+3.7%) 0.594s (+48.8% 🔺) 4.228s (+9.1% 🔺) 2.322s 10 1.11x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 0.502s 1.000s 0.003s 1.012s 0.511s 60 1.00x
💻 Local Next.js (Turbopack) 0.649s 1.008s 0.009s 1.022s 0.373s 59 1.29x
🐘 Postgres Nitro 0.687s (-5.2% 🟢) 1.006s (~) 0.006s (+6.6% 🔺) 1.030s (~) 0.343s 59 1.37x
🐘 Postgres Next.js (Turbopack) 0.696s 1.009s 0.008s 1.033s 0.337s 59 1.39x
🐘 Postgres Express 0.700s (-0.5%) 1.023s (+1.9%) 0.004s (-39.1% 🟢) 1.044s (+1.1%) 0.344s 59 1.39x
💻 Local Express 0.721s (~) 1.009s (~) 0.009s (-1.7%) 1.022s (~) 0.301s 59 1.44x
💻 Local Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.101s (-23.2% 🟢) 5.333s (-13.2% 🟢) 0.177s (-71.9% 🟢) 6.131s (-18.7% 🟢) 2.031s 10 1.00x
▲ Vercel Next.js (Turbopack) 4.808s (+5.3% 🔺) 6.443s (+15.3% 🔺) 0.209s (-6.9% 🟢) 7.346s (+13.7% 🔺) 2.538s 9 1.17x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 0.912s 1.016s 0.000s 1.021s 0.109s 59 1.00x
🐘 Postgres Nitro 1.066s (-8.9% 🟢) 1.738s (-12.9% 🟢) 0.000s (-100.0% 🟢) 1.755s (-13.3% 🟢) 0.689s 35 1.17x
🐘 Postgres Express 1.084s (-3.4%) 1.787s (~) 0.000s (-75.0% 🟢) 1.805s (~) 0.721s 34 1.19x
🐘 Postgres Next.js (Turbopack) 1.169s 1.969s 0.000s 1.987s 0.817s 31 1.28x
💻 Local Express 1.213s (+0.8%) 2.019s (~) 0.000s (+100.0% 🔺) 2.022s (~) 0.809s 30 1.33x
💻 Local Next.js (Turbopack) 1.215s 2.018s 0.000s 2.022s 0.806s 30 1.33x
💻 Local Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.018s (~) 4.089s (+4.2%) 0.000s (-46.2% 🟢) 4.803s (+8.5% 🔺) 1.785s 13 1.00x
▲ Vercel Next.js (Turbopack) 3.792s (+7.7% 🔺) 4.745s (+2.0%) 0.019s (+Infinity% 🔺) 5.555s (+6.7% 🔺) 1.764s 11 1.26x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.612s 2.000s 0.000s 2.006s 0.394s 30 1.00x
🐘 Postgres Nitro 2.077s (-8.3% 🟢) 2.608s (-11.7% 🟢) 0.000s (-100.0% 🟢) 2.623s (-11.7% 🟢) 0.546s 23 1.29x
🐘 Postgres Express 2.100s (+2.0%) 2.579s (+4.1%) 0.000s (+Infinity% 🔺) 2.596s (+3.9%) 0.496s 24 1.30x
🐘 Postgres Next.js (Turbopack) 2.323s 3.056s 0.000s 3.065s 0.742s 20 1.44x
💻 Local Next.js (Turbopack) 3.437s 4.030s 0.000s 4.035s 0.597s 15 2.13x
💻 Local Express 3.651s (+7.7% 🔺) 4.164s (+1.6%) 0.000s (+200.0% 🔺) 4.168s (+1.6%) 0.517s 15 2.26x
💻 Local Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.368s (-2.4%) 5.278s (-2.6%) 0.001s (+450.0% 🔺) 6.039s (+1.5%) 1.671s 10 1.00x
▲ Vercel Nitro 6.806s (+96.6% 🔺) 7.760s (+85.1% 🔺) 0.000s (+Infinity% 🔺) 8.573s (+80.6% 🔺) 1.767s 7 1.56x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 12/21
🐘 Postgres Next.js (Turbopack) 13/21
▲ Vercel Nitro 16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 18/21
Next.js (Turbopack) 🌐 Redis 10/21
Nitro 🐘 Postgres 19/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: cancelled
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via globalThis[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.

TooTallNate added a commit to vercel/sandbox that referenced this pull request Mar 22, 2026
…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).
Copy link
Collaborator

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM aside from nits that Pranay mentioned

Copy link
Collaborator

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@TooTallNate
Copy link
Member Author

Re: @pranaygp's review comments —

Stale docstring in class-serialization.ts: Fixed in cb61bee. Updated to note that the SWC plugin now inlines equivalent logic and the function is retained for programmatic use and testing. Re: updating tests to exercise the inline IIFE pattern — the SWC fixture tests already validate the generated IIFE output, so the existing serialization.test.ts tests that use registerSerializationClass() directly still serve as a unit test for the registry mechanics.

Moving to @workflow/serde/class-serialization: That's a reasonable alternative. The tradeoff: importing from @workflow/serde would work for 3rd-party packages (they already depend on it for the symbols), and you'd get module-level deduplication. However, the inline approach has zero module dependencies in the generated code — no resolution needed at all, which is maximally robust across package managers, bundlers, and module formats. For a follow-up, the per-module hoisted helper (mentioned in the thread comments) would address the duplication concern while keeping the zero-dependency property.

@TooTallNate TooTallNate merged commit 7dcddb5 into main Mar 23, 2026
99 of 102 checks passed
@TooTallNate TooTallNate deleted the nrajlich/inline-class-serialization-registration branch March 23, 2026 23:06
pranaygp added a commit that referenced this pull request Mar 23, 2026
…rovements

* origin/main:
  [world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
  Inline class serialization registration to fix 3rd-party package support (#1480)

# Conflicts:
#	pnpm-lock.yaml
pranaygp added a commit that referenced this pull request Mar 24, 2026
The revert of #1475 accidentally removed the __builtin special case
in generate_step_id that was present on main (added by #1480). This
restores it so __builtin_response_* functions get stable IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 24, 2026
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>
pranaygp added a commit that referenced this pull request Mar 24, 2026
…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)
  ...
pranaygp added a commit that referenced this pull request Mar 24, 2026
…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
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…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
TooTallNate added a commit that referenced this pull request Mar 24, 2026
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.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…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.
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.

4 participants