Skip to content

fix(builders): shim __dirname/__filename in fully-bundled ESM output - #3876

Merged
TooTallNate merged 3 commits into
mainfrom
fix/esm-dirname-shim
Aug 28, 2026
Merged

fix(builders): shim __dirname/__filename in fully-bundled ESM output#3876
TooTallNate merged 3 commits into
mainfrom
fix/esm-dirname-shim

Conversation

@TooTallNate

@TooTallNate TooTallNate commented Aug 27, 2026

Copy link
Copy Markdown
Member

Problem

A customer upgrading from workflow v4 to v5 (5.0.0-beta.46) hit a hard init crash on every flow-route invocation:

ReferenceError: __dirname is not defined in ES module scope

google-gax@5.0.8/build/src/grpc.js
→ @google-cloud/pubsub@5.3.0
→ /var/task/index.mjs

The function dies with Runtime.ExitError before any workflow code runs, so the flow route returns 500 and runs sit in pending forever.

Root cause

#1562 switched the Vercel Build Output API function from CJS (index.js) to a fully-bundled ESM index.mjs. esbuild wraps inlined CJS modules in __commonJS closures and our banner provides require via createRequire, but esbuild deliberately leaves the CJS globals __dirname/__filename as free identifiers in ESM output (evanw/esbuild#1921). google-gax is CJS-only (no ESM build exists) and computes a path from __dirname at module scope (build/src/grpc.js:66), so the whole bundle throws at import. v4 never hit this because the function was real CJS.

Fix

Extend getEsmRequireBanner to also define __filename/__dirname from import.meta.url — the standard interop shim (tsup injects the same one for format: esm + platform: node). This is the shim already written and verified live (with @prisma/client, which fails the same way) in #2770; it's extracted here so the regression fix isn't blocked on that PR's larger runtime-asset-tracing work. The existing skipEsmRequireBanner dedup keeps it declared exactly once in the combined bundle.

The shimmed __dirname points at the function root, not each dep's original package directory — that's sufficient for pubsub (protos load via required JSON, which esbuild inlines) and per-module-accurate asset paths remain #2770's territory.

Test

New regression test builds a temp app through VercelBuildOutputAPIBuilder with a google-gax-shaped CJS dependency (module-scope path.join(__dirname, ...)), then imports the emitted index.mjs under plain Node and executes the step. On main it fails with exactly the production error; with the fix it passes. It also asserts the shim appears exactly once in the bundle.

Related: #2770, #1956.

esbuild leaves the CJS globals __dirname/__filename as free identifiers
when inlining CJS modules into ESM output, so dependencies that reference
them at module scope (google-gax via @google-cloud/pubsub, Prisma's
runtime) crash the deployed Vercel function at init with
'ReferenceError: __dirname is not defined in ES module scope' before any
workflow code runs. v4 was immune because the Build Output API function
was CJS; #1562 switched it to ESM with a banner that shimmed only
require().

Extend the ESM banner to define __filename/__dirname from
import.meta.url, matching the shim verified live in #2770.
@TooTallNate
TooTallNate requested a review from a team as a code owner August 27, 2026 23:29
Copilot AI lite review requested due to automatic review settings August 27, 2026 23:29
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, v0 Aug 28, 2026 6:33am
example-nextjs-workflow-webpack Ready Ready Preview, v0 Aug 28, 2026 6:33am
example-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-astro-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-express-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-fastify-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-hono-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-nestjs-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-nitro-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-nuxt-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-python-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-sveltekit-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-tanstack-start-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workbench-vite-workflow Ready Ready Preview, v0 Aug 28, 2026 6:33am
workflow-docs Ready Ready Preview, v0 Aug 28, 2026 6:33am
workflow-swc-playground Ready Ready Preview, v0 Aug 28, 2026 6:33am
workflow-tarballs Ready Ready Preview, v0 Aug 28, 2026 6:33am
workflow-web Ready Ready Preview, v0 Aug 28, 2026 6:33am

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0e44ce9

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

This PR includes changesets to release 16 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/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

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (8 failed)

