// A directory's cheap content signature — moved here, unchanged, from // compose-site.ts (release 18): the publish stages' `inputSig` is computed // with the SAME function compose uses to decide what to skip, so "a site is // fresh" and "compose would skip everything" can never disagree. import path from "node:path"; import { readdir, stat } from "node:fs/promises"; import { createHash } from "node:crypto"; import type { Dirent } from "node:fs"; // A cheap content signature for a directory: relative path + size + mtime of // every file within, hashed. It reflects exactly what a recursive copy would // move, so an unchanged source (build:index skipped it incrementally) yields the // same signature and the copy is skipped. The shared transcript/subs trees hold // only a bounded handful of paginated page files per channel, so this is far // cheaper than the copy it guards. Returns "" when the dir is absent/empty. // // `ignoreBasename` excludes files by name from the signature. The shared // per-channel trees carry a manifest.json that build:index rewrites with a fresh // `generatedAt` for EVERY channel whenever ANY channel mutates (the shared // page-writer loop runs over all channels). Its pageCount/slugToPage only change // when the channel's pages change — which the page files already capture — so // excluding it lets an unchanged channel stay skipped on a partial-change build // instead of re-copying all 50-odd channels. The tiny stale manifest left behind // on a skip is structurally identical (only its timestamp differs). export async function dirSignature( dir: string, ignoreBasename?: string, ): Promise { const h = createHash("sha1"); let any = false; const walk = async (rel: string): Promise => { let ents: Dirent[]; try { ents = await readdir(path.join(dir, rel), { withFileTypes: true }); } catch { return; } ents.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); for (const e of ents) { if (!e.isDirectory() && e.name === ignoreBasename) continue; const childRel = rel ? `${rel}/${e.name}` : e.name; if (e.isDirectory()) { await walk(childRel); } else { any = true; const s = await stat(path.join(dir, childRel)); h.update(`${childRel}\t${s.size}\t${s.mtimeMs}\n`); } } }; await walk(""); return any ? h.digest("hex") : ""; }