From 6659bd63df9b81f7a1a68628daabc982670d2412 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 21 Apr 2026 12:02:50 -0700 Subject: [PATCH] fix(minting-service): bundle Vercel functions via Nx esbuild build The prior `tsc --noEmit` buildCommand didn't emit any JS, so Vercel would still try to compile `api/*.ts` directly at deploy time. That path can't resolve `@cacheplane/db` or `@cacheplane/licensing` because `libs/*` isn't in the root npm workspaces and the libs don't declare `exports` entry points. Switch to Nx-native build: a new `build` target runs esbuild over every `api/*.ts`, inlining workspace deps via tsconfig paths and emitting Vercel Build Output API v3 layout under `.vercel/output/`. Each `.func` colocates a `package.json` pinning `commonjs` to override the app's `"type": "module"`. Co-Authored-By: Claude Opus 4 --- apps/minting-service/project.json | 8 +++ apps/minting-service/scripts/build.mjs | 95 ++++++++++++++++++++++++++ apps/minting-service/vercel.json | 10 +-- 3 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 apps/minting-service/scripts/build.mjs diff --git a/apps/minting-service/project.json b/apps/minting-service/project.json index 09be25624..4ed7aee90 100644 --- a/apps/minting-service/project.json +++ b/apps/minting-service/project.json @@ -5,6 +5,14 @@ "projectType": "application", "tags": ["scope:service", "type:app"], "targets": { + "build": { + "executor": "nx:run-commands", + "outputs": ["{projectRoot}/.vercel/output"], + "options": { + "command": "node scripts/build.mjs", + "cwd": "apps/minting-service" + } + }, "lint": { "executor": "@nx/eslint:lint" }, "test": { "executor": "@nx/vitest:test", diff --git a/apps/minting-service/scripts/build.mjs b/apps/minting-service/scripts/build.mjs new file mode 100644 index 000000000..18ba7d9f5 --- /dev/null +++ b/apps/minting-service/scripts/build.mjs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +/** + * Builds the minting-service API functions using Vercel Build Output API v3. + * + * - Bundles each `api/*.ts` into a self-contained CommonJS module via esbuild, + * inlining workspace deps (@cacheplane/*) and npm deps alike so no install + * step is required inside each function directory. + * - Writes to `.vercel/output/functions/api/.func/` with the companion + * `.vc-config.json` Vercel's Node runtime expects. + * - Writes `.vercel/output/config.json` at the top level so Vercel picks up + * the Build Output API layout instead of scanning `api/` for TS sources. + * + * Run via `nx build minting-service`, which sets `cwd` to this app. + */ +import { build } from 'esbuild'; +import { mkdir, readdir, rm, writeFile } from 'node:fs/promises'; +import { basename, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const appRoot = resolve(fileURLToPath(import.meta.url), '..', '..'); +const apiDir = join(appRoot, 'api'); +const outputRoot = join(appRoot, '.vercel', 'output'); +const functionsRoot = join(outputRoot, 'functions', 'api'); + +async function listEntries() { + const files = await readdir(apiDir); + return files + .filter((f) => f.endsWith('.ts') && !f.endsWith('.spec.ts') && !f.endsWith('.d.ts')) + .map((f) => join(apiDir, f)); +} + +async function buildEntry(entry) { + const name = basename(entry, '.ts'); + const funcDir = join(functionsRoot, `${name}.func`); + await mkdir(funcDir, { recursive: true }); + + await build({ + entryPoints: [entry], + outfile: join(funcDir, 'index.js'), + bundle: true, + platform: 'node', + target: 'node20', + format: 'cjs', + // Lets esbuild resolve `@cacheplane/*` via tsconfig paths and + // follows `extends` up to the workspace tsconfig.base.json. + tsconfig: join(appRoot, 'tsconfig.app.json'), + // @vercel/node provides these as ambient in the runtime environment. + external: ['@vercel/node'], + sourcemap: 'inline', + logLevel: 'info', + }); + + const vcConfig = { + runtime: 'nodejs20.x', + handler: 'index.js', + launcherType: 'Nodejs', + shouldAddHelpers: true, + maxDuration: 10, + }; + await writeFile(join(funcDir, '.vc-config.json'), JSON.stringify(vcConfig, null, 2)); + + // The app's package.json declares `"type": "module"`, which would make Node + // load `index.js` as ESM. esbuild emits CJS, so drop a colocated package.json + // that pins `commonjs` inside each function directory. + await writeFile( + join(funcDir, 'package.json'), + JSON.stringify({ type: 'commonjs' }, null, 2), + ); +} + +async function main() { + await rm(outputRoot, { recursive: true, force: true }); + await mkdir(functionsRoot, { recursive: true }); + + const entries = await listEntries(); + if (entries.length === 0) { + throw new Error(`no api entries found in ${apiDir}`); + } + + await Promise.all(entries.map(buildEntry)); + + // Minimal Build Output API v3 config — no custom routes needed; + // Vercel maps `functions/api/.func/` to `/api/` by convention. + await writeFile( + join(outputRoot, 'config.json'), + JSON.stringify({ version: 3 }, null, 2), + ); + + console.log(`\nbuilt ${entries.length} function(s) to ${outputRoot}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/minting-service/vercel.json b/apps/minting-service/vercel.json index 7bccdd339..4196c9d81 100644 --- a/apps/minting-service/vercel.json +++ b/apps/minting-service/vercel.json @@ -1,11 +1,5 @@ { "installCommand": "cd ../.. && npm ci", - "buildCommand": "cd ../.. && npx tsc --noEmit -p apps/minting-service/tsconfig.app.json", - "framework": null, - "functions": { - "api/*.ts": { - "runtime": "nodejs20.x", - "maxDuration": 10 - } - } + "buildCommand": "cd ../.. && npx nx build minting-service", + "framework": null }