Skip to content

[builders] Compile JSX in steps bundles loaded directly at runtime - #5304

Open
joelfickson wants to merge 3 commits into
mainfrom
joel/fix-5286-nitro-dev-jsx
Open

joelfickson wants to merge 3 commits into
mainfrom
joel/fix-5286-nitro-dev-jsx

Conversation

@joelfickson

@joelfickson joelfickson commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Description

When bundleTransitiveLocalStepDependencies is on (Nitro dev, Vitest, NestJS ESM), project-local files are inlined into a steps bundle that is loaded straight from disk, but the bundle kept jsx: 'preserve'. A step that imports a .tsx module failed with Unexpected token '<'. These bundles now compile JSX with esbuild's automatic runtime, which still honors the tsconfig's jsxImportSource. Bundles handed to another bundler keep preserve, so production output is unchanged.

A classic runtime ("jsx": "react" with jsxFactory) is not fixed by this: the SWC pass that runs before esbuild removes the factory binding, so those projects now fail with ReferenceError instead of a syntax error.

Closes #5286

How did you test your changes?

New steps-bundle-jsx.test.ts builds a steps bundle where a step imports a .tsx file, imports it in a fresh Node process and runs the step, once with a stub react JSX runtime and once with a stub preact runtime set through jsxImportSource, plus a control that checks JSX stays preserved when the flag is off. The full @workflow/builders suite passes. A Nitro dev app on workflow 5.4.0 with a packed build of this branch went from a run stuck in pending to completed, and nitro build still succeeds.

PR Checklist - Required to merge

  • pnpm changeset was run to create a changelog for this PR
  • DCO sign-off passes (run git commit --signoff on your commits)

Signed-off-by: Joel Fickson <joel.fickson@vercel.com>
@changeset-bot

changeset-bot Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 439309e

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

This PR includes changesets to release 17 packages
Name Type
@workflow/builders Patch
@workflow/astro Patch
@workflow/cli Patch
@workflow/nest Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/vitest Patch
workflow Patch
@workflow/world-testing Patch
@workflow/nuxt Patch
@workflow/core Patch
@workflow/world-vercel Patch
@workflow/web-shared Patch
@workflow/web 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

@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated
example-nextjs-workflow-turbopack Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
example-nextjs-workflow-webpack Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
example-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-astro-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-express-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-fastify-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-hono-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-nestjs-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-nitro-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-nuxt-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-python-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-sveltekit-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-tanstack-start-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workbench-vite-workflow Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workflow-docs Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workflow-swc-playground Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workflow-tarballs Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC
workflow-web Ready Ready Preview, v0 Nov 7, 2026 6:56pm UTC

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 439309e · Sat, 07 Nov 2026 19:08:30 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1666 (+39%) 🔻 1792 🔴 (+37%) 🔻 1836 🔴 (+35%) 🔻 2084 🔴 (+30%) 🔻 30
TTFS stream 1338 (+56%) 🔻 1726 🔴 (+33%) 🔻 1738 🔴 (+32%) 🔻 1855 🔴 (+35%) 🔻 30
TTFS hook + stream 1849 (+88%) 🔻 2049 🔴 (+34%) 🔻 2133 🔴 (+34%) 🔻 2255 🔴 (+11%) 30
Fan-out TTFS Promise.all(100 steps) 434 (+10%) 688 (+27%) 🔻 799 (-49%) 💚 2045 (+23%) 🔻 10
Fan-out TTLS Promise.all(100 steps) 1716 (-50%) 💚 4079 (-18%) 💚 4131 (-21%) 💚 7590 (+27%) 🔻 10
STSO 1020 steps (inline) 80 (±0%) 98 (-1.0%) 117 (-1.7%) 167 (+5.0%) 1019
WO 1020 steps 98678 (±0%) 98678 (±0%) 98678 (±0%) 98678 (±0%) 1
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 98830ms → this run 98457ms (Δ -373ms, 0%)

 50-100 ms  ███████████████████████┃  main 781  this 799   +18
100-150 ms  █████┃█                   main 223  this 204   -19
150-200 ms  ┃                         main  12  this  11    -1
200-250 ms  ┃                         main   2  this   3    +1
250-300 ms  ┃                         main   0  this   1    +1
300-350 ms  ┃                         main   1  this   0    -1
350-400 ms  ┃                         main   0  this   1    +1
📜 Previous results (1)

f4f0bb4

