milestone 15: make a green run mean a real pass
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
#!/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`);
|
||||
Reference in New Issue
Block a user