python-node (8 failed):

  • promiseAllWorkflow | wrun_41M13H219Q0GQ31G4BF3MRSP1V | 🔍 observability
  • sleepingWorkflow | wrun_41M13H2XZF0GXBMSC9NRFXS3NC | 🔍 observability
  • parallelSleepWorkflow | wrun_41M13H3AQY0GZQTACCQAPT1H7M | 🔍 observability
  • nullByteWorkflow | wrun_41M13H3P4S0GQBSMPDNS66RGTN | 🔍 observability
  • cancelRun - cancelling a running workflow | wrun_41M13H8WF90GYK87Z1NR89D0P9 | 🔍 observability
  • cancelRun via CLI - cancelling a running workflow | wrun_41M13H923Z0GGDKGQWHRFPPP8C | 🔍 observability
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M13H969P0GKVB99AJKYGXH0V | 🔍 observability
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M13H9P8X0GW7XTQ1A03MD84D | 🔍 observability

🌐 Cross-language Conformance (9 failed)

python (9 failed):

  • deploymentId: 'latest' is a no-op in non-Vercel worlds | wrun_01M13RFJV9NFE0HY0NK0RM0F83
  • promiseAllWorkflow | wrun_41M13H219Q0GQ31G4BF3MRSP1V
  • sleepingWorkflow | wrun_41M13H2XZF0GXBMSC9NRFXS3NC
  • parallelSleepWorkflow | wrun_41M13H3AQY0GZQTACCQAPT1H7M
  • nullByteWorkflow | wrun_41M13H3P4S0GQBSMPDNS66RGTN
  • cancelRun - cancelling a running workflow | wrun_41M13H8WF90GYK87Z1NR89D0P9
  • cancelRun via CLI - cancelling a running workflow | wrun_41M13H923Z0GGDKGQWHRFPPP8C
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_41M13H969P0GKVB99AJKYGXH0V
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_41M13H9P8X0GW7XTQ1A03MD84D

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

15 flaky tests
  • addTenWorkflow (hono)
  • cancelRun via CLI - cancelling a running workflow (nuxt)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (hono)
  • hookWithSleepFinalStepWorkflow - step only on final payload (nextjs-webpack)
  • hookWorkflow (tanstack-start)
  • hookWorkflow (vite)
  • promiseRaceWorkflow (hono)
  • sleepWinsRaceWorkflow (nuxt)
  • stepWinsRaceWorkflow (astro)
  • stepWinsRaceWorkflow (express)
  • stepWinsRaceWorkflow (fastify)
  • stepWinsRaceWorkflow (sveltekit)
  • stepWinsRaceWorkflow (tanstack-start)
  • stepWinsRaceWorkflow (vite)
  • utf8StreamWorkflow (hono)

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