Sat, 07 Nov 2026 16:13:07 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1915 (+144%) 🔻 2033 🔴 (+55%) 🔻 2067 🔴 (+55%) 🔻 2122 🔴 (+55%) 🔻 30
TTFS stream 326 (-74%) 💚 1789 🔴 (+37%) 🔻 1913 🔴 (+46%) 🔻 2085 🔴 (+51%) 🔻 30
TTFS hook + stream 1630 (+14%) 2040 🔴 (+28%) 🔻 2070 🔴 (+16%) 🔻 2312 🔴 (+9.3%) 30
Fan-out TTFS Promise.all(100 steps) 393 (-12%) 1969 (+187%) 🔻 2055 (+191%) 🔻 2225 (+41%) 🔻 10
Fan-out TTLS Promise.all(100 steps) 1562 (-59%) 💚 4033 (-5.5%) 4059 (-6.0%) 7162 (+64%) 🔻 10
STSO 1020 steps (inline) 79 (-9.2%) 114 (-17%) 💚 132 (-19%) 💚 200 (-17%) 💚 1019
WO 1020 steps 110153 (-20%) 💚 110153 (-20%) 💚 110153 (-20%) 💚 110153 (-20%) 💚 1
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). █ = main, ┃ = this run, ░ = fill.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies. Streaming delivery performance is measured in durabench.

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

