fix(tooling): clear the web Vite dep cache when skills/core output changes (#163)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-08-03 17:02:22 +08:00
committed by GitHub
parent 77a67e95d6
commit c4bf673309
2 changed files with 78 additions and 3 deletions
+65 -2
View File
@@ -31,7 +31,15 @@
*/
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -48,6 +56,8 @@ const INSTALL_STAMP = path.join(os.tmpdir(), `penguin-dev-${KEY}.install-stamp`)
const BUILD_STAMP = path.join(os.tmpdir(), `penguin-dev-${KEY}.build-stamp`);
const STALE_LOCK_MS = 10 * 60 * 1000; // no install+build takes 10 minutes; older locks are leftovers
const SKIP_WINDOW_MS = 5_000;
const VITE_STAMP = path.join(os.tmpdir(), `penguin-dev-${KEY}.vite-stamp`);
const WEB_VITE_CACHE = path.join(ROOT, "packages", "web", "node_modules", ".vite");
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -97,6 +107,51 @@ function readText(file) {
}
}
/**
* Content fingerprint of the injected workspace deps' build output (skills + core dist):
* every file's bytes are hashed — chunk renames alone would not do, because tsup's entry
* outputs keep stable filenames and hold entry-resident code, so an equal-length change
* there would slip past a path+size check. mtimes are deliberately excluded (tsup runs
* with clean:true, so every rebuild rewrites them even when nothing changed). The two
* dist trees are a few MB, so this stays well under the cost of anything else here.
*/
function injectedDistFingerprint() {
const parts = [];
for (const rel of ["packages/skills/dist", "packages/core/dist"]) {
const base = path.join(ROOT, rel);
try {
for (const entry of readdirSync(base, { recursive: true, withFileTypes: true })) {
if (!entry.isFile()) continue;
const abs = path.join(entry.parentPath, entry.name);
const digest = createHash("sha256").update(readFileSync(abs)).digest("hex");
parts.push(`${rel}/${path.relative(base, abs)}:${digest}`);
}
} catch {
parts.push(`${rel}:missing`);
}
}
return createHash("sha256").update(parts.sort().join("\n")).digest("hex");
}
/**
* Drop Vite's dependency-prebundle cache when the injected deps' output changed.
*
* The workspace runs with injectWorkspacePackages, so the web app consumes skills/core as
* snapshot copies inside node_modules/.pnpm (kept current by syncInjectedDepsAfterScripts
* when the build above runs). Vite then prebundles those copies into
* packages/web/node_modules/.vite/deps — and that cache is keyed by lockfile/config only,
* never by dependency content, so a rebuilt core would keep serving the browser the OLD
* code (e.g. a stale model catalog) until the cache is deleted. Purging only on a real
* output change keeps the usual dev start free of Vite's re-optimize cost.
*/
function refreshViteCache() {
const fingerprint = injectedDistFingerprint();
if (readText(VITE_STAMP) === fingerprint) return;
rmSync(WEB_VITE_CACHE, { recursive: true, force: true });
writeFileSync(VITE_STAMP, fingerprint);
console.log("[dev-prebuild] skills/core output changed; cleared the web Vite dep cache.");
}
/**
* Ensure `pnpm install` is current: runs it when node_modules is missing or the
* pnpm-lock.yaml hash differs from the last stamped install. Returns false on failure.
@@ -149,6 +204,11 @@ try {
}
if (stampAge < SKIP_WINDOW_MS) {
console.log("[dev-prebuild] deps were just built by a concurrent invocation; skipping.");
// The concurrent builder normally refreshed the cache too, but verify against the
// current output anyway — the check is two directory walks and a hash, and it keeps
// the invariant unconditional: after any successful prestep, the Vite dep cache
// matches the injected deps actually on disk.
refreshViteCache();
exitCode = 0;
} else {
console.log("[dev-prebuild] building skills + core...");
@@ -164,7 +224,10 @@ try {
// shell on Windows: pnpm is a .cmd shim there (see ensureInstalled).
{ cwd: ROOT, stdio: "inherit", shell: process.platform === "win32" },
);
if (res.status === 0) writeFileSync(BUILD_STAMP, String(Date.now()));
if (res.status === 0) {
writeFileSync(BUILD_STAMP, String(Date.now()));
refreshViteCache();
}
exitCode = res.status ?? 1;
}
}