#!/usr/bin/env node /** * Rebuild the Next.js web host only when web source files are newer than the * staged standalone build. Skips the build when nothing has changed. * * Also self-heals a missing/incomplete web dependency install so `npm run sf:web` * doesn't fail with bare `next` command-not-found errors. * * Exit codes: * 0 — build was up-to-date or successfully rebuilt * 1 — build failed */ "use strict"; const { execSync } = require("node:child_process"); const { existsSync, readdirSync, statSync } = require("node:fs"); const { join, resolve } = require("node:path"); // Skip on Windows — Next.js webpack build hits EPERM scanning system dirs if (process.platform === "win32") { console.log("[forge] Web build skipped on Windows."); process.exit(0); } const root = resolve(__dirname, ".."); const webRoot = join(root, "web"); // Also watch src/ because api routes import directly from src/web/* and src/resources/* const srcRoot = join(root, "src"); const stagedSentinel = join(root, "dist", "web", "standalone", "server.js"); // Directories inside web/ that are not source and should be ignored for // staleness comparison. const IGNORED_DIRS = new Set([ "node_modules", ".next", ".turbo", "dist", "out", ".cache", ]); /** * Walk a directory tree, yield the mtime of every file, skipping ignored dirs. * Returns the maximum mtime found (ms since epoch), or 0 if nothing found. */ function newestMtime(dir) { let max = 0; const stack = [dir]; while (stack.length > 0) { const current = stack.pop(); let entries; try { entries = readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { if (entry.isDirectory()) { if (!IGNORED_DIRS.has(entry.name)) { stack.push(join(current, entry.name)); } continue; } try { const mt = statSync(join(current, entry.name)).mtimeMs; if (mt > max) max = mt; } catch { // skip unreadable files } } } return max; } function sentinelMtime() { try { return statSync(stagedSentinel).mtimeMs; } catch { return 0; } } function hasWebBuildDependencies() { return existsSync(join(webRoot, "node_modules", ".bin", "next")); } function ensureWebBuildDependencies() { if (hasWebBuildDependencies()) { return; } console.log( "[forge] Web build dependencies are missing or incomplete — running npm --prefix web ci...", ); execSync("npm --prefix web ci", { cwd: root, stdio: "inherit" }); } const sourceMtime = Math.max(newestMtime(webRoot), newestMtime(srcRoot)); const builtMtime = sentinelMtime(); if (builtMtime > 0 && builtMtime >= sourceMtime) { console.log("[forge] Web build is up-to-date, skipping rebuild."); process.exit(0); } if (builtMtime === 0) { console.log("[forge] No staged web build found — building now..."); } else { console.log( "[forge] Web/src source has changed since last build — rebuilding...", ); } try { ensureWebBuildDependencies(); execSync("npm run build:web-host", { cwd: root, stdio: "inherit" }); } catch (err) { console.error("[forge] Web build failed:", err.message); process.exit(1); }