commit 90345b517bbf52739b9b0942ce0e1b4f624dc988
parent 0278e0af2c2dfefde5332afce1cd9ec53772875b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 6 Oct 2026 08:14:01 -0400
common: dirSignature moves to lib/dirSignature.ts, unchanged (compose imports it)
The publish stages' inputSig is computed with the function compose decides
its skips with, so the two can never disagree.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 55 insertions(+), 46 deletions(-)
diff --git a/common/bin/compose-site.ts b/common/bin/compose-site.ts
@@ -23,9 +23,9 @@
import path from "node:path";
import { cp, link, mkdir, rm, readdir, access, readFile, writeFile, stat, rename } from "node:fs/promises";
-import { createHash } from "node:crypto";
import type { Dirent } from "node:fs";
import { getPaths, type Paths } from "../lib/paths";
+import { dirSignature } from "../lib/dirSignature";
import { getSite, resolveSocialLinks, resolveHubUrl, type Site } from "../lib/site";
import { getSettings } from "../lib/settings";
import {
@@ -688,51 +688,6 @@ async function writeComposeCache(p: string, cache: ComposeCache): Promise<void>
await writeFile(p, JSON.stringify(cache));
}
-// 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).
-async function dirSignature(
- dir: string,
- ignoreBasename?: string,
-): Promise<string> {
- const h = createHash("sha1");
- let any = false;
- const walk = async (rel: string): Promise<void> => {
- 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") : "";
-}
-
// Materialize the member subset of a shared per-channel tree into public/,
// skipping channels whose source is unchanged since the last compose. Replaces
// the previous "rm -rf the whole tree then cp every member" with an in-place
diff --git a/common/lib/dirSignature.ts b/common/lib/dirSignature.ts
@@ -0,0 +1,54 @@
+// 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<string> {
+ const h = createHash("sha1");
+ let any = false;
+ const walk = async (rel: string): Promise<void> => {
+ 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") : "";
+}