Skip to content

Commit fc73d3e

Browse files
bloveclaude
andauthored
fix(minting-service): bundle Vercel functions via Nx esbuild build (#133)
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 <noreply@anthropic.com>
1 parent 9fa6136 commit fc73d3e

3 files changed

Lines changed: 105 additions & 8 deletions

File tree

apps/minting-service/project.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
"projectType": "application",
66
"tags": ["scope:service", "type:app"],
77
"targets": {
8+
"build": {
9+
"executor": "nx:run-commands",
10+
"outputs": ["{projectRoot}/.vercel/output"],
11+
"options": {
12+
"command": "node scripts/build.mjs",
13+
"cwd": "apps/minting-service"
14+
}
15+
},
816
"lint": { "executor": "@nx/eslint:lint" },
917
"test": {
1018
"executor": "@nx/vitest:test",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
2+
/**
3+
* Builds the minting-service API functions using Vercel Build Output API v3.
4+
*
5+
* - Bundles each `api/*.ts` into a self-contained CommonJS module via esbuild,
6+
* inlining workspace deps (@cacheplane/*) and npm deps alike so no install
7+
* step is required inside each function directory.
8+
* - Writes to `.vercel/output/functions/api/<name>.func/` with the companion
9+
* `.vc-config.json` Vercel's Node runtime expects.
10+
* - Writes `.vercel/output/config.json` at the top level so Vercel picks up
11+
* the Build Output API layout instead of scanning `api/` for TS sources.
12+
*
13+
* Run via `nx build minting-service`, which sets `cwd` to this app.
14+
*/
15+
import { build } from 'esbuild';
16+
import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
17+
import { basename, join, resolve } from 'node:path';
18+
import { fileURLToPath } from 'node:url';
19+
20+
const appRoot = resolve(fileURLToPath(import.meta.url), '..', '..');
21+
const apiDir = join(appRoot, 'api');
22+
const outputRoot = join(appRoot, '.vercel', 'output');
23+
const functionsRoot = join(outputRoot, 'functions', 'api');
24+
25+
async function listEntries() {
26+
const files = await readdir(apiDir);
27+
return files
28+
.filter((f) => f.endsWith('.ts') && !f.endsWith('.spec.ts') && !f.endsWith('.d.ts'))
29+
.map((f) => join(apiDir, f));
30+
}
31+
32+
async function buildEntry(entry) {
33+
const name = basename(entry, '.ts');
34+
const funcDir = join(functionsRoot, `${name}.func`);
35+
await mkdir(funcDir, { recursive: true });
36+
37+
await build({
38+
entryPoints: [entry],
39+
outfile: join(funcDir, 'index.js'),
40+
bundle: true,
41+
platform: 'node',
42+
target: 'node20',
43+
format: 'cjs',
44+
// Lets esbuild resolve `@cacheplane/*` via tsconfig paths and
45+
// follows `extends` up to the workspace tsconfig.base.json.
46+
tsconfig: join(appRoot, 'tsconfig.app.json'),
47+
// @vercel/node provides these as ambient in the runtime environment.
48+
external: ['@vercel/node'],
49+
sourcemap: 'inline',
50+
logLevel: 'info',
51+
});
52+
53+
const vcConfig = {
54+
runtime: 'nodejs20.x',
55+
handler: 'index.js',
56+
launcherType: 'Nodejs',
57+
shouldAddHelpers: true,
58+
maxDuration: 10,
59+
};
60+
await writeFile(join(funcDir, '.vc-config.json'), JSON.stringify(vcConfig, null, 2));
61+
62+
// The app's package.json declares `"type": "module"`, which would make Node
63+
// load `index.js` as ESM. esbuild emits CJS, so drop a colocated package.json
64+
// that pins `commonjs` inside each function directory.
65+
await writeFile(
66+
join(funcDir, 'package.json'),
67+
JSON.stringify({ type: 'commonjs' }, null, 2),
68+
);
69+
}
70+
71+
async function main() {
72+
await rm(outputRoot, { recursive: true, force: true });
73+
await mkdir(functionsRoot, { recursive: true });
74+
75+
const entries = await listEntries();
76+
if (entries.length === 0) {
77+
throw new Error(`no api entries found in ${apiDir}`);
78+
}
79+
80+
await Promise.all(entries.map(buildEntry));
81+
82+
// Minimal Build Output API v3 config — no custom routes needed;
83+
// Vercel maps `functions/api/<name>.func/` to `/api/<name>` by convention.
84+
await writeFile(
85+
join(outputRoot, 'config.json'),
86+
JSON.stringify({ version: 3 }, null, 2),
87+
);
88+
89+
console.log(`\nbuilt ${entries.length} function(s) to ${outputRoot}`);
90+
}
91+
92+
main().catch((err) => {
93+
console.error(err);
94+
process.exit(1);
95+
});

apps/minting-service/vercel.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
{
22
"installCommand": "cd ../.. && npm ci",
3-
"buildCommand": "cd ../.. && npx tsc --noEmit -p apps/minting-service/tsconfig.app.json",
4-
"framework": null,
5-
"functions": {
6-
"api/*.ts": {
7-
"runtime": "nodejs20.x",
8-
"maxDuration": 10
9-
}
10-
}
3+
"buildCommand": "cd ../.. && npx nx build minting-service",
4+
"framework": null
115
}

0 commit comments

Comments
 (0)