Skip to content

fix(swc-plugin): preserve imports referenced by step function bodies in client mode#1267

Closed
julianbenegas wants to merge 9 commits into
mainfrom
jb/fix-client-mode-import-stripping
Closed

fix(swc-plugin): preserve imports referenced by step function bodies in client mode#1267
julianbenegas wants to merge 9 commits into
mainfrom
jb/fix-client-mode-import-stripping

Conversation

@julianbenegas
Copy link
Copy Markdown
Contributor

@julianbenegas julianbenegas commented Mar 5, 2026

Summary

  • Fix dead code elimination in the SWC plugin's client mode to preserve imports and local declarations referenced by "use step" function bodies. Previously, the usage collector unconditionally skipped step function bodies in all modes, treating them as replaced — but in client mode, step bodies are kept intact and may be called directly from server-side code (e.g. route handlers). This caused ReferenceError at runtime for any import only referenced inside a step body.
  • Add a skip_step_bodies flag to the ComprehensiveUsageCollector, set to true only in workflow mode (where step bodies are actually replaced with stubs).
  • Add e2e test coverage for calling a step function with external imports directly from a route handler.

Test plan

  • All 126 SWC plugin fixture tests pass (3 updated outputs + 1 new fixture)
  • All 451 core unit tests pass
  • e2e test stepDirectCallWithImports passes against nextjs-turbopack dev server
  • Verify no regressions in existing workflow/step/client mode transforms

Made with Cursor

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 5, 2026

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: 8fd78e0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@workflow/swc-plugin Patch
@workflow/astro Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/nest Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
workflow Patch
@workflow/vite Patch
@workflow/vitest Patch
@workflow/world-testing Patch
@workflow/nuxt Patch
@workflow/core Patch
@workflow/web-shared Patch

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

github-actions Bot commented Mar 5, 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.032s (-3.0%) 1.005s (~) 0.973s 10 1.00x
💻 Local Nitro 0.035s (+7.4% 🔺) 1.006s (~) 0.971s 10 1.09x
🐘 Postgres Nitro 0.052s (-20.3% 🟢) 1.011s (-0.7%) 0.959s 10 1.63x
🐘 Postgres Express 0.056s (+7.1% 🔺) 1.013s (~) 0.957s 10 1.74x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.976s (+59.8% 🔺) 4.099s (+74.2% 🔺) 3.123s 10 1.00x
▲ Vercel Express 1.271s (+58.0% 🔺) 3.514s (+44.6% 🔺) 2.244s 10 1.30x

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.100s (-0.5%) 2.005s (~) 0.906s 10 1.00x
💻 Local Nitro 1.105s (~) 2.005s (~) 0.900s 10 1.01x
🐘 Postgres Nitro 1.137s (-0.7%) 2.012s (~) 0.875s 10 1.03x
🐘 Postgres Express 1.147s (+2.2%) 2.013s (~) 0.866s 10 1.04x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.476s (~) 5.014s (+7.0% 🔺) 2.539s 10 1.00x
▲ Vercel Express 2.554s (+12.3% 🔺) 5.143s (+20.4% 🔺) 2.589s 10 1.03x

