121 lines
3.1 KiB
JavaScript
121 lines
3.1 KiB
JavaScript
#!/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);
|
|
}
|