✅ All tests passed

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

  • run-pickup-stall · runs app-generated source against a registered step (tanstack-start) · at 18:58:43Z · abandoned wrun_01M6VP0QC17YF21AGXADKZZBYK
  • cold-start-warmup · suite warmup (tanstack-start) · at 18:58:43Z · abandoned wrun_01M6VP0QC4BBD0JJWYQM93MP92
  • run-pickup-stall · thisSerializationWorkflow - step function invoked with .call() and .apply() (nextjs-turbopack) · at 19:01:02Z · abandoned wrun_01M6VP4YT4F40XKP5TVVS2BQ33

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 4619 0 916 5535
✅ 💻 Local Development 5234 0 711 5945
✅ 📦 Local Production 5234 0 711 5945
✅ 🐘 Local Postgres 5379 0 566 5945
✅ 🪟 Windows 397 0 13 410
✅ 🌐 Cross-language Conformance 68 0 112 180
✅ dynamic-runs 0 0 0 0
✅ local-postgres-spec9 0 0 360 360
✅ vercel-http-transport 1047 0 183 1230
✅ vercel-multi-region 27 0 0 27
✅ vercel-spec9 391 0 19 410
✅ vercel-ws-transport 707 0 113 820
Total 23103 0 3704 26807
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 170 0 35
✅ astro-quickjs 169 0 36
✅ example-node 170 0 35
✅ example-quickjs 169 0 36
✅ express-node 170 0 35
✅ express-quickjs 169 0 36
✅ fastify-node 170 0 35
✅ fastify-quickjs 169 0 36
✅ hono-node 170 0 35
✅ hono-quickjs 169 0 36
✅ nest-node 170 0 35
✅ nest-quickjs 169 0 36
✅ nextjs-turbopack-node 197 0 8
✅ nextjs-turbopack-quickjs 196 0 9
✅ nextjs-webpack-node 197 0 8
✅ nextjs-webpack-quickjs 196 0 9
✅ nitro-node 170 0 35
✅ nitro-quickjs 169 0 36
✅ nuxt-node 170 0 35
✅ nuxt-quickjs 169 0 36
✅ python-node 66 0 139
✅ sveltekit-node 189 0 16
✅ sveltekit-quickjs 188 0 17
✅ tanstack-start-node 170 0 35
✅ tanstack-start-quickjs 169 0 36
✅ vite-node 170 0 35
✅ vite-quickjs 169 0 36

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 171 0 34
✅ astro-stable-quickjs 170 0 35
✅ express-stable-node 171 0 34
✅ express-stable-quickjs 170 0 35
✅ fastify-stable-node 171 0 34
✅ fastify-stable-quickjs 170 0 35
✅ hono-stable-node 171 0 34
✅ hono-stable-quickjs 170 0 35
✅ nest-stable-node 171 0 34
✅ nest-stable-quickjs 170 0 35
✅ nextjs-turbopack-canary-node 199 0 6
✅ nextjs-turbopack-canary-quickjs 198 0 7
✅ nextjs-turbopack-quickjs-snapshot 198 0 7
✅ nextjs-turbopack-stable-node 199 0 6
✅ nextjs-turbopack-stable-quickjs 198 0 7
✅ nextjs-webpack-canary-node 199 0 6
✅ nextjs-webpack-canary-quickjs 198 0 7
✅ nextjs-webpack-stable-node 199 0 6
✅ nextjs-webpack-stable-quickjs 198 0 7
✅ nitro-stable-node 171 0 34
✅ nitro-stable-quickjs 170 0 35
✅ nuxt-stable-node 171 0 34
✅ nuxt-stable-quickjs 170 0 35
✅ sveltekit-stable-node 190 0 15
✅ sveltekit-stable-quickjs 189 0 16
✅ tanstack-start-node 171 0 34
✅ tanstack-start-quickjs 170 0 35
✅ vite-stable-node 171 0 34
✅ vite-stable-quickjs 170 0 35

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 171 0 34
✅ astro-stable-quickjs 170 0 35
✅ express-stable-node 171 0 34
✅ express-stable-quickjs 170 0 35
✅ fastify-stable-node 171 0 34
✅ fastify-stable-quickjs 170 0 35
✅ hono-stable-node 171 0 34
✅ hono-stable-quickjs 170 0 35
✅ nest-stable-node 171 0 34
✅ nest-stable-quickjs 170 0 35
✅ nextjs-turbopack-canary-node 199 0 6
✅ nextjs-turbopack-canary-quickjs 198 0 7
✅ nextjs-turbopack-quickjs-snapshot 198 0 7
✅ nextjs-turbopack-stable-node 199 0 6
✅ nextjs-turbopack-stable-quickjs 198 0 7
✅ nextjs-webpack-canary-node 199 0 6
✅ nextjs-webpack-canary-quickjs 198 0 7
✅ nextjs-webpack-stable-node 199 0 6
✅ nextjs-webpack-stable-quickjs 198 0 7
✅ nitro-stable-node 171 0 34
✅ nitro-stable-quickjs 170 0 35
✅ nuxt-stable-node 171 0 34
✅ nuxt-stable-quickjs 170 0 35
✅ sveltekit-stable-node 190 0 15
✅ sveltekit-stable-quickjs 189 0 16
✅ tanstack-start-node 171 0 34
✅ tanstack-start-quickjs 170 0 35
✅ vite-stable-node 171 0 34
✅ vite-stable-quickjs 170 0 35

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 176 0 29
✅ astro-stable-quickjs 175 0 30
✅ express-stable-node 176 0 29
✅ express-stable-quickjs 175 0 30
✅ fastify-stable-node 176 0 29
✅ fastify-stable-quickjs 175 0 30
✅ hono-stable-node 176 0 29
✅ hono-stable-quickjs 175 0 30
✅ nest-stable-node 176 0 29
✅ nest-stable-quickjs 175 0 30
✅ nextjs-turbopack-canary-node 204 0 1
✅ nextjs-turbopack-canary-quickjs 203 0 2
✅ nextjs-turbopack-quickjs-snapshot 203 0 2
✅ nextjs-turbopack-stable-node 204 0 1
✅ nextjs-turbopack-stable-quickjs 203 0 2
✅ nextjs-webpack-canary-node 204 0 1
✅ nextjs-webpack-canary-quickjs 203 0 2
✅ nextjs-webpack-stable-node 204 0 1
✅ nextjs-webpack-stable-quickjs 203 0 2
✅ nitro-stable-node 176 0 29
✅ nitro-stable-quickjs 175 0 30
✅ nuxt-stable-node 176 0 29
✅ nuxt-stable-quickjs 175 0 30
✅ sveltekit-stable-node 195 0 10
✅ sveltekit-stable-quickjs 194 0 11
✅ tanstack-start-node 176 0 29
✅ tanstack-start-quickjs 175 0 30
✅ vite-stable-node 176 0 29
✅ vite-stable-quickjs 175 0 30

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-node 199 0 6
✅ nextjs-turbopack-quickjs 198 0 7

✅ 🌐 Cross-language Conformance

App Passed Failed Skipped
✅ python 68 0 112

✅ dynamic-runs

App Passed Failed Skipped
✅ astro-vercel 0 0 0
✅ example-vercel 0 0 0
✅ express-vercel 0 0 0
✅ fastify-vercel 0 0 0
✅ hono-vercel 0 0 0
✅ nest-vercel 0 0 0
✅ nextjs-turbopack-vercel 0 0 0
✅ nextjs-webpack-vercel 0 0 0
✅ nitro-vercel 0 0 0
✅ nuxt-vercel 0 0 0
✅ sveltekit-vercel 0 0 0
✅ tanstack-start-vercel 0 0 0
✅ vite-vercel 0 0 0

✅ local-postgres-spec9

App Passed Failed Skipped
✅ nextjs-turbopack-stable-node 0 0 180
✅ nextjs-turbopack-stable-quickjs 0 0 180