🔍 Observability: Nitro | Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 10.775s (~) 11.022s (~) 0.247s 3 1.00x
💻 Local Nitro 10.798s (~) 11.022s (~) 0.225s 3 1.00x
🐘 Postgres Nitro 10.800s (-1.1%) 11.042s (~) 0.242s 3 1.00x
🐘 Postgres Express 10.897s (~) 11.044s (~) 0.147s 3 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 16.852s (+1.3%) 19.788s (+4.9%) 2.936s 2 1.00x
▲ Vercel Nitro 17.968s (+5.7% 🔺) 21.443s (+14.5% 🔺) 3.475s 2 1.07x

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 26.920s (~) 27.065s (-2.4%) 0.145s 3 1.00x
🐘 Postgres Express 27.002s (~) 27.068s (~) 0.066s 3 1.00x
💻 Local Express 27.159s (~) 28.056s (~) 0.897s 3 1.01x
💻 Local Nitro 27.231s (~) 28.053s (~) 0.822s 3 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 45.346s (+2.0%) 48.232s (+5.2% 🔺) 2.886s 2 1.00x
▲ Vercel Nitro 46.516s (+4.1%) 49.943s (+7.7% 🔺) 3.427s 2 1.03x

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 53.715s (~) 54.105s (~) 0.390s 2 1.00x
🐘 Postgres Express 54.072s (+0.6%) 54.602s (+0.9%) 0.530s 2 1.01x
💻 Local Express 55.878s (~) 56.096s (-1.8%) 0.218s 2 1.04x
💻 Local Nitro 56.191s (+0.6%) 57.106s (+1.8%) 0.915s 2 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 96.289s (~) 98.990s (~) 2.701s 1 1.00x
▲ Vercel Express 97.893s (+2.5%) 100.080s (+3.2%) 2.187s 1 1.02x

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.361s (~) 2.011s (~) 0.650s 15 1.00x
🐘 Postgres Express 1.378s (+1.2%) 2.012s (~) 0.634s 15 1.01x
💻 Local Express 1.393s (-1.9%) 2.005s (~) 0.612s 15 1.02x
💻 Local Nitro 1.423s (~) 2.005s (~) 0.582s 15 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.689s (-13.4% 🟢) 5.487s (-8.7% 🟢) 2.797s 6 1.00x
▲ Vercel Nitro 2.903s (-8.7% 🟢) 5.247s (-18.7% 🟢) 2.344s 6 1.08x

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.952s (-9.1% 🟢) 2.402s (-4.5%) 0.450s 13 1.00x
🐘 Postgres Nitro 1.960s (+0.5%) 2.512s (~) 0.552s 12 1.00x
💻 Local Express 2.579s (-4.1%) 3.008s (~) 0.429s 10 1.32x
💻 Local Nitro 2.625s (+0.5%) 3.007s (~) 0.382s 10 1.34x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.025s (+5.4% 🔺) 5.578s (+14.0% 🔺) 2.553s 6 1.00x
▲ Vercel Nitro 3.410s (+23.7% 🔺) 5.751s (+5.6% 🔺) 2.341s 6 1.13x

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.525s (-12.5% 🟢) 4.306s (-6.3% 🟢) 0.781s 7 1.00x
🐘 Postgres Nitro 3.746s (-10.7% 🟢) 4.591s (-6.1% 🟢) 0.845s 7 1.06x
💻 Local Express 7.415s (-3.8%) 8.021s (~) 0.606s 4 2.10x
💻 Local Nitro 8.012s (+8.9% 🔺) 8.271s (+3.2%) 0.258s 4 2.27x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.724s (~) 6.201s (-8.0% 🟢) 2.477s 5 1.00x
▲ Vercel Express 4.070s (+33.3% 🔺) 6.747s (+18.0% 🔺) 2.678s 5 1.09x

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.392s (+1.4%) 2.011s (~) 0.619s 15 1.00x
🐘 Postgres Express 1.422s (+2.2%) 2.013s (~) 0.590s 15 1.02x
💻 Local Express 1.440s (~) 2.006s (~) 0.566s 15 1.03x
💻 Local Nitro 1.471s (+3.5%) 2.006s (~) 0.535s 15 1.06x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.422s (+5.6% 🔺) 4.371s (-15.3% 🟢) 1.949s 7 1.00x
▲ Vercel Nitro 2.575s (~) 4.540s (-11.5% 🟢) 1.964s 7 1.06x

🔍 Observability: Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.937s (-3.9%) 2.600s (~) 0.663s 12 1.00x
🐘 Postgres Express 2.065s (-3.2%) 2.742s (~) 0.678s 11 1.07x
💻 Local Express 2.699s (-2.2%) 3.007s (~) 0.308s 10 1.39x
💻 Local Nitro 2.773s (+3.0%) 3.010s (~) 0.237s 10 1.43x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.678s (-22.2% 🟢) 4.717s (-23.3% 🟢) 2.039s 7 1.00x
▲ Vercel Nitro 2.925s (~) 5.351s (-12.0% 🟢) 2.426s 6 1.09x

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.500s (-12.7% 🟢) 4.022s (-12.5% 🟢) 0.522s 8 1.00x
🐘 Postgres Express 3.992s (+13.9% 🔺) 4.604s (+3.2%) 0.612s 7 1.14x
💻 Local Express 7.881s (-4.1%) 8.018s (-11.1% 🟢) 0.137s 4 2.25x
💻 Local Nitro 8.315s (+4.1%) 9.021s (+5.8% 🔺) 0.706s 4 2.38x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.418s (+10.0% 🔺) 4.704s (-26.6% 🟢) 1.286s 7 1.00x
▲ Vercel Nitro 3.527s (+7.2% 🔺) 5.462s (-21.6% 🟢) 1.935s 6 1.03x

