[builders] Switch Vercel Build Output API from CJS to ESM#1562
[builders] Switch Vercel Build Output API from CJS to ESM#1562VaguelySerious merged 14 commits intomainfrom
Conversation
The step, workflow, and webhook bundles produced by VercelBuildOutputAPIBuilder and the standalone builder now default to ESM format instead of CJS. This preserves native import.meta.url support, fixing issues with libraries like Prisma that rely on it. The intermediate workflow bundle (which runs inside vm.runInContext) remains CJS — the only place where CJS is genuinely required. Closes #1507 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 2a01a03 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (61 failed)mongodb-dev (1 failed):
redis-dev (1 failed):
turso-dev (1 failed):
turso (58 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
|
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
❌ Some benchmark jobs failed:
Check the workflow run for details. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Fully-bundled ESM output needs a real `require` function for CJS dependencies that call require() on Node.js builtins (e.g. debug requiring tty). Add a createRequire banner to the steps, workflow, and webhook bundles. Also update the CLI standalone config to output .mjs files, and update world-testing to import from the new .mjs paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The webhook handler (resumeWebhook) needs to read hook metadata to determine the respondWith behavior. However, the webhook Lambda may not have the deployment encryption key available, causing metadata hydration to fail with "Encrypted stream data encountered but no encryption key is available". Fix by passing undefined instead of the encryption key when serializing hook metadata in the suspension handler. Hook metadata is small (just respondWith config) and doesn't need encryption. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The package version was reset from 4.2.x-beta to 4.0.0 for the v5
beta release. The encryption format capability check used 4.2.0-beta.64
as the minimum version, causing getRunCapabilities("4.0.0") to report
encryption as unsupported. This made resumeHook strip the encryption
key, while the step handler still encrypted data — causing "Encrypted
stream data encountered but no encryption key is available" errors in
the webhook handler.
Fix by lowering the minVersion to 4.0.0 to cover the reset range.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts debug-only changes that modified the webhook error response format and added console.error/console.warn logging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…anges
The PR branches had version 4.0.0 (intermediate changeset reset) instead
of 5.0.0-beta.0 (the actual published version). This caused
getRunCapabilities("4.0.0") to report encryption as unsupported, breaking
the webhook respondWith flow on Vercel Prod deployments.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TooTallNate
left a comment
There was a problem hiding this comment.
Clean, well-motivated change. Fixes the root cause of #1507 (import.meta.url undefined in CJS bundles) rather than polyfilling it. The approach is correct: switch the outer bundles to ESM while keeping the intermediate VM bundle as CJS.
What I verified
Intermediate workflow bundle stays CJS: base-builder.ts:726 is hardcoded format: 'cjs' with comment "Runs inside the VM which expects cjs" — untouched by this PR. Correct.
createRequire banner: The ESM require shim (import { createRequire as __createRequire } from "node:module"; var require = __createRequire(import.meta.url);) is the standard pattern for CJS interop in ESM. This is needed because esbuild's CJS-to-ESM shimming generates require() calls for CJS dependencies (e.g. debug → require('tty')), and ESM doesn't provide require natively. The banner runs once at module load — negligible overhead.
All three bundle types switched:
- Steps:
format = 'esm'default (was'cjs'),index.mjs,"type": "module" - Final workflow wrapper:
format = 'esm'default (was'cjs'),index.mjs,"type": "module" - Webhook: hardcoded
'esm'(was'cjs'),index.mjs,"type": "module"
Handler config updated: .vc-config.json now uses handler: 'index.mjs' (was 'index.js'). Correct.
CJS polyfill removed: Webhook no longer calls getCjsImportMetaPolyfill() or sets define: webhookImportMetaDefine. Steps/workflows still call it but it's a no-op for format === 'esm' (returns empty string/empty object). Clean.
world-testing updated: server.mts switches from CJS default imports (import flow from '...flow.js'; flow.POST(...)) to ESM named imports (import { POST as flowPOST } from '...flow.mjs'). Build script copies *.mjs instead of *.js. .d.mts stubs generated. All consistent.
CLI paths updated: workflow-config.ts changes .js → .mjs for step/flow/webhook bundle paths.
Tests updated: local-build.test.ts now asserts ESM output (no CJS polyfill __import_meta_url, no pathToFileURL(__filename)). Good negative assertions.
Changeset: patch for @workflow/builders and @workflow/cli. Correct.
LGTM.
| */ | ||
| private getEsmRequireBanner(format: string): string { | ||
| if (format !== 'esm') return ''; | ||
| return 'import { createRequire as __createRequire } from "node:module";\nvar require = __createRequire(import.meta.url);\n'; |
There was a problem hiding this comment.
Non-blocking: The var require = __createRequire(import.meta.url) declaration shadows any ambient require in the scope. In ESM, require isn't defined anyway so there's nothing to shadow. But if esbuild's CJS shim emits its own var require = ... later in the bundle, there could be a double declaration. In practice esbuild doesn't do this for format: 'esm' (it uses __require internally), so it's fine. Just noting for awareness.
Summary
VercelBuildOutputAPIBuildernow emits.mjsfiles with"type": "module"inpackage.jsonand"handler": "index.mjs"in.vc-config.jsonStandaloneBuilderinherits the new ESM defaults automaticallyvm.runInContext) remains CJS — the only place where CJS is genuinely requiredimport.meta.urlpolyfill from the webhook bundle (no longer needed with ESM output)Motivation
Every framework-specific local builder (Next.js, SvelteKit, Astro, NestJS, Nitro, Vitest) already overrides the format to ESM. The Vercel Build Output API builder and standalone builder were the only ones still defaulting to CJS, which caused
import.meta.urlto be undefined at runtime — breaking libraries like Prisma that rely on it (#1507).The CJS polyfill from #1509 fixed the symptom but this PR fixes the root cause: there's no reason these bundles need CJS since Vercel serverless functions fully support ESM.
Supersedes #1509.
Test plan
@workflow/builderstests pass (115/115)@workflow/coretests pass (499/499)"type": "module"and"handler": "index.mjs"__import_meta_url) and uses native ESMCloses #1507
🤖 Generated with Claude Code