33 infra events
  • cold-start-warmup · suite warmup (tanstack-start) · at 06:35:01Z · abandoned wrun_01M13H7HS856ZVS90E0FQ1XDF6
  • run-pickup-stall · hookCleanupTestWorkflow - hook token reuse after workflow completion (nextjs-webpack) · at 06:41:56Z · abandoned wrun_01M13HMKKSG40E350FNAJ1PJJ0
  • cold-start-warmup · suite warmup (python) · at 08:39:58Z · abandoned wrun_41M13R9HHG0GMYMGYH8MXJ5WN8 · (+7 more)
  • cold-start-warmup · suite warmup (python) · at 08:40:01Z · abandoned wrun_01M13R9MDQKZ4V73C7F1W1A771 · (+7 more)
  • run-pickup-stall · parallelSleepWorkflow (python) · at 08:40:15Z · abandoned wrun_41M13RD6KW0GNSMTTM34SGVTPY
  • run-pickup-stall · nullByteWorkflow (python) · at 08:40:15Z · abandoned wrun_41M13RD6KZ0GPM9BER9RNEGJ87
  • run-pickup-stall · sleepingWorkflow (python) · at 08:40:15Z · abandoned wrun_41M13RD6KW0GNSMTTM34SGVTPX
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 08:40:15Z · abandoned wrun_41M13RD77P0GJVEJZ0RD2ZA0WT
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 08:40:16Z · abandoned wrun_01M13RD9J47ZVZ957XT9G3SZFB
  • run-pickup-stall · parallelSleepWorkflow (python) · at 08:40:16Z · abandoned wrun_01M13RD9JCFVHDDCGCYDK5SCB7
  • run-pickup-stall · promiseAllWorkflow (python) · at 08:40:16Z · abandoned wrun_01M13RD9J5E6HF5HE9QPZ0Y5JV
  • run-pickup-stall · sleepingWorkflow (python) · at 08:40:16Z · abandoned wrun_01M13RD9JBKXED0CBS5A3124FY
  • run-pickup-stall · nullByteWorkflow (python) · at 08:40:16Z · abandoned wrun_01M13RD9JFT6YRB2W9TNBMQNF9
  • run-pickup-stall · promiseAllWorkflow (python) · at 08:40:17Z · abandoned wrun_41M13RD6KP0GYQCVYS1B4MRBWY
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 08:40:48Z · abandoned wrun_41M13RE7390GYTX4HVJ2368SBA
  • run-pickup-stall · parallelSleepWorkflow (python) · at 08:41:16Z · abandoned wrun_41M13RF26Q0GR0RK0BDCCFPEF5
  • run-pickup-stall · promiseAllWorkflow (python) · at 08:41:16Z · abandoned wrun_41M13RF24X0GRV0V8EJ6C0TSB3
  • run-pickup-stall · sleepingWorkflow (python) · at 08:41:16Z · abandoned wrun_41M13RF2660GZZZQ98CAXHZRRZ
  • run-pickup-stall · nullByteWorkflow (python) · at 08:41:16Z · abandoned wrun_41M13RF25A0GWYCXYQ0HF3C8JJ
  • run-pickup-stall · nullByteWorkflow (python) · at 08:41:16Z · abandoned wrun_01M13RF467FMQY0K7PMF07CXDX
  • run-pickup-stall · parallelSleepWorkflow (python) · at 08:41:16Z · abandoned wrun_01M13RF465GE215BD42NFS5101
  • run-pickup-stall · sleepingWorkflow (python) · at 08:41:16Z · abandoned wrun_01M13RF4690XZNT56BV1WCYPHF
  • run-pickup-stall · deploymentId: 'latest' is a no-op in non-Vercel worlds (python) · at 08:41:16Z · abandoned wrun_01M13RF4621WQBZ4KD4D46J8VS
  • run-pickup-stall · promiseAllWorkflow (python) · at 08:41:16Z · abandoned wrun_01M13RF4648A1FFC7N49AM29WH
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 08:42:06Z · abandoned wrun_41M13RF6XN0GRK48XHAM8QZGP8
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 08:42:16Z · abandoned wrun_01M13RGYSR1PXV9HJPYGD80HG5
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 08:42:16Z · abandoned wrun_01M13RGYT0B0B8JTCG56B7GJ1F
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 08:42:16Z · abandoned wrun_01M13RGYT36XEDBJN0NJ45NBSM
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 08:42:46Z · abandoned wrun_01M13RHW5F4RHJMP06ZJWPG0WJ
  • run-pickup-stall · cancelRun - cancelling a running workflow (python) · at 08:42:46Z · abandoned wrun_01M13RHW5DD15N2GWRY0NVHMS5
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 08:43:16Z · abandoned wrun_01M13RJSD7JHX02KEAPDNV3RGV
  • run-pickup-stall · sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration (python) · at 08:43:47Z · abandoned wrun_41M13RHT1F0GQSNB8GREFY5HM4
  • run-pickup-stall · cancelRun via CLI - cancelling a running workflow (python) · at 08:43:47Z · abandoned wrun_41M13RHYT70GNWKXCMWERN4DQC

E2E Test Summary

Summary
Passed Failed Skipped Total
❌ ▲ Vercel Production 3570 8 742 4320
✅ 💻 Local Development 3922 0 558 4480
✅ 📦 Local Production 3922 0 558 4480
✅ 🐘 Local Postgres 3922 0 558 4480
✅ 🪟 Windows 320 0 0 320
❌ 🌐 Cross-language Conformance 0 9 132 141
✅ vercel-http-transport 817 0 143 960
✅ vercel-multi-region 27 0 0 27
✅ vercel-ws-transport 553 0 87 640
Total 17053 17 2778 19848
Details by Category

