fix(builders): copy traced runtime assets into the Vercel workflow function#2770
fix(builders): copy traced runtime assets into the Vercel workflow function#2770NathanColosimo wants to merge 16 commits into
Conversation
…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
…imize lockfile churn
…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 detectedLatest commit: c118ebf 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 |
📊 Workflow Benchmarkscommit Backend:
📜 Previous results (1)da3d43bThu, 09 Jul 2026 00:23:52 GMT · run logs
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. |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests▲ Vercel Production (1 failed)express (1 failed):
Details by Category❌ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
… 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.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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/clientis the reported case: its generated client resolves the native query engine (libquery_engine-*.so.node) andschema.prismafrom disk at runtime, so every step that touches the database crashes withPrismaClientInitializationErroron 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.3on a live deployment surfaced a second, earlier crash: Prisma's CJS runtime references__dirnameat module scope, which doesn't exist when inlined into the ESM bundle — the function dies withReferenceError: __dirname is not defined in ES module scopebefore 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/nftat their original on-disk locations (where relative asset references still resolve), and the runtime assets they reference are copied intoflow.func:asset(static analysis of fs reads) orsharedlib, or when it's a.nodenative addon (nft traces those as plaindependency, and itssharedlibglobs are platform-specific — a.dylib.nodePrisma engine traced on Linux gets no tag).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)..env,.env.*,.npmrc,*.pem,*.key) are never copied, files outside both the app directory andnode_modulesare never copied, and traced assets can't overwrite generated function files.2.
__dirname/__filenameESM shims. The ESM banner already providesrequirefor 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
VercelBuildOutputAPIBuilderand 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 theexample-workflowpreview project with a real@prisma/client@6.19.3, SQLite datasource, andbinaryTargetsfor both lambda architectures, then started the workflow through the queue on the deployment:The step instantiated
PrismaClient, loaded the native arm64 query engine from the traced copy, and executedCREATE TABLE+create+findUniqueagainst SQLite. The live run is also what surfaced the__dirnamecrash and Prisma's real probe list (it probes the baked generate-time relative path, not a genericnode_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):@prisma/client6.19.3prisma-ok:engine-loaded(also on nitro-v2)sharp0.35.3.noderequire → JS shim → dlopens libvips from sibling packagesharp-ok:2x2:pngtiktokentiktoken-ok:3pdfkitreadFileSync(__dirname + '/data/…')pdfkit-ok:%PDF-:truegeoip-lite.datfiles viajoin(__dirname, '../data')geodatadiroverride)Prisma layout coverage
node_modules(the issue's setup)cwd()/node_modules/.prisma/clientcwd()/node_modules/.pnpm/…/.prisma/clientoutputin app source (Prisma's recommended setup)cwd()/<output dir>cwd()/../../…)Tests
__dirnamereference like Prisma's runtime). They fail onmainwith exactly the issue's failure modes and pass with the fix:node_moduleslayout: asserts engine + schema are copied, and that the built bundle executes under plain Node (catches the__dirnamecrash).envexcluded, and that app files colliding with generated function files never clobber the bundlelibquery_engine-darwin.dylib.nodeon purpose: it matches no nftsharedlibglob on any platform, so the.node-addon path stays covered.@workflow/builderssuite: 430 tests green. Typecheck + build green.Known limitations (follow-up territory)
serverExternalPackages).__dirname-relative lookups inside bundled code resolve against the function root, not the copiednode_modulespath (the shim makes them defined, not per-module accurate).Review
Reviewed with
/simplify(3-agent reuse/quality/efficiency pass) and 4 rounds of autoreview (Codexgpt-5.5:xhigh+ Claude Opus4.8:xhighpanel). 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
.noderule (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.jsoncompanion copying addressed a scenario that requires.nodeexternalization this builder never uses, and live Prisma runs on two apps pass without it.