The cleanStaleChunks plugin lives in the shared vite config, so its closeBundle hook also fired under `vp test` — where generateBundle never runs, produced stayed empty, and the sweep deleted every hashed chunk in web/static (28 files → 3). The entries still referenced their old imports, so every page 404'd and Alpine never started: the whole reader and media system appeared dead after merely running the test suite. Guard: skip the sweep when produced.size === 0 — not a build run, no build output to clean. Verified: build → 28 files, test → 28 files; back-to-back builds stay byte-identical.
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { defineConfig } from "vite-plus";
|
|
import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const pdfjsAssets = () => ({
|
|
name: "pdfjs-assets",
|
|
closeBundle() {
|
|
const outDir = "web/static/vendor/pdfjs";
|
|
const srcDir = "node_modules/@bookhoard/foliate-js/vendor/pdfjs";
|
|
mkdirSync(outDir, { recursive: true });
|
|
cpSync(join(srcDir, "standard_fonts"), join(outDir, "standard_fonts"), {
|
|
recursive: true,
|
|
});
|
|
cpSync(join(srcDir, "cmaps"), join(outDir, "cmaps"), { recursive: true });
|
|
},
|
|
});
|
|
|
|
// emptyOutDir must stay false (web/static also holds tracked assets),
|
|
// so hashed chunks from previous builds would otherwise accumulate
|
|
// forever and leak into Docker images via the build context. Remove
|
|
// any *-<hash>.js/.css/.mjs(.map) that this build did not produce.
|
|
//
|
|
// The produced.size === 0 guard is load-bearing: this config also drives
|
|
// `vp test`, and vitest runs closeBundle with generateBundle never having
|
|
// fired — an empty produced set means "not a build run", and sweeping
|
|
// then deleted every live chunk (the 2026-10-21 incident where
|
|
// `npm run test` gutted web/static down to the three entry files and
|
|
// every page 404'd its imports).
|
|
const cleanStaleChunks = () => {
|
|
const produced = new Set<string>();
|
|
return {
|
|
name: "clean-stale-chunks",
|
|
generateBundle(_options, bundle) {
|
|
for (const fileName of Object.keys(bundle)) produced.add(fileName);
|
|
},
|
|
closeBundle() {
|
|
if (produced.size === 0) return;
|
|
const outDir = "web/static";
|
|
const chunkRe = /-[A-Za-z0-9_-]{8}\.(js|css|mjs)(\.map)?$/;
|
|
for (const f of readdirSync(outDir)) {
|
|
if (chunkRe.test(f) && !produced.has(f)) {
|
|
rmSync(join(outDir, f));
|
|
}
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
export default defineConfig({
|
|
// web/static holds vendored and build-output assets (htmx.min.js,
|
|
// compiled css) that must never be reformatted; only web/src is fmt scope.
|
|
fmt: {
|
|
ignorePatterns: ["web/static/**"],
|
|
},
|
|
test: {
|
|
environment: "jsdom",
|
|
include: ["web/src/**/*.test.ts"],
|
|
},
|
|
resolve: {
|
|
alias: {
|
|
"@": "/web/src",
|
|
"foliate-js": "/node_modules/@bookhoard/foliate-js",
|
|
},
|
|
},
|
|
base: "/static/",
|
|
plugins: [pdfjsAssets(), cleanStaleChunks()],
|
|
build: {
|
|
outDir: "web/static",
|
|
emptyOutDir: false,
|
|
assetsDir: "",
|
|
rollupOptions: {
|
|
input: {
|
|
main: "web/src/main.ts",
|
|
reader: "web/src/reader/reader.ts",
|
|
},
|
|
output: {
|
|
entryFileNames: "[name].js",
|
|
},
|
|
},
|
|
target: "esnext",
|
|
sourcemap: true,
|
|
minify: true,
|
|
},
|
|
});
|