❌ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 132 0 28
✅ astro-quickjs 132 0 28
✅ example-node 132 0 28
✅ example-quickjs 132 0 28
✅ express-node 132 0 28
✅ express-quickjs 132 0 28
✅ fastify-node 132 0 28
✅ fastify-quickjs 132 0 28
✅ hono-node 132 0 28
✅ hono-quickjs 132 0 28
✅ nest-node 132 0 28
✅ nest-quickjs 132 0 28
✅ nextjs-turbopack-node 157 0 3
✅ nextjs-turbopack-quickjs 157 0 3
✅ nextjs-webpack-node 157 0 3
✅ nextjs-webpack-quickjs 157 0 3
✅ nitro-node 132 0 28
✅ nitro-quickjs 132 0 28
✅ nuxt-node 132 0 28
✅ nuxt-quickjs 132 0 28
❌ python-node 0 8 152
✅ sveltekit-node 151 0 9
✅ sveltekit-quickjs 151 0 9
✅ tanstack-start-node 132 0 28
✅ tanstack-start-quickjs 132 0 28
✅ vite-node 132 0 28
✅ vite-quickjs 132 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 134 0 26
✅ astro-stable-quickjs 134 0 26
✅ express-stable-node 134 0 26
✅ express-stable-quickjs 134 0 26
✅ fastify-stable-node 134 0 26
✅ fastify-stable-quickjs 134 0 26
✅ hono-stable-node 134 0 26
✅ hono-stable-quickjs 134 0 26
✅ nest-stable-node 134 0 26
✅ nest-stable-quickjs 134 0 26
✅ nextjs-turbopack-canary-node 141 0 19
✅ nextjs-turbopack-canary-quickjs 141 0 19
✅ nextjs-turbopack-stable-node 160 0 0
✅ nextjs-turbopack-stable-quickjs 160 0 0
✅ nextjs-webpack-canary-node 141 0 19
✅ nextjs-webpack-canary-quickjs 141 0 19
✅ nextjs-webpack-stable-node 160 0 0
✅ nextjs-webpack-stable-quickjs 160 0 0
✅ nitro-stable-node 134 0 26
✅ nitro-stable-quickjs 134 0 26
✅ nuxt-stable-node 134 0 26
✅ nuxt-stable-quickjs 134 0 26
✅ sveltekit-stable-node 153 0 7
✅ sveltekit-stable-quickjs 153 0 7
✅ tanstack-start-node 134 0 26
✅ tanstack-start-quickjs 134 0 26
✅ vite-stable-node 134 0 26
✅ vite-stable-quickjs 134 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 134 0 26
✅ astro-stable-quickjs 134 0 26
✅ express-stable-node 134 0 26
✅ express-stable-quickjs 134 0 26
✅ fastify-stable-node 134 0 26
✅ fastify-stable-quickjs 134 0 26
✅ hono-stable-node 134 0 26
✅ hono-stable-quickjs 134 0 26
✅ nest-stable-node 134 0 26
✅ nest-stable-quickjs 134 0 26
✅ nextjs-turbopack-canary-node 141 0 19
✅ nextjs-turbopack-canary-quickjs 141 0 19
✅ nextjs-turbopack-stable-node 160 0 0
✅ nextjs-turbopack-stable-quickjs 160 0 0
✅ nextjs-webpack-canary-node 141 0 19
✅ nextjs-webpack-canary-quickjs 141 0 19
✅ nextjs-webpack-stable-node 160 0 0
✅ nextjs-webpack-stable-quickjs 160 0 0
✅ nitro-stable-node 134 0 26
✅ nitro-stable-quickjs 134 0 26
✅ nuxt-stable-node 134 0 26
✅ nuxt-stable-quickjs 134 0 26
✅ sveltekit-stable-node 153 0 7
✅ sveltekit-stable-quickjs 153 0 7
✅ tanstack-start-node 134 0 26
✅ tanstack-start-quickjs 134 0 26
✅ vite-stable-node 134 0 26
✅ vite-stable-quickjs 134 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 134 0 26
✅ astro-stable-quickjs 134 0 26
✅ express-stable-node 134 0 26
✅ express-stable-quickjs 134 0 26
✅ fastify-stable-node 134 0 26
✅ fastify-stable-quickjs 134 0 26
✅ hono-stable-node 134 0 26
✅ hono-stable-quickjs 134 0 26
✅ nest-stable-node 134 0 26
✅ nest-stable-quickjs 134 0 26
✅ nextjs-turbopack-canary-node 141 0 19
✅ nextjs-turbopack-canary-quickjs 141 0 19
✅ nextjs-turbopack-stable-node 160 0 0
✅ nextjs-turbopack-stable-quickjs 160 0 0
✅ nextjs-webpack-canary-node 141 0 19
✅ nextjs-webpack-canary-quickjs 141 0 19
✅ nextjs-webpack-stable-node 160 0 0
✅ nextjs-webpack-stable-quickjs 160 0 0
✅ nitro-stable-node 134 0 26
✅ nitro-stable-quickjs 134 0 26
✅ nuxt-stable-node 134 0 26
✅ nuxt-stable-quickjs 134 0 26
✅ sveltekit-stable-node 153 0 7
✅ sveltekit-stable-quickjs 153 0 7
✅ tanstack-start-node 134 0 26
✅ tanstack-start-quickjs 134 0 26
✅ vite-stable-node 134 0 26
✅ vite-stable-quickjs 134 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-node 160 0 0
✅ nextjs-turbopack-quickjs 160 0 0

