Skip to content

fix(builders): copy traced runtime assets into the Vercel workflow function#2770

Draft
NathanColosimo wants to merge 16 commits into
mainfrom
nathanc/issue-1956
Draft

fix(builders): copy traced runtime assets into the Vercel workflow function#2770
NathanColosimo wants to merge 16 commits into
mainfrom
nathanc/issue-1956

Conversation

@NathanColosimo

@NathanColosimo NathanColosimo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #1956

Problem

The Vercel Build Output API builder bundles all workflow/step code into flow.func/index.mjs. Bundling inlines JavaScript, but drops files that code loads from disk at runtime. @prisma/client is the reported case: its generated client resolves the native query engine (libquery_engine-*.so.node) and schema.prisma from disk at runtime, so every step that touches the database crashes with PrismaClientInitializationError on Vercel. The same applies to any native addon, WASM file, or data file a dependency reads at runtime.

Verifying against a real @prisma/client@6.19.3 on a live deployment surfaced a second, earlier crash: Prisma's CJS runtime references __dirname at module scope, which doesn't exist when inlined into the ESM bundle — the function dies with ReferenceError: __dirname is not defined in ES module scope before engine lookup even starts. Both are fixed here.

Fix

1. Runtime asset tracing. The steps-bundle esbuild metafile already lists every module that was inlined into the function bundle. After bundling, those modules are traced with @vercel/nft at their original on-disk locations (where relative asset references still resolve), and the runtime assets they reference are copied into flow.func:

  • A traced file counts as a runtime asset when nft tags it asset (static analysis of fs reads) or sharedlib, or when it's a .node native addon (nft traces those as plain dependency, and its sharedlib globs are platform-specific — a .dylib.node Prisma engine traced on Linux gets no tag).
  • Runtime lookups probe paths recorded at build time (Prisma bakes the generate-time engine-dir path relative to cwd), so each asset is copied at every location a lookup may probe: its path below the innermost node_modules (flattening pnpm store layouts to the plain layout npm-style lookups expect) and its app-directory-relative real path (covering pnpm stores nested in the app and Prisma clients generated into app source, resolved against the function root — process.cwd() at runtime).
  • Credential-like files (.env, .env.*, .npmrc, *.pem, *.key) are never copied, files outside both the app directory and node_modules are never copied, and traced assets can't overwrite generated function files.
  • The whole step is best-effort: a tracing failure logs a warning and degrades to the previous behavior (nothing copied) instead of failing builds that don't depend on runtime assets.

2. __dirname/__filename ESM shims. The ESM banner already provides require for inlined CJS; it now also provides __filename/__dirname (pointing at the bundle location, the function root at runtime), so CJS dependencies referencing them at module scope — like Prisma's runtime — execute.

Because the trace entries come from the metafile, everything esbuild already resolved (tsconfig path aliases, package export conditions, the SWC plugin's output) is handled for free — no parallel resolution logic. The metafile is skipped in watch mode, so dev rebuilds don't pay for it.

This runs for VercelBuildOutputAPIBuilder and everything that extends it: CLI --target vercel-build-output-api, the legacy Nitro-v2/Nuxt Vercel path, Astro, and SvelteKit Vercel builders. (Nitro v3 bundles workflow routes through Nitro's own rollup pipeline and doesn't use this builder.)

Verified live on Vercel

Deployed workbench/example (prebuilt output) to the example-workflow preview project with a real @prisma/client@6.19.3, SQLite datasource, and binaryTargets for both lambda architectures, then started the workflow through the queue on the deployment:

# example app (CLI / vercel-build-output-api target)
{"runId":"wrun_01KWWBTBPTSAQ57R8SZ3A28W4D","status":"completed","returnValue":"prisma-ok:engine-loaded"}
# nitro-v2 app (the legacy Build Output path Nuxt uses — the issue reporter's stack)
{"runId":"wrun_01KWWD0Z5A32Q56NZ4X9PSAV22","status":"completed","returnValue":"prisma-ok:engine-loaded"}

The step instantiated PrismaClient, loaded the native arm64 query engine from the traced copy, and executed CREATE TABLE + create + findUnique against SQLite. The live run is also what surfaced the __dirname crash and Prisma's real probe list (it probes the baked generate-time relative path, not a generic node_modules/.prisma/client), which shaped the multi-path placement.

Real-world package verification (live preview deploys)

Each package below was exercised in a real workflow step on a live deployment of workbench/example (queue-triggered, full round-trip):

Package Runtime-loading mechanism Result
@prisma/client 6.19.3 generated sidecars, baked cwd-relative probe prisma-ok:engine-loaded (also on nitro-v2)
sharp 0.35.3 exports-mapped .node require → JS shim → dlopens libvips from sibling package sharp-ok:2x2:png
tiktoken WASM sidecar tiktoken-ok:3
pdfkit AFM fonts via readFileSync(__dirname + '/data/…') pdfkit-ok:%PDF-:true
geoip-lite .dat files via join(__dirname, '../data') ❌ path escapes the function root — unfixable by copying (package offers a geodatadir override)

Prisma layout coverage

Layout Engine probe path at runtime Covered
npm/yarn flat node_modules (the issue's setup) cwd()/node_modules/.prisma/client ✅ flattened copy
pnpm store inside the app cwd()/node_modules/.pnpm/…/.prisma/client ✅ real-path copy
custom output in app source (Prisma's recommended setup) cwd()/<output dir> ✅ app-relative copy (verified live)
pnpm monorepo store above the app escapes the function root (cwd()/../../…) ❌ needs the package externalized instead of bundled — follow-up

Tests

  • Regression tests build a real app in a temp dir with a Prisma-shaped fixture (package resolving a dot-prefixed generated sibling that reads a native engine + schema at runtime, with a module-scope __dirname reference like Prisma's runtime). They fail on main with exactly the issue's failure modes and pass with the fix:
    • flat node_modules layout: asserts engine + schema are copied, and that the built bundle executes under plain Node (catches the __dirname crash)
    • pnpm store layout with symlinked package: asserts the engine at both the flattened and real store paths, app data files kept, .env excluded, and that app files colliding with generated function files never clobber the bundle
  • The engine fixture is named libquery_engine-darwin.dylib.node on purpose: it matches no nft sharedlib glob on any platform, so the .node-addon path stays covered.
  • Full @workflow/builders suite: 430 tests green. Typecheck + build green.

Known limitations (follow-up territory)

  • Bundled packages whose generate-time path escapes the function root (pnpm monorepo stores) can't be fixed by copying — they need externalization support (trace + ship the package unbundled, Next.js-style serverExternalPackages).
  • __dirname-relative lookups inside bundled code resolve against the function root, not the copied node_modules path (the shim makes them defined, not per-module accurate).
  • JS worker files emitted as assets are copied, but their own imports aren't recursively traced.
  • When two sources map to the same output path, the first in trace order wins (a warning is logged on conflict).

Review

Reviewed with /simplify (3-agent reuse/quality/efficiency pass) and 4 rounds of autoreview (Codex gpt-5.5:xhigh + Claude Opus 4.8:xhigh panel). One earlier review finding (app-directory assets can include credentials) led to app-dir copying being removed; the live Prisma verification showed that removal breaks Prisma's recommended custom-output setup, so it was reinstated with the credential denylist and parent-escape guard — flagging explicitly for maintainer judgment.

Post-review empirical audit

Each safeguard was toggled off individually to confirm it's load-bearing: the TS transpile hook (tests fail without it — assets referenced from TS step files are missed), the .node rule (verified against nft's tagging directly), and the multi-path placement (derived from Prisma's live probe list on the deployment). One addition was pruned by the same audit: owning-package.json companion copying addressed a scenario that requires .node externalization this builder never uses, and live Prisma runs on two apps pass without it.

…nction

Bundling inlines JavaScript but drops files that code loads from disk at
runtime — Prisma query engines, native addons, data files. Trace the
modules esbuild inlined into the steps bundle with @vercel/nft and copy
the runtime assets they reference into flow.func so runtime lookups
relative to the function root succeed.

Fixes #1956
…s-scoped

- Wrap tracing in try/catch so a trace failure degrades to the previous
  behavior (nothing copied) instead of failing builds
- Only copy assets inside node_modules; app-directory files (which can
  include credentials) are never copied
- Resolve absolute traced paths (cross-drive on Windows) directly
@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c118ebf

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/nuxt Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/vitest Patch
workflow Patch
@workflow/world-testing 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

@vercel

vercel Bot commented Jul 3, 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, Comment Jul 9, 2026 4:12am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 9, 2026 4:12am
example-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-astro-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-express-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-fastify-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-hono-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-nitro-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workbench-vite-workflow Ready Ready Preview, Comment Jul 9, 2026 4:12am
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 9, 2026 4:12am
workflow-swc-playground Ready Ready Preview, Comment Jul 9, 2026 4:12am
workflow-tarballs Ready Ready Preview, Comment Jul 9, 2026 4:12am
workflow-web Ready Ready Preview, Comment Jul 9, 2026 4:12am

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c118ebf · Thu, 09 Jul 2026 04:36:40 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1633 1673 🔴 1708 🔴 1727 🔴 30
TTFS hook + stream 1591 1953 🔴 2155 🔴 2482 🔴 30
STSO 1020 steps (1-20) 277 305 🔴 397 🔴 503 🔴 19
STSO 1020 steps (101-120) 425 442 🔴 544 🔴 604 🔴 19
STSO 1020 steps (1001-1020) 838 854 🔴 971 🔴 1076 🔴 19
WO stream 1633 1673 1708 1727 30
WO hook + stream 1591 1953 2155 2482 30
SL stream 4906 4990 🔴 5102 🔴 5915 🔴 30
SL hook + stream 4952 5535 🔴 5756 🔴 7099 🔴 30
📜 Previous results (1)

da3d43b

Thu, 09 Jul 2026 00:23:52 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1292 1766 🔴 1847 🔴 1992 🔴 30
TTFS hook + stream 1659 2047 🔴 2178 🔴 2377 🔴 30
STSO 1020 steps (1-20) 291 340 🔴 400 🔴 441 🔴 19
STSO 1020 steps (101-120) 447 473 🔴 602 🔴 654 🔴 19
STSO 1020 steps (1001-1020) 909 966 🔴 1027 🔴 1066 🔴 19
WO stream 1292 1766 1847 1992 30
WO hook + stream 1659 2047 2178 2377 30
SL stream 4240 5010 🔴 5724 🔴 5914 🔴 30
SL hook + stream 4992 5605 🔴 5711 🔴 5916 🔴 30

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1453 1 240 1694
✅ 💻 Local Development 1617 0 231 1848
✅ 📦 Local Production 1617 0 231 1848
✅ 🐘 Local Postgres 1617 0 231 1848
✅ 🪟 Windows 153 0 1 154
✅ 📋 Other 894 0 184 1078
Total 7351 1 1118 8470

❌ Failed Tests

▲ Vercel Production (1 failed)

express (1 failed):

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 28
✅ example 127 0 27
❌ express 125 1 28
✅ fastify 126 0 28
✅ hono 126 0 28
✅ nextjs-turbopack 150 0 4
✅ nextjs-webpack 150 0 4
✅ nitro 126 0 28
✅ nuxt 126 0 28
✅ sveltekit 145 0 9
✅ vite 126 0 28
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 134 0 20
✅ nextjs-turbopack-stable 153 0 1
✅ nextjs-webpack-canary 134 0 20
✅ nextjs-webpack-stable 153 0 1
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 134 0 20
✅ nextjs-turbopack-stable 153 0 1
✅ nextjs-webpack-canary 134 0 20
✅ nextjs-webpack-stable 153 0 1
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 134 0 20
✅ nextjs-turbopack-stable 153 0 1
✅ nextjs-webpack-canary 134 0 20
✅ nextjs-webpack-stable 153 0 1
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 153 0 1
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 26
✅ e2e-local-dev-tanstack-start- 128 0 26
✅ e2e-local-postgres-nest-stable 128 0 26
✅ e2e-local-postgres-tanstack-start- 128 0 26
✅ e2e-local-prod-nest-stable 128 0 26
✅ e2e-local-prod-tanstack-start- 128 0 26
✅ e2e-vercel-prod-tanstack-start 126 0 28

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

Comment thread packages/builders/package.json Outdated
… assets at every probed path

Verified live on Vercel with @prisma/client 6.19.3: without the shim the
bundle crashes at import (Prisma's runtime references __dirname at module
scope), and Prisma probes its engine at the cwd-relative path baked at
generate time — so assets are copied at both the flattened node_modules
path and their app-relative real path.
Comment thread packages/builders/src/vercel-build-output-api.ts Outdated
@socket-security

socket-security Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​vercel/​nft@​0.30.47510010093100

View full report

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.

Prisma engine binary not bundled with workflow steps on Vercel

1 participant