🔍 Observability: Express | Nitro

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.170s (-0.6%) 1.003s (~) 0.011s (-6.1% 🟢) 1.016s (~) 0.847s 10 1.00x
💻 Local Nitro 0.173s (+3.3%) 1.003s (~) 0.011s (+1.8%) 1.017s (~) 0.844s 10 1.02x
🐘 Postgres Express 0.198s (+5.3% 🔺) 0.997s (~) 0.002s (+33.3% 🔺) 1.013s (~) 0.816s 10 1.17x
🐘 Postgres Nitro 0.201s (~) 0.992s (-0.5%) 0.001s (+7.7% 🔺) 1.012s (~) 0.811s 10 1.19x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.168s (+17.4% 🔺) 3.662s (+23.7% 🔺) 0.101s (+52.4% 🔺) 4.823s (+25.5% 🔺) 2.656s 10 1.00x
▲ Vercel Nitro 2.440s (+26.2% 🔺) 3.652s (+27.6% 🔺) 0.120s (+74.4% 🔺) 5.163s (+24.5% 🔺) 2.724s 10 1.13x

🔍 Observability: Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Express 12/12
🐘 Postgres Nitro 9/12
▲ Vercel Express 8/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 7/12
Nitro 🐘 Postgres 7/12
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: failure
  • Postgres: failure
  • Vercel: failure

Check the workflow run for details.

⚠️ Community world benchmarks failed (non-blocking):

  • Community Worlds: failure

Check the workflow run for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 5, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 346 54 56 456
✅ 💻 Local Development 336 0 63 399
✅ 📦 Local Production 336 0 63 399
✅ 🐘 Local Postgres 336 0 63 399
❌ 🌍 Community Worlds 9 54 9 72
❌ 📋 Other 141 3 27 171
Total 1504 111 281 1896

❌ Failed Tests

▲ Vercel Production (54 failed)

astro (4 failed):

  • parallelSleepWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior regular Error retries until success

example (9 failed):

  • sleepingWorkflow
  • parallelSleepWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • stepDirectCallWithImports - step function with external imports called directly
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running

express (7 failed):

  • sleepingWorkflow
  • parallelSleepWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running

fastify (6 failed):

  • sleepingWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running

hono (6 failed):

  • sleepingWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running

nitro (7 failed):

  • sleepingWorkflow
  • parallelSleepWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

sveltekit (8 failed):

  • parallelSleepWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running

vite (7 failed):

  • parallelSleepWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
🌍 Community Worlds (54 failed)

turso (54 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • 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 retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • stepDirectCallWithImports - step function with external imports called directly
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint (HTTP) - workflow and step endpoints respond to __health query parameter
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
📋 Other (3 failed)

e2e-local-dev-nest-stable (1 failed):

  • stepDirectCallWithImports - step function with external imports called directly

e2e-local-postgres-nest-stable (1 failed):

  • stepDirectCallWithImports - step function with external imports called directly

e2e-local-prod-nest-stable (1 failed):

  • stepDirectCallWithImports - step function with external imports called directly

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
❌ astro 46 4 7
❌ example 41 9 7
❌ express 43 7 7
❌ fastify 44 6 7
❌ hono 44 6 7
❌ nitro 43 7 7
❌ sveltekit 42 8 7
❌ vite 43 7 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 48 0 9
✅ express-stable 48 0 9
✅ fastify-stable 48 0 9
✅ hono-stable 48 0 9
✅ nitro-stable 48 0 9
✅ sveltekit-stable 48 0 9
✅ vite-stable 48 0 9
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 48 0 9
✅ express-stable 48 0 9
✅ fastify-stable 48 0 9
✅ hono-stable 48 0 9
✅ nitro-stable 48 0 9
✅ sveltekit-stable 48 0 9
✅ vite-stable 48 0 9
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 48 0 9
✅ express-stable 48 0 9
✅ fastify-stable 48 0 9
✅ hono-stable 48 0 9
✅ nitro-stable 48 0 9
✅ sveltekit-stable 48 0 9
✅ vite-stable 48 0 9
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 2
✅ redis-dev 3 0 2
✅ turso-dev 3 0 2
❌ turso 0 54 3
❌ 📋 Other
App Passed Failed Skipped
❌ e2e-local-dev-nest-stable 47 1 9
❌ e2e-local-postgres-nest-stable 47 1 9
❌ e2e-local-prod-nest-stable 47 1 9

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: failure
  • Local Postgres: failure
  • Windows: cancelled

Check the workflow run for details.

…in client mode

The SWC plugin's dead code elimination unconditionally skipped step function
bodies during usage analysis in all modes. This is correct for workflow mode
(where step bodies are replaced with stubs) but wrong for client mode (where
step bodies are kept intact). Imports referenced only inside step function
bodies were considered "unused" and stripped, causing ReferenceError when step
functions were called directly from server-side code (e.g. route handlers).

The fix adds a mode-aware `skip_step_bodies` flag to the usage collector,
set to true only in workflow mode.

Made-with: Cursor
The stepDirectCallWithImports e2e test needs this endpoint in every
framework, not just Next.js. Adds the route and symlinks the workflow
files (_direct_call_step.ts, _direct_call_helper.ts) into nitro-v3
and sveltekit (which feed the other framework workbenches via
directory symlinks).

Made-with: Cursor
julianbenegas and others added 3 commits March 5, 2026 23:48
The client-mode transform now correctly preserves imports referenced
by step function bodies. This means the './serde-models.js' import
survives into the Turbopack build, which can't resolve .js extensions
through symlinks. Drop the extension so Turbopack resolves it properly.

Made-with: Cursor
pranaygp
pranaygp previously approved these changes Mar 6, 2026
Copy link
Copy Markdown
Contributor

@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.

Good fix. The root cause is well-understood: ComprehensiveUsageCollector was unconditionally skipping step function bodies during usage analysis, but in client mode those bodies are preserved intact — so their imports are needed at runtime.

The fix is clean and targeted:

  • skip_step_bodies flag correctly scoped to workflow mode only
  • Hoisted step function bodies (object property + nested) are analyzed before they're inserted into the module, avoiding the timing gap with dead code elimination
  • stepId assignments for hoisted functions are now inserted inline (right after the declaration) instead of deferred to registration_calls — this is a nice improvement for readability and correctness of ordering

All the fixture output changes are consistent with the new behavior. Workflow-mode outputs are unaffected (step-only imports still correctly stripped). Test coverage is thorough: 2 new fixtures, updated expectations for 6 existing fixtures, and an e2e test propagated across all workbenches.

@pranaygp
Copy link
Copy Markdown
Contributor

pranaygp commented Mar 6, 2026

@TooTallNate The e2e failures on CI (nextjs-turbopack, nextjs-webpack, nuxt, nest) are caused by this fix being too broad in what it preserves in client mode.

What's happening: The ComprehensiveUsageCollector now analyzes step function bodies in client mode, which correctly preserves imports those bodies reference. But workflows/99_e2e.ts has step functions that call start() and getRun() from workflow/api (lines 669-682):

// Step function that spawns another workflow using start()
async function spawnChildWorkflow(value: number) {
  'use step';
  const childRun = await start(childWorkflow, [value]);
  return childRun.runId;
}

async function awaitWorkflowResult<T>(runId: string) {
  'use step';
  const run = getRun<T>(runId);
  // ...
}

Previously, start and getRun were (incorrectly) stripped as dead code because step bodies were skipped in all modes. Now they're preserved, which pulls the full server runtime into the client bundle:

99_e2e.ts [Client Component Browser]
  → workflow/api (start, getRun)
    → @workflow/core/runtime/start.js
      → @workflow/core/runtime/world.js
        → @workflow/world-vercel
          → @vercel/queue (imports fs, net, path) 💥

Non-Next.js workbenches (express, fastify, hono, etc.) pass because they're server-only and don't bundle for the browser.

The tension: The fix correctly preserves step-body imports for the direct-call case (route handler calling a step function). But it also preserves server-only imports like workflow/api that can't be resolved in a browser bundle. The old behavior was wrong (ReferenceError) but sidestepped this bundling problem.

The fix likely needs to be more targeted — e.g., always stripping known server-only modules (workflow/api, workflow/internal/private) in client mode regardless of step body references, or only preserving relative imports from step bodies.

@TooTallNate
Copy link
Copy Markdown
Member

Superseded by #1312.

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.

3 participants