❌ 🌐 Cross-language Conformance

App Passed Failed Skipped
❌ python 0 9 132

✅ vercel-http-transport

App Passed Failed Skipped
✅ example 132 0 28
✅ express 132 0 28
✅ hono 132 0 28
✅ nextjs-turbopack 157 0 3
✅ nitro 132 0 28
✅ vite 132 0 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

✅ vercel-ws-transport

App Passed Failed Skipped
✅ example 132 0 28
✅ express 132 0 28
✅ nextjs-turbopack 157 0 3
✅ vite 132 0 28

📋 View full workflow run

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 0e44ce9 · Fri, 28 Aug 2026 06:54:19 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 216 (-78%) 💚 1310 🔴 (+20%) 🔻 1336 🔴 (+18%) 🔻 1391 🔴 (+5.2%) 30
TTFS stream 164 (-28%) 💚 1307 🔴 (+22%) 🔻 1363 🔴 (+21%) 🔻 1493 🔴 (+26%) 🔻 30
TTFS hook + stream 1179 (+108%) 🔻 1605 🔴 (+21%) 🔻 1621 🔴 (+17%) 🔻 1853 🔴 (+25%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 638 (+13%) 1913 (+24%) 🔻 1929 (+14%) 1945 (+10%) 10
Fan-out TTLS Promise.all(100 steps) 2022 (+4.6%) 3350 (-12%) 3569 (-11%) 4577 (-28%) 💚 10
STSO 1020 steps (inline) 111 (+23%) 🔻 141 (+2.2%) 157 (-3.1%) 256 (+13%) 1019
WO 1020 steps 140754 (+0.9%) 140754 (+0.9%) 140754 (+0.9%) 140754 (+0.9%) 1
CRTT first chunk (pooled) 89 (-13%) 125 (-39%) 💚 132 (-78%) 💚 360 (-62%) 💚 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 116 (-16%) 148 (-67%) 216 (-65%) 437 (-50%) 110 (-77%) 10
size sweep (100/s, 160B-12KB) 116 (-41%) 161 (-74%) 203 (-73%) 468 (-61%) 154 (-65%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 130 (-35%) 141 (-43%) 189 (-62%) 347 (-63%) 203 (-60%) 3
replay eve-gpt-5.6-sol-2000t (1x) 123 (-46%) 162 (-11%) 1139 (+335%) 4626 (+568%) 2566 (+395%) 2
replay eve-gpt-5.6-sol-2000t (2x) 117 (-37%) 157 (-94%) 212 (-94%) 514 (-89%) 294 (-69%) 3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 138419ms → this run 140590ms (Δ +2171ms, +2%)

 50-100 ms  ┃                         main   1  this   0    -1
100-150 ms  ███████████████████████┃  main 870  this 865    -5
150-200 ms  ██┃█                      main 131  this 125    -6
200-250 ms  ┃                         main   9  this  18    +9
250-300 ms  ┃                         main   6  this   6    +0
300-350 ms  ┃                         main   1  this   2    +1
350-400 ms  ┃                         main   0  this   2    +2
450-500 ms  ┃                         main   1  this   1    +0
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant  RTT 1ms→5s+             avg         p50           p90           p99     n
control  ······▇█▁····  115.7 (-56%)  103 (-44%)    216 (-65%)    437 (-50%)  3000
sweep    ······▃█▁····  135.5 (-56%)  122 (-42%)    203 (-73%)    468 (-61%)  3000
gw 1x    ·····▁▅█▁····  117.1 (-38%)  110 (-20%)    189 (-62%)    347 (-63%)  5295
eve 1x   ·····▁▅█▁▁▁▁·  286.1 (+73%)  113 (-16%)  1139 (+335%)  4626 (+568%)  5186
eve 2x   ·····▁▄█▂▁···  139.3 (-85%)  120 (-80%)    212 (-94%)    514 (-89%)  7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control  █▃▄▁▁▁▃▃▁▁  104–157ms
sweep    ▆█▅▃▅▄▁▁▁▃  117–163ms
gw 1x    ▄▂█▄▄▄▃▅▅▁  106–133ms
eve 1x   ▁▁█▂▁▁▁▁▁▁  113–1627ms
eve 2x   ▄▁▂▂▁▆█▆█▂  106–182ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep  ▄▅▆▇█▄▁  133–138ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control  ▆▇▆▅▁▄██▁▃  30–37ms
sweep    ▅▃▁██▆▃▄▆▄  50–62ms
gw 1x    ▅▄█▂▅▅▇█▄▁  31–38ms
eve 1x   ▁▁█▁▂▁▁▁▁▂  23–71ms
eve 2x   ▇▂▁▃▃▇█▂▃█  19–26ms
📜 Previous results (2)

4bb2883

Fri, 28 Aug 2026 03:57:47 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 218 (+16%) 🔻 1296 🔴 (+17%) 🔻 1417 🔴 (+23%) 🔻 1568 🔴 (+7.7%) 30
TTFS stream 215 (+34%) 🔻 1295 🔴 (+15%) 1322 🔴 (+14%) 1335 🔴 (-1.5%) 30
TTFS hook + stream 1201 (+0.5%) 1579 🔴 (+14%) 1607 🔴 (+11%) 1818 🔴 (+19%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 618 (-4.9%) 1939 (+10%) 1950 (+1.5%) 1989 (-5.6%) 10
Fan-out TTLS Promise.all(100 steps) 1963 (+5.2%) 3617 (-2.7%) 3903 (+2.4%) 8352 (+20%) 🔻 10
STSO 1020 steps (inline) 115 (+5.5%) 148 (-6.9%) 170 (-8.1%) 379 (+33%) 🔻 1019
WO 1020 steps 154173 (±0%) 154173 (±0%) 154173 (±0%) 154173 (±0%) 1
CRTT first chunk (pooled) 93 (-2.1%) 134 (-16%) 💚 157 (-21%) 💚 244 (-27%) 💚 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 117 (-14%) 143 (-22%) 299 (+21%) 476 (-13%) 133 (-28%) 10
size sweep (100/s, 160B-12KB) 104 (-20%) 144 (-32%) 200 (-66%) 297 (-67%) 109 (-49%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 133 (-2%) 134 (-25%) 171 (-28%) 391 (-5%) 270 (-18%) 3
replay eve-gpt-5.6-sol-2000t (1x) 144 (±0%) 134 (-28%) 183 (-36%) 398 (-42%) 373 (-41%) 2
replay eve-gpt-5.6-sol-2000t (2x) 139 (-7%) 174 (-31%) 322 (-37%) 553 (-41%) 284 (-40%) 3

b7e64be

Thu, 27 Aug 2026 23:46:20 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 267 (+42%) 🔻 1279 🔴 (+15%) 🔻 1426 🔴 (+24%) 🔻 1441 🔴 (-1.0%) 30
TTFS stream 1193 (+641%) 🔻 1247 🔴 (+11%) 1266 🔴 (+9.4%) 1317 🔴 (-2.9%) 30
TTFS hook + stream 465 (-61%) 💚 1579 🔴 (+14%) 1895 🔴 (+31%) 🔻 5324 🔴 (+248%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 569 (-12%) 1807 (+2.6%) 1822 (-5.2%) 1888 (-10%) 10
Fan-out TTLS Promise.all(100 steps) 1683 (-9.8%) 3310 (-11%) 3649 (-4.3%) 8235 (+19%) 🔻 10
STSO 1020 steps (inline) 109 (±0%) 134 (-16%) 💚 154 (-17%) 💚 205 (-28%) 💚 1019
WO 1020 steps 134737 (-12%) 134737 (-12%) 134737 (-12%) 134737 (-12%) 1
CRTT first chunk (pooled) 84 (-12%) 108 (-32%) 💚 153 (-23%) 💚 289 (-13%) 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 95 (-30%) 139 (-24%) 275 (+11%) 681 (+25%) 114 (-38%) 10
size sweep (100/s, 160B-12KB) 104 (-21%) 122 (-42%) 154 (-74%) 277 (-69%) 105 (-51%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 105 (-23%) 119 (-33%) 151 (-37%) 264 (-36%) 174 (-47%) 3
replay eve-gpt-5.6-sol-2000t (1x) 99 (-31%) 116 (-37%) 145 (-49%) 371 (-46%) 234 (-63%) 2
replay eve-gpt-5.6-sol-2000t (2x) 149 (-1%) 159 (-37%) 223 (-56%) 402 (-57%) 252 (-47%) 3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

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.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

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) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

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 · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000t eaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t 6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 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; CRTT inside the workflow (excludes the api.vercel.com read path).

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

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

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

Full trace: world-sim.txt

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor
Framework Flow route Step reg. Framework output
hono 200.9 KiB (±0) 40.4 KiB (+25 B) 1.76 MiB (-729 B)
nextjs-turbopack 206.3 KiB (±0) 439 B (±0) 763.5 KiB (-206 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.

0e44ce9 · run

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

Reviewed the diff and verified the fix end-to-end locally. The diagnosis is right and the fix is the standard interop shim — no blocking concerns. One forward-looking naming issue inline, plus a test nit.

What I verified

  • The test is a genuine regression test: reverting only the banner change makes it fail with exactly ReferenceError: __dirname is not defined in ES module scope; restoring it passes. Full packages/builders suite is 250/251, and the one failure (workflow-bundle-boundary.test.ts, zod in the workflow bundle) reproduces on main.
  • The shim cannot leak into the workflow sandbox. The interim VM bundle is format: 'cjs' / platform: 'neutral', so getEsmRequireBanner returns '' for it. That matters: __dirname being absent inside the VM is intentional (#374), and this PR correctly leaves that alone.
  • All four esm banner sites are platform: 'node', so the new node:url / node:path imports always resolve.
  • No collision hazard from var __dirname at module scope. I specifically probed the case that worried me — a bundled ESM dep using the ubiquitous const __dirname = dirname(fileURLToPath(import.meta.url)) idiom. esbuild reserves __dirname whenever any module references it free and renames the declaration to __dirname2, so the CJS dep still reads the banner's value. Confirmed with a standalone esbuild repro; no SyntaxError, no clobbering.
  • tsc --noEmit and biome check clean on the changed files.
  • The python-node / python E2E failures are not from this PR — the same 8/9 failures appear on #3877 and #3873.

On linking issues to close

I searched issue bodies and comments; there is no existing report of this exact bug, so I'd avoid Fixes on anything:

  • #1956 (Prisma engine binary) and #1003 (sharp) are the tempting ones, and #2770 claims both — but they need that PR's runtime asset tracing. This shim only removes the earlier __dirname crash that #2770's own description mentions hitting first. Link as "unblocks the init crash in", not as closing.
  • #3778 ([nest] steps bundle declares require twice) is the closest live relative — same banner, same dedup flag. See the inline note; it's a trap for whoever fixes it, not a break here.
  • #374 and #1161 match by title but are the VM-sandbox class (__dirname / require inside node:vm), which this deliberately does not change. Don't link them as fixed.

'import { dirname as __pathDirname } from "node:path";\n' +
'var require = __createRequire(import.meta.url);\n' +
'var __filename = __fileURLToPath(import.meta.url);\n' +
'var __dirname = __pathDirname(__filename);\n'

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.

skipEsmRequireBanner now gates three globals — worth renaming before #3778 gets fixed

This banner is now the sole provider of require, __filename, and __dirname for every fully-bundled ESM output, but the flag that suppresses it is still skipEsmRequireBanner, and its JSDoc (L923-928) still describes it purely in terms of __createRequire being declared twice.

That is a live trap rather than a cosmetic one. #3778 ([nest] steps bundle declares require twice) is open, and the issue itself identifies skipEsmRequireBanner as the mechanism that should have prevented it: NestLocalBuilder calls createCombinedBundle({ bundleFinalOutput: false }), so the flag resolves to false, the steps bundle keeps the banner, and it collides with the const require = ... that #rewriteStepsBundleForCjs() prepends.

To be clear, this PR does not make #3778 worse — nest's shim only declares require, so no new duplicate identifier appears. But the obvious fix for #3778 (pass skipEsmRequireBanner: true from NestLocalBuilder) would now also strip __filename/__dirname from the nest steps bundle, silently reintroducing exactly the crash this PR fixes — for nest CJS users only, with no test covering it.

Suggest renaming to something like skipEsmInteropBanner and updating the JSDoc at L923-928 to say it gates all three globals, so whoever fixes #3778 has to reason about the dirname shim rather than discover it in production.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed the doc was the load-bearing part — the JSDoc now spells out that the flag drops all three globals and warns against using it for #3778's duplicate-require (4bb2883). Leaving the rename itself to whoever fixes #3778, per your approval note.

);
expect(
bundle.match(/var __dirname = __pathDirname\(__filename\);/g)
).toHaveLength(1);

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.

Two small things, neither blocking:

  1. Other emit sites are uncovered. This exercises the combined flow route (createCombinedBundle + bundleFinalOutput: true), which is the reported crash — but the shim is also emitted from createWorkflowsBundle's final wrapper (L1550) and the webhook bundle (L2031, which hardcodes getEsmRequireBanner('esm')). The webhook route is a separately deployed function, so a CJS dep with module-scope __dirname reachable from it would have failed identically. Worth at least noting in the comment that those paths are untested.

  2. The assertion guards the second-order symptom. A double banner's first failure is duplicate import { fileURLToPath as __fileURLToPath } bindings — a parse-time SyntaxError — which the executeStep import on L150 already catches. Matching one of the import lines instead of the var __dirname = ... line would fail closer to the real cause. Belt-and-braces as written, so take it or leave it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both taken: the import-binding assertion landed in 4bb2883, and rather than only noting the uncovered emit sites, 0e44ce9 asserts the webhook function bundle (its own esbuild pass, separately deployed) carries the shim too. createWorkflowsBundle's final wrapper remains covered only via the combined bundle, which the comment now says.

…terop shim

The webhook route is a separately deployed function built through its own
esbuild pass (createWebhookBundle); a CJS dependency referencing __dirname
reachable from it would have crashed identically, so cover that emit site
rather than only noting it as untested.
@TooTallNate
TooTallNate enabled auto-merge (squash) August 28, 2026 07:25
@TooTallNate
TooTallNate merged commit 88f5d21 into main Aug 28, 2026
453 of 464 checks passed
@TooTallNate
TooTallNate deleted the fix/esm-dirname-shim branch August 28, 2026 08:45
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 88f5d21 (AI decision).

This is a genuine crash fix, but the defect it fixes does not exist on stable: I verified that origin/stable's packages/builders/src/base-builder.ts has no getEsmRequireBanner/skipEsmRequireBanner at all, and vercel-build-output-api.ts there still emits CJS functions (index.js with package.json type commonjs), so __dirname/__filename are real bindings. The ESM Build Output API switch (#1562) that introduced the ReferenceError is main-only, so the fix has no applicable code to patch on the maintenance line.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

88f5d214d48b15d3126943313ed03d48667e772c

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