#!/usr/bin/env node // Freshness stamp for web/dist (milestone-15 ruling 5). A stale dist has // already shipped a crashing settings page once. Write mode runs from web/ as // part of `npm run build`; check mode runs from the repository root as a // build.zig system command. Every path resolves from this file's own location // so both working directories hash the same set. import { createHash } from "node:crypto"; import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const webRoot = dirname(dirname(fileURLToPath(import.meta.url))); const distDir = join(webRoot, "dist"); const stampFile = join(distDir, ".src-hash"); const stampRelative = "web/dist/.src-hash"; const inputDirs = ["src", "public"]; const inputFiles = [ "index.html", "package.json", "package-lock.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json", ]; const staleMessage = "web/dist is stale: rebuild the frontend (npm run build)"; function fail(message) { process.stderr.write(`${message}\n`); process.exit(1); } function walk(relativeDir) { const absolute = join(webRoot, relativeDir); let entries; try { entries = readdirSync(absolute, { withFileTypes: true }); } catch (err) { fail(`stamp-dist: cannot read web/${relativeDir}: ${err.message}`); } const found = []; for (const entry of entries) { const child = `${relativeDir}/${entry.name}`; if (entry.isDirectory()) { found.push(...walk(child)); } else if (entry.isFile()) { found.push(child); } } return found; } function inputSet() { const paths = [...inputFiles, ...inputDirs.flatMap(walk)]; for (const path of inputFiles) { try { if (!statSync(join(webRoot, path)).isFile()) fail(`stamp-dist: web/${path} is not a file`); } catch (err) { fail(`stamp-dist: cannot stat web/${path}: ${err.message}`); } } // Sorted by path so the digest does not depend on directory order. return paths.sort(); } function digest() { const hash = createHash("sha256"); for (const path of inputSet()) { hash.update(path); hash.update("\0"); hash.update(readFileSync(join(webRoot, path))); hash.update("\0"); } return hash.digest("hex"); } const check = process.argv.includes("--check"); const computed = digest(); if (check) { let recorded; try { recorded = readFileSync(stampFile, "utf8").trim(); } catch { fail(staleMessage); } if (recorded !== computed) fail(staleMessage); process.exit(0); } try { writeFileSync(stampFile, `${computed}\n`); } catch (err) { fail(`stamp-dist: cannot write ${stampRelative}: ${err.message}`); } process.stdout.write(`${stampRelative} ${computed}\n`);