✅ vercel-http-transport

App Passed Failed Skipped
✅ example 170 0 35
✅ express 170 0 35
✅ hono 170 0 35
✅ nextjs-turbopack 197 0 8
✅ nitro 170 0 35
✅ vite 170 0 35

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

✅ vercel-spec9

App Passed Failed Skipped
✅ nextjs-turbopack-node 196 0 9
✅ nextjs-turbopack-quickjs 195 0 10

✅ vercel-ws-transport

App Passed Failed Skipped
✅ example 170 0 35
✅ express 170 0 35
✅ nextjs-turbopack 197 0 8
✅ vite 170 0 35

📋 View full workflow run

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟢 world-sim scenario book — 0 fail of 45 total
scenario outcome events virt replay violations
✅ smoke-no-steps completed 3 0ms ok 0
✅ smoke-one-step completed 5 0ms ok 0
✅ hook-at-step-started completed 9 0ms ok 0
✅ hook-at-step-completed completed 9 0ms ok 0
✅ hook-at-hook-created completed 9 0ms ok 0
✅ claim-hook-at-registration completed 9 0ms ok 0
✅ claim-hook-at-step-started completed 9 0ms ok 0
✅ deadline-hook-wins completed 6 1.0h ok 0
✅ deadline-expires completed 6 1.0h ok 0
✅ step-vs-timer-early-settlement completed 6 1.0h ok 0
✅ long-sleep completed 9 30.0d ok 0
✅ hook-never-arrives stalled 3 0ms skipped 0
✅ step-retries-twice completed 9 2.0s ok 0
✅ parallel-steps completed 7 0ms ok 0
✅ hook-on-execution-state completed 9 0ms ok 0
✅ peek-hook-before-branch completed 9 0ms ok 0
✅ peek-hook-after-branch completed 9 0ms ok 0
✅ peek-hook-at-registration completed 9 0ms ok 0
✅ race-hook-before-probe completed 9 0ms ok 0
✅ race-hook-after-probe completed 9 0ms ok 0
✅ race-duplicate-delivery completed 10 0ms ok 0
✅ attr-hook-before-step completed 9 0ms ok 0
✅ attr-hook-after-step completed 9 0ms ok 0
✅ attr-from-step-body completed 10 0ms ok 0
✅ fork-hook-after-timeout completed 11 1.0m ok 0
✅ fork-hook-before-timeout completed 11 1.0m ok 0
✅ count-hook-after-timeout completed 13 1.0m ok 0
✅ count-hook-before-timeout completed 15 1.0m ok 0
✅ stale-read-step-count-fork completed 15 1.0m ok 0
✅ stale-read-equal-step-counts completed 11 1.0m ok 0
✅ step-vs-step-fork completed 9 0ms ok 0
✅ step-vs-step-fork-fenced completed 9 1.0s ok 0
✅ fence-catches-benign-direction completed 9 1.0s ok 0
✅ run-ahead-fenced-overlap completed 10 1.0s ok 0
✅ in-flight-before-decision completed 15 1.0m ok 0
✅ in-flight-before-decision-counted completed 13 1.0m ok 0
✅ in-flight-after-decision completed 15 2.0m ok 0
✅ stale-read-step-count-fork-fenced completed 15 1.0m ok 0
✅ fork-hook-wins completed 10 1.0m ok 0
✅ fork-timeout-wins completed 10 1.0m ok 0
✅ unclaimed-payload-under-fork completed 14 1.0m ok 0
✅ claimed-payload-under-fork completed 14 1.0m ok 0
✅ writers-independent-step-bodies completed 9 0ms ok 0
✅ writers-scripted-tempo completed 9 0ms ok 0
✅ cancel-mid-step cancelled 6 0ms skipped 0

Full trace: world-sim.txt

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor
Framework Flow route Step reg. Framework output
hono 274.2 KiB (±0) 102.5 KiB (±0) 2.19 MiB (+60 B)
nextjs-turbopack 281.5 KiB (±0) 433 B (±0) 1010.1 KiB (+8 B)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

439309e · run

Signed-off-by: Joel Fickson <joel.fickson@vercel.com>
Signed-off-by: Joel Fickson <joel.fickson@vercel.com>

This branch was successfully deployed

1 active deployment
Preview – workflow-docs — 439309e0 Deployed Nov 7, 2026 by vercel[bot]
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.

Nitro dev: steps bundle leaves JSX raw in bundled local imports, so every workflow job fails with "Unexpected <"

1 participant