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:
+13
-1
@@ -29,9 +29,21 @@ serializes concurrent invocations) **keeps `pnpm install` current automatically*
|
||||
fresh clone or a pulled lockfile change installs before starting, and an up-to-date tree
|
||||
pays nothing (the lockfile hash is stamped) — then prebuilds the workspace deps (skills,
|
||||
core) with back-to-back builds deduped: starting `dev:server` and `dev:web` at the same
|
||||
time (or just `pnpm dev`) installs and builds exactly once. `dev:docs` / `dev:landing`
|
||||
time (or just `pnpm dev`) installs and builds exactly once. When that build changes
|
||||
skills/core output, the prestep also clears the web app's Vite dep cache
|
||||
(`packages/web/node_modules/.vite`), which is keyed by lockfile/config only and would
|
||||
otherwise keep serving the browser the previous core. `dev:docs` / `dev:landing`
|
||||
run the install check only (`--install-only`).
|
||||
|
||||
One rule when bypassing the dev commands: **rebuild skills/core through pnpm, in that
|
||||
order** (`pnpm build`, or restart `pnpm dev`) — the workspace uses injected dependencies
|
||||
(`injectWorkspacePackages` in pnpm-workspace.yaml), so web/server consume snapshot copies
|
||||
that only re-sync when the package's `build` script runs via pnpm
|
||||
(`syncInjectedDepsAfterScripts`). A bare `npx tsup` in packages/core updates
|
||||
`packages/core/dist` but leaves those snapshots — and any already-populated Vite dep
|
||||
cache — on the old build; if a running dev web app still serves stale core after a manual
|
||||
rebuild, delete `packages/web/node_modules/.vite` and restart.
|
||||
|
||||
Dev entry points that touch data (`pnpm dev`, `pnpm dev:server`, `pnpm penguin`) default
|
||||
to a separate data root, `~/.penguin/dev-data`, kept apart from the installed CLI/server's
|
||||
`~/.penguin/data` — hacking on the repo never mixes state with your real agents. Export
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user