') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Inline class serialization registration to fix 3rd-party package support by TooTallNate · Pull Request #1480 · vercel/workflow · GitHub
Skip to content

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

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

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

Conversation

@TooTallNate

Copy link
Copy Markdown
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]=newMap());__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.
CopilotAI review requested due to automatic review settings March 22, 2026 20:00
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 22, 2026 20:00
@vercel

vercelBot commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Mar 22, 2026

Copy link
Copy Markdown

🦋 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

github-actionsBot commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ 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
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.042s (-7.8% 🟢)1.006s (~)0.964s101.00x
💻 LocalNext.js (Turbopack)0.049s1.005s0.957s101.15x
🌐 RedisNext.js (Turbopack)0.054s1.007s0.952s101.29x
🐘 PostgresNext.js (Turbopack)0.060s1.011s0.952s101.41x
🐘 PostgresNitro0.060s (-13.4% 🟢)1.012s (~)0.952s101.42x
🐘 PostgresExpress0.062s (+11.7% 🔺)1.011s (~)0.949s101.46x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.445s (-23.7% 🟢)2.437s (+14.4% 🔺)1.991s101.00x
▲ VercelNext.js (Turbopack)0.473s (-28.4% 🟢)2.606s (+9.0% 🔺)2.132s101.06x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.120s2.007s0.887s101.00x
💻 LocalExpress1.124s (~)2.006s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)1.124s2.005s0.882s101.00x
🐘 PostgresNext.js (Turbopack)1.135s2.013s0.878s101.01x
🐘 PostgresExpress1.151s (~)2.012s (~)0.861s101.03x
🐘 PostgresNitro1.161s (~)2.012s (~)0.851s101.04x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.031s (-2.6%)3.660s (+8.6% 🔺)1.629s101.00x
▲ VercelNext.js (Turbopack)2.080s (+0.9%)3.705s (+1.1%)1.625s101.02x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.748s11.022s0.274s31.00x
💻 LocalNext.js (Turbopack)10.754s11.023s0.270s31.00x
🐘 PostgresExpress10.895s (~)11.038s (~)0.143s31.01x
💻 LocalExpress10.905s (-0.5%)11.021s (~)0.116s31.01x
🐘 PostgresNitro10.929s (-1.9%)11.043s (-8.4% 🟢)0.114s31.02x
🐘 PostgresNext.js (Turbopack)10.940s11.046s0.106s31.02x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)17.281s (~)19.115s (~)1.834s21.00x
▲ VercelNitro17.340s (-3.1%)18.742s (-4.5%)1.402s21.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.247s15.029s0.781s41.00x
🐘 PostgresNext.js (Turbopack)14.563s15.044s0.481s41.02x
🐘 PostgresExpress14.586s (-1.6%)15.040s (~)0.454s41.02x
💻 LocalNext.js (Turbopack)14.614s15.030s0.416s41.03x
🐘 PostgresNitro14.715s (-1.2%)15.044s (~)0.329s41.03x
💻 LocalExpress14.916s (-0.8%)15.030s (-4.8%)0.114s41.05x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)34.234s (+1.8%)36.289s (+4.2%)2.055s21.00x
▲ VercelNitro34.818s (+6.5% 🔺)36.646s (+7.6% 🔺)1.828s21.02x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.485s14.027s0.542s71.00x
🐘 PostgresNext.js (Turbopack)13.985s14.324s0.339s71.04x
🐘 PostgresExpress14.218s (-1.5%)15.037s (~)0.819s61.05x
🐘 PostgresNitro14.272s (-3.9%)15.042s (-1.1%)0.770s61.06x
💻 LocalNext.js (Turbopack)16.071s16.696s0.625s61.19x
💻 LocalExpress16.568s (-2.7%)17.029s (-1.9%)0.461s61.23x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro59.376s (-3.8%)60.985s (-3.0%)1.609s21.00x
▲ VercelNext.js (Turbopack)64.849s (+5.9% 🔺)67.087s (+7.3% 🔺)2.239s21.09x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.251s2.011s0.760s151.00x
🐘 PostgresNitro1.285s (-1.2%)2.011s (~)0.726s151.03x
🐘 PostgresExpress1.298s (+1.2%)2.011s (~)0.713s151.04x
🌐 RedisNext.js (Turbopack)1.323s2.007s0.683s151.06x
💻 LocalExpress1.491s (-2.6%)2.005s (~)0.514s151.19x
💻 LocalNext.js (Turbopack)1.538s2.006s0.468s151.23x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.535s (-2.2%)3.958s (-3.4%)1.423s81.00x
▲ VercelNitro2.864s (+13.4% 🔺)4.299s (+12.0% 🔺)1.436s71.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.449s (~)3.012s (~)0.562s101.00x
🐘 PostgresNext.js (Turbopack)2.469s3.011s0.542s101.01x
🐘 PostgresNitro2.480s (-0.5%)3.012s (~)0.532s101.01x
🌐 RedisNext.js (Turbopack)2.575s3.008s0.433s101.05x
💻 LocalExpress2.900s (-6.4% 🟢)3.108s (-22.5% 🟢)0.208s101.18x
💻 LocalNext.js (Turbopack)2.952s3.454s0.502s91.21x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.269s (+25.5% 🔺)4.828s (+31.6% 🔺)1.559s71.00x
▲ VercelNext.js (Turbopack)3.523s (+20.0% 🔺)5.320s (+24.1% 🔺)1.797s61.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.609s (+1.1%)4.013s (~)0.404s81.00x
🐘 PostgresNitro3.637s (-0.8%)4.014s (~)0.377s81.01x
🐘 PostgresNext.js (Turbopack)3.855s4.139s0.285s81.07x
🌐 RedisNext.js (Turbopack)4.197s5.011s0.814s61.16x
💻 LocalExpress8.162s (-6.4% 🟢)9.020s (-2.7%)0.858s42.26x
💻 LocalNext.js (Turbopack)8.210s8.770s0.561s42.27x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.141s (-7.3% 🟢)4.745s (-1.0%)1.605s71.00x
▲ VercelNext.js (Turbopack)4.035s (-3.1%)5.706s (+2.7%)1.672s61.28x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.243s2.011s0.768s151.00x
🐘 PostgresNitro1.264s (-2.1%)2.011s (~)0.747s151.02x
🐘 PostgresExpress1.302s (+0.9%)2.012s (~)0.710s151.05x
🌐 RedisNext.js (Turbopack)1.327s2.006s0.679s151.07x
💻 LocalExpress1.503s (-2.6%)2.006s (~)0.502s151.21x
💻 LocalNext.js (Turbopack)1.530s2.006s0.475s151.23x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.545s (+10.3% 🔺)4.135s (+10.0% 🔺)1.590s81.00x
▲ VercelNext.js (Turbopack)2.877s (+11.3% 🔺)4.500s (+19.0% 🔺)1.623s71.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.445s3.011s0.566s101.00x
🐘 PostgresExpress2.450s (~)3.012s (~)0.561s101.00x
🐘 PostgresNitro2.462s (-1.3%)3.011s (~)0.549s101.01x
🌐 RedisNext.js (Turbopack)2.566s3.008s0.442s101.05x
💻 LocalExpress2.934s (-7.5% 🟢)3.759s (-6.3% 🟢)0.824s81.20x
💻 LocalNext.js (Turbopack)2.976s3.563s0.587s91.22x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.533s (-11.9% 🟢)4.008s (-6.9% 🟢)1.475s81.00x
▲ VercelNext.js (Turbopack)2.780s (-7.2% 🟢)4.222s (-1.6%)1.442s81.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.589s (-1.6%)4.013s (~)0.424s81.00x
🐘 PostgresExpress3.610s (~)4.013s (~)0.403s81.01x
🐘 PostgresNext.js (Turbopack)3.751s4.015s0.263s81.05x
🌐 RedisNext.js (Turbopack)4.180s4.725s0.545s71.16x
💻 LocalExpress8.544s (-6.6% 🟢)9.023s (-10.0% 🟢)0.479s42.38x
💻 LocalNext.js (Turbopack)8.722s9.020s0.298s42.43x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.288s (+6.0% 🔺)4.917s (+4.4%)1.630s71.00x
▲ VercelNext.js (Turbopack)3.522s (-1.8%)5.385s (+9.3% 🔺)1.863s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.689s1.005s0.315s601.00x
🐘 PostgresNext.js (Turbopack)0.822s1.009s0.187s601.19x
💻 LocalNext.js (Turbopack)0.863s1.038s0.176s591.25x
🐘 PostgresNitro0.873s (-9.9% 🟢)1.009s (-18.3% 🟢)0.136s601.27x
🐘 PostgresExpress0.888s (-2.9%)1.100s (+1.9%)0.211s551.29x
💻 LocalExpress0.974s (-3.7%)1.113s (-27.9% 🟢)0.140s551.41x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)9.921s (~)11.817s (-4.4%)1.896s61.00x
▲ VercelNitro10.297s (+2.3%)12.033s (+5.3% 🔺)1.736s51.04x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.700s2.028s0.328s451.00x
🐘 PostgresNext.js (Turbopack)1.996s2.285s0.290s401.17x
🐘 PostgresNitro2.128s (-7.9% 🟢)3.012s (~)0.883s301.25x
🐘 PostgresExpress2.149s (-3.4%)3.011s (~)0.862s301.26x
💻 LocalNext.js (Turbopack)2.634s3.007s0.373s301.55x
💻 LocalExpress2.972s (-2.0%)3.258s (-11.7% 🟢)0.286s281.75x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro32.733s (+9.0% 🔺)34.644s (+8.6% 🔺)1.911s31.00x
▲ VercelNext.js (Turbopack)34.015s (+4.9%)35.574s (+5.7% 🔺)1.559s31.04x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.403s4.009s0.606s301.00x
🐘 PostgresNext.js (Turbopack)4.080s4.706s0.626s261.20x
🐘 PostgresNitro4.300s (-10.2% 🟢)5.014s (-1.7%)0.714s241.26x
🐘 PostgresExpress4.354s (-3.1%)5.055s (+0.8%)0.701s241.28x
💻 LocalNext.js (Turbopack)8.435s9.017s0.582s142.48x
💻 LocalExpress9.069s (+3.0%)9.633s (+5.2% 🔺)0.564s132.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro86.802s (+0.7%)88.199s (+1.1%)1.397s21.00x
▲ VercelNext.js (Turbopack)91.081s (+5.8% 🔺)92.971s (+5.1% 🔺)1.891s21.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.269s1.009s0.740s601.00x
🐘 PostgresNitro0.294s (-7.3% 🟢)1.009s (~)0.715s601.09x
🐘 PostgresExpress0.311s (+4.8%)1.009s (~)0.699s601.15x
🌐 RedisNext.js (Turbopack)0.430s1.021s0.591s591.60x
💻 LocalNext.js (Turbopack)0.562s1.021s0.459s592.09x
💻 LocalExpress0.607s (+6.7% 🔺)1.005s (~)0.398s602.26x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.594s (-5.1% 🟢)3.351s (+1.5%)1.757s181.00x
▲ VercelNext.js (Turbopack)2.095s (+10.7% 🔺)3.941s (+5.1% 🔺)1.847s161.31x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.530s (-9.2% 🟢)1.010s (~)0.480s901.00x
🐘 PostgresNext.js (Turbopack)0.550s1.010s0.459s901.04x
🐘 PostgresExpress0.554s (+1.2%)1.010s (~)0.455s901.05x
🌐 RedisNext.js (Turbopack)1.209s2.006s0.797s452.28x
💻 LocalNext.js (Turbopack)2.484s3.008s0.524s304.69x
💻 LocalExpress2.503s (+2.1%)3.008s (~)0.505s304.73x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.237s (+7.8% 🔺)4.812s (+8.2% 🔺)1.575s191.00x
▲ VercelNext.js (Turbopack)3.512s (+8.3% 🔺)5.160s (+9.2% 🔺)1.647s181.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.912s1.125s0.212s1071.00x
🐘 PostgresNitro0.921s (-4.7%)1.203s (-14.0% 🟢)0.282s1001.01x
🐘 PostgresExpress0.947s (+3.0%)1.306s (+5.3% 🔺)0.359s931.04x
🌐 RedisNext.js (Turbopack)2.876s3.137s0.260s393.15x
💻 LocalNext.js (Turbopack)10.349s10.931s0.582s1111.34x
💻 LocalExpress11.134s (+1.1%)11.844s (+1.5%)0.710s1112.21x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.896s (+9.2% 🔺)9.588s (+10.5% 🔺)1.692s131.00x
▲ VercelNext.js (Turbopack)37.668s (+17.3% 🔺)39.241s (+16.2% 🔺)1.572s104.77x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.179s1.001s0.012s1.018s0.839s101.00x
🌐 RedisNext.js (Turbopack)0.189s1.000s0.002s1.008s0.819s101.06x
🐘 PostgresNext.js (Turbopack)0.200s1.001s0.001s1.012s0.811s101.12x
💻 LocalExpress0.206s (~)1.003s (~)0.012s (-2.5%)1.017s (~)0.812s101.15x
🐘 PostgresNitro0.217s (-9.7% 🟢)0.996s (~)0.002s (-5.6% 🟢)1.013s (~)0.796s101.21x
🐘 PostgresExpress0.219s (-1.8%)0.995s (~)0.002s (+38.5% 🔺)1.013s (~)0.794s101.22x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.723s (+11.4% 🔺)3.078s (+17.8% 🔺)0.333s (-27.2% 🟢)4.065s (+13.1% 🔺)2.343s101.00x
▲ VercelNext.js (Turbopack)1.907s (+16.6% 🔺)3.027s (+3.7%)0.594s (+48.8% 🔺)4.228s (+9.1% 🔺)2.322s101.11x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.502s1.000s0.003s1.012s0.511s601.00x
💻 LocalNext.js (Turbopack)0.649s1.008s0.009s1.022s0.373s591.29x
🐘 PostgresNitro0.687s (-5.2% 🟢)1.006s (~)0.006s (+6.6% 🔺)1.030s (~)0.343s591.37x
🐘 PostgresNext.js (Turbopack)0.696s1.009s0.008s1.033s0.337s591.39x
🐘 PostgresExpress0.700s (-0.5%)1.023s (+1.9%)0.004s (-39.1% 🟢)1.044s (+1.1%)0.344s591.39x
💻 LocalExpress0.721s (~)1.009s (~)0.009s (-1.7%)1.022s (~)0.301s591.44x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.101s (-23.2% 🟢)5.333s (-13.2% 🟢)0.177s (-71.9% 🟢)6.131s (-18.7% 🟢)2.031s101.00x
▲ VercelNext.js (Turbopack)4.808s (+5.3% 🔺)6.443s (+15.3% 🔺)0.209s (-6.9% 🟢)7.346s (+13.7% 🔺)2.538s91.17x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.912s1.016s0.000s1.021s0.109s591.00x
🐘 PostgresNitro1.066s (-8.9% 🟢)1.738s (-12.9% 🟢)0.000s (-100.0% 🟢)1.755s (-13.3% 🟢)0.689s351.17x
🐘 PostgresExpress1.084s (-3.4%)1.787s (~)0.000s (-75.0% 🟢)1.805s (~)0.721s341.19x
🐘 PostgresNext.js (Turbopack)1.169s1.969s0.000s1.987s0.817s311.28x
💻 LocalExpress1.213s (+0.8%)2.019s (~)0.000s (+100.0% 🔺)2.022s (~)0.809s301.33x
💻 LocalNext.js (Turbopack)1.215s2.018s0.000s2.022s0.806s301.33x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.018s (~)4.089s (+4.2%)0.000s (-46.2% 🟢)4.803s (+8.5% 🔺)1.785s131.00x
▲ VercelNext.js (Turbopack)3.792s (+7.7% 🔺)4.745s (+2.0%)0.019s (+Infinity% 🔺)5.555s (+6.7% 🔺)1.764s111.26x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.612s2.000s0.000s2.006s0.394s301.00x
🐘 PostgresNitro2.077s (-8.3% 🟢)2.608s (-11.7% 🟢)0.000s (-100.0% 🟢)2.623s (-11.7% 🟢)0.546s231.29x
🐘 PostgresExpress2.100s (+2.0%)2.579s (+4.1%)0.000s (+Infinity% 🔺)2.596s (+3.9%)0.496s241.30x
🐘 PostgresNext.js (Turbopack)2.323s3.056s0.000s3.065s0.742s201.44x
💻 LocalNext.js (Turbopack)3.437s4.030s0.000s4.035s0.597s152.13x
💻 LocalExpress3.651s (+7.7% 🔺)4.164s (+1.6%)0.000s (+200.0% 🔺)4.168s (+1.6%)0.517s152.26x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.368s (-2.4%)5.278s (-2.6%)0.001s (+450.0% 🔺)6.039s (+1.5%)1.671s101.00x
▲ VercelNitro6.806s (+96.6% 🔺)7.760s (+85.1% 🔺)0.000s (+Infinity% 🔺)8.573s (+80.6% 🔺)1.767s71.56x
▲ VercelExpress⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)12/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🌐 Redis10/21
Nitro🐘 Postgres19/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.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsGenerates inline, dependency-free class registration IIFE and removes import injection.
packages/swc-plugin-workflow/spec.mdUpdates documentation examples to the new inlined registration pattern.
.changeset/inline-class-serialization.mdPublishes a patch changeset for the SWC plugin behavior change.
packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/static-method-step/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/static-method-step/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/static-method-step/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-local-const/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-local-const/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-local-const/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-imported/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-imported/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-imported/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/fixture/class-expression-binding-name/output-client.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.jsFixture updated to expect inline class registration (no import).
packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.jsFixture updated to expect inline class registration (no import).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs
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).

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

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?

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

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

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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 mainMar 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.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious