[nest] Fix Vercel build output when using NextLocalBuilder#1472
[nest] Fix Vercel build output when using NextLocalBuilder#1472VaguelySerious wants to merge 80 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: bf49620 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 (56 failed)mongodb (3 failed):
redis (2 failed):
turso (51 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) 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. |
NestLocalBuilder.build() only generates runtime .mjs bundles for the WorkflowController — no Vercel Build Output API functions are created. VQS discovers consumers via experimentalTriggers in .vc-config.json, so NestJS deployments silently fail with "no v2beta consumer". Add buildVercelOutput() which generates: - .well-known/workflow/v1/step.func with STEP_QUEUE_TRIGGER - .well-known/workflow/v1/flow.func with WORKFLOW_QUEUE_TRIGGER - .well-known/workflow/v1/webhook/[token].func - api/index.js.func (esbuild-bundled NestJS app) - config.json with routing rules Also update NestJS getting-started docs and README with Vercel deployment instructions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
880e1af to
565712c
Compare
Add nest to the e2e-vercel-prod matrix in tests.yml with the correct project ID (prj_G3RlX1TsB5V2hcqTNvuVoJXdfE1o) and slug (workbench-nestjs-workflow). Set up the workbench for Vercel Build Output API deployment: - Add api/index.js entry point for the NestJS serverless function - Add scripts/build-vercel.ts postbuild step calling buildVercelOutput() - Update vercel.json with buildCommand - Fix project slug in create-test-matrix.mjs - Clear framework preset from "NestJS" to "Other" on Vercel project Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a `build` subcommand to the workflow-nest CLI that builds workflow bundles and, when VERCEL env is detected, automatically generates the Build Output API with experimentalTriggers. The entry point is auto- detected from api/index.js (or can be specified via --entry). This replaces the need for a custom scripts/build-vercel.ts file — users just add `"postbuild": "workflow-nest build"` to package.json. Also document the Vercel project requirement: framework preset must be set to "Other" (not "NestJS") to avoid conflicting with Build Output API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vercel Build Output API requires check:true on routes after handle:miss. With framework=null (no NestJS preset), this validation is enforced. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With framework=null (no NestJS preset), Build Output API functions must be self-contained since no NFT tracing is done. Bundle the NestJS entry point with esbuild into a single CJS file. Use esbuild define to inject the manifest JSON as a constant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With framework=nestjs, @vercel/node handles NFT and function bundling. The entry point imports __manifest.js which the builder populates during postbuild. NFT traces the static import and includes the file. Remove esbuild bundling and catch-all routing — the NestJS framework preset handles both. Only keep the webhook dynamic route and workflow function generation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use readFileSync with a path computed from process.cwd() — NFT should trace the join() pattern and include the manifest file. Remove the __manifest.js import approach that was fragile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With framework=nestjs, @vercel/node creates the handler from
dist/main.js — our _vercel/entry.js is not used. The
WorkflowController.handleManifest() uses readFileSync with a computed
path that NFT can't trace.
Add a static readFileSync('.nestjs/workflow/manifest.json') call in
app.module.ts that NFT CAN analyze statically. This ensures the
manifest file (created by workflow-nest build during postbuild) is
included in the Lambda bundle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The WorkflowController's readFileSync with a dynamic path can't be traced by NFT. Instead, eagerly read the manifest in app.module.ts at module load time and store it on globalThis. The controller checks globalThis.__workflowManifestJson before falling back to readFileSync. The static string '.nestjs/workflow/manifest.json' in app.module.ts gives NFT the best chance to trace the file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NFT resolves readFileSync paths relative to the containing file. Since app.module.ts compiles to dist/app.module.js, the path to .nestjs/workflow/manifest.json needs to be ../.nestjs/workflow/... Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tdirs NFT might skip .nestjs/ directory (dotdir). Rename output dir to workflow-out/ and update the readFileSync path to ../workflow-out/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NFT always includes files in dist/ for NestJS projects. Copy manifest
to dist/workflow-manifest.json during postbuild. The app.module reads
it with readFileSync('./workflow-manifest.json') which resolves
relative to dist/app.module.js.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
readFileSync with a relative path resolves from CWD at runtime, not from the file's directory. Use import.meta.url + dirname to resolve from the actual module location (dist/). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
new URL('./file', import.meta.url) is the ESM standard pattern that
@vercel/nft traces statically AND resolves correctly at runtime
(relative to the module, not CWD).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NFT doesn't trace readFileSync with new URL() or computed paths. Use createRequire(import.meta.url) to require() the manifest JSON. require() resolves relative to the file AND NFT traces it statically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ist/ Generate the manifest in the build step before nest build compiles. Then copy manifest.json to dist/workflow-manifest.json so it's alongside the compiled app.module.js. The require() call in the compiled module resolves it correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explicitly include dist/workflow-manifest.json in all serverless functions via vercel.json's functions.includeFiles. NFT can't trace the createRequire pattern, so the file must be explicitly included. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The functions config only matches api/ directory files. Move entry back to api/index.js and use includeFiles to force-include the manifest file in the Lambda. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The workflow-nest build CLI copies manifest.json to dist/ during postbuild. The app.module.ts reads it at module load time using join(process.cwd(), 'dist', 'workflow-manifest.json'). On Vercel's Lambda, CWD is the project root and dist/ is always NFT-included. The globalThis.__workflowManifestJson value is read by the WorkflowController as a fallback when readFileSync fails. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NFT can't trace readFileSync with computed paths. But it DOES trace
dynamic import() with string literals. Generate a JS module
(dist/workflow-manifest.js) and use await import('./workflow-manifest.js')
in app.module.ts. NFT sees the static string and includes the file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The env var may not be reaching the Lambda runtime. Force-set it in app.module.ts at import time when VERCEL env is detected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Top-level await may crash in the Vercel Lambda context. Use createRequire(import.meta.url) for synchronous require() of the manifest module. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SWC compiles app.module.ts to CJS. The createRequire require() can't load ESM modules. Write the manifest module as CJS (module.exports) instead of ESM (export default). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With package.json type=module, .js files are ESM. CJS module.exports in a .js file is misinterpreted. Use .cjs extension to force CJS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ively .cjs files may not be included by the NestJS Vercel preset. Use .json which require() auto-parses and is universally included in dist/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In ESM modules, bare require() isn't available. createRequire creates a require function that resolves relative to the file. The .json file in dist/ IS copied to the Lambda by the NestJS Vercel preset (it copies all of dist/). This approach works both locally and on Vercel. Verified locally: globalThis.__workflowManifestJson is set correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The fix: 1. prebuild: workflow-nest build generates .nestjs/workflow/manifest.json 2. nest-cli.json assets: copies manifest to dist/workflow/manifest.json 3. nest build: produces dist/ with the manifest included 4. app.module.ts: createRequire loads dist/workflow/manifest.json 5. NestJS Vercel preset includes ALL nest build output in Lambda Key insight: nest build's assets copy runs during compilation, so the manifest is part of the official dist/ output that the NestJS Vercel preset deploys. No NFT tracing needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add configureManifest(json) to WorkflowController which sets the manifest as a string constant. The controller checks this before falling back to readFileSync. WorkflowModule.forRoot() accepts a manifestJson option that calls configureManifest during setup. In the workbench: - prebuild generates manifest via workflow-nest build - nest-cli.json assets copies it to dist/workflow/manifest.json - app.module.ts loads it via createRequire and passes to forRoot() - On Vercel, the controller serves the pre-loaded manifest directly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The definitive fix for the manifest on Vercel:
1. prebuild: workflow-nest build generates src/_manifest-data.ts
containing the manifest as an exported string constant
2. nest build: SWC compiles it to dist/_manifest-data.js
3. app.module.ts: imports _manifest-data.js via standard ESM import
4. NFT traces the standard import and includes the compiled module
5. WorkflowModule.forRoot({ manifestJson }) passes it to the controller
This avoids ALL the previous issues:
- No readFileSync with untraceable paths
- No createRequire that NFT can't follow
- No .cjs/.json files that Vercel doesn't include
- The manifest is part of the standard compiled output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NestLocalBuilder.build() only generates runtime .mjs bundles for the WorkflowController — no Vercel Build Output API functions were created.
See community issue here: https://community.vercel.com/t/vercel-workflows-runs-stuck-in-pending-state-when-using-nestjs/36611/9, user repro in https://github.com/stasbel/nestjs-workflow-example/blob/main/package.json