#!/usr/bin/env node /** * Production build with auto host detection (Windows/Linux/macOS, RAM, CPU). * Web then Go — never parallel. Caps cores so rebuilds do not pin 100%. * * Env (optional overrides): * BUILD_PROFILE=low|medium|high * BUILD_NODE_HEAP_MB / BUILD_GO_P / BUILD_GO_MAXPROCS / BUILD_GO_MEMLIMIT * BUILD_LOW_MEM=0|1 * BUILD_SKIP_WEB=1 / BUILD_SKIP_GO=1 / BUILD_GO_ALL=1 * BUILD_BIN_DIR (default /bin) * * Usage: * node scripts/build.mjs * npm run build | build:web | build:api */ import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { detectHostProfile, formatHostNote, withGoLowMem, withNodeHeap, } from "./lowmem-env.mjs"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const isWin = process.platform === "win32"; const exe = isWin ? ".exe" : ""; const args = new Set(process.argv.slice(2)); const onlyWeb = args.has("--web") || args.has("web"); const onlyGo = args.has("--api") || args.has("api") || args.has("--go") || args.has("go"); const wantHelp = args.has("-h") || args.has("--help"); if (wantHelp) { console.log(`Usage: node scripts/build.mjs [--web|--api] Auto-detects OS/RAM/CPU and scales Node heap + Go -p / GOMAXPROCS. Alias: npm run build | build:web | build:api`); process.exit(0); } const profile = detectHostProfile(); function section(title) { console.log(`\n==> ${title}`); } function run(command, cmdArgs, { cwd = root, env = process.env, label, shell } = {}) { return new Promise((resolve, reject) => { const child = spawn(command, cmdArgs, { cwd, env, stdio: "inherit", shell: shell ?? (isWin && command === "npm"), }); child.on("error", reject); child.on("exit", (code, signal) => { if (signal) { reject(new Error(`${label ?? command}: killed by ${signal}`)); return; } if (code !== 0) { reject(new Error(`${label ?? command}: exit ${code ?? 1}`)); return; } resolve(); }); }); } async function buildWeb() { section("Web (Vite / SvelteKit)"); const env = withNodeHeap(process.env, profile.nodeHeapMb, profile.uvThreadpool); console.log( `NOTE NODE_OPTIONS max-old-space-size=${profile.nodeHeapMb} UV_THREADPOOL_SIZE=${env.UV_THREADPOOL_SIZE} BUILD_LOW_MEM=${env.BUILD_LOW_MEM}`, ); await run( "npm", ["run", "build", "--workspace=web"], { env, label: "web build" }, ); } async function buildGo() { section("API + worker build"); const apiDir = path.join(root, "apps", "api"); const binDir = path.resolve(root, process.env.BUILD_BIN_DIR || "bin"); fs.mkdirSync(binDir, { recursive: true }); const env = withGoLowMem(process.env, profile); const apiOut = path.join(binDir, `api${exe}`); const workerOut = path.join(binDir, `worker${exe}`); const p = profile.goP; console.log( `NOTE GOMAXPROCS=${env.GOMAXPROCS}` + (env.GOMEMLIMIT ? ` GOMEMLIMIT=${env.GOMEMLIMIT}` : "") + (env.GOGC ? ` GOGC=${env.GOGC}` : "") + ` go -p ${p}`, ); // Always sequential api then worker — parallel binaries double peak RAM. const ldflags = "-s -w"; await run( "go", ["build", `-p=${p}`, "-trimpath", `-ldflags=${ldflags}`, "-o", apiOut, "./cmd/api"], { cwd: apiDir, env, label: "go build api", shell: false }, ); console.log(`OK ${path.relative(root, apiOut)}`); await run( "go", ["build", `-p=${p}`, "-trimpath", `-ldflags=${ldflags}`, "-o", workerOut, "./cmd/worker"], { cwd: apiDir, env, label: "go build worker", shell: false }, ); console.log(`OK ${path.relative(root, workerOut)}`); if (process.env.BUILD_GO_ALL === "1") { section("Go packages (go build ./...)"); await run( "go", ["build", `-p=${p}`, "./..."], { cwd: apiDir, env, label: "go build ./..." }, ); } } async function main() { section("Host"); console.log(`NOTE ${formatHostNote(profile)}`); const skipWeb = process.env.BUILD_SKIP_WEB === "1" || onlyGo; const skipGo = process.env.BUILD_SKIP_GO === "1" || onlyWeb; if (!skipWeb) await buildWeb(); if (!skipGo) await buildGo(); section("Done"); console.log(`Build finished (tier=${profile.tier}, sequential web→go).`); } main().catch((err) => { console.error(`FAIL ${err?.message || err}`); process.exit(1); });