Files
HealthLog/scripts/print-bundle-report.mjs
T
Marc-André Bombeck 8e05f9227d feat(http): centralise authed Cache-Control with bfcache-friendly preset
Add `src/lib/http/cache-headers.ts` exporting two presets and one
applier. `NO_STORE_BUT_BFCACHE` (`private, max-age=0, must-revalidate`)
replaces the framework's stock `no-store, must-revalidate` on
authenticated page responses; Chromium treats the latter as a hard
bfcache breaker, while the former keeps shared caches out, forces
revalidation on every navigation, and stays bfcache-eligible.
`SHORT_LIVED_PUBLIC` covers static-ish one-hour public payloads.
`applyAuthedHeaders(res)` stamps the bfcache-friendly directive onto
a Response in-place for route handlers that opt into the same posture.

Also add `scripts/print-bundle-report.mjs` (wired earlier as the
`bundle-report` npm script). Turbopack dropped the webpack "First
Load JS" summary line from `next build`; the script reads back the
`.next/analyze/client.json` payload that `@next/bundle-analyzer`
writes when `pnpm analyze` runs and prints a sorted table of the
largest client chunks (parsed, gzip, stat sizes plus totals). Output
restores an at-a-glance signal for client-bundle regressions on
release rounds without re-running Lighthouse.
2026-05-16 20:26:59 +02:00

119 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
/**
* Reads `.next/analyze/client.json` (emitted when `pnpm analyze` runs
* `@next/bundle-analyzer` with `ANALYZE=1`) and prints a sorted table
* of the largest client chunks. Turbopack dropped the webpack
* "First Load JS" summary line from `next build`; this script restores
* an equivalent at-a-glance signal so release rounds can spot
* client-bundle regressions without re-running Lighthouse.
*
* Usage:
* pnpm analyze # writes .next/analyze/client.json
* pnpm bundle-report # reads it back, prints top chunks
*
* Output columns: rank, chunk label, parsed (post-minify) size, gzip
* size, stat (pre-minify) size. Sizes report in KiB to two decimals.
* Exits 0 unless `client.json` is missing or unreadable.
*/
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
const REPORT_PATH = resolve(process.cwd(), ".next", "analyze", "client.json");
const DEFAULT_TOP_N = 10;
function formatKib(bytes) {
if (typeof bytes !== "number" || Number.isNaN(bytes)) return "—";
return `${(bytes / 1024).toFixed(2)} KiB`;
}
function padCell(value, width) {
const str = String(value ?? "");
return str.length >= width ? str : str + " ".repeat(width - str.length);
}
async function main() {
const argTopN = Number(process.argv[2]);
const topN =
Number.isFinite(argTopN) && argTopN > 0
? Math.floor(argTopN)
: DEFAULT_TOP_N;
let raw;
try {
raw = await readFile(REPORT_PATH, "utf8");
} catch (err) {
if (err && err.code === "ENOENT") {
console.error(
`bundle-report: ${REPORT_PATH} not found. Run \`pnpm analyze\` first.`,
);
} else {
console.error(`bundle-report: failed to read ${REPORT_PATH}:`, err);
}
process.exit(1);
}
let chunks;
try {
chunks = JSON.parse(raw);
} catch (err) {
console.error(`bundle-report: failed to parse ${REPORT_PATH}:`, err);
process.exit(1);
}
if (!Array.isArray(chunks)) {
console.error(
"bundle-report: client.json is not an array of chunks — analyzer output shape changed?",
);
process.exit(1);
}
const sorted = [...chunks].sort(
(a, b) => (b.parsedSize ?? 0) - (a.parsedSize ?? 0),
);
const top = sorted.slice(0, topN);
const totals = chunks.reduce(
(acc, c) => ({
parsed: acc.parsed + (c.parsedSize ?? 0),
gzip: acc.gzip + (c.gzipSize ?? 0),
stat: acc.stat + (c.statSize ?? 0),
}),
{ parsed: 0, gzip: 0, stat: 0 },
);
const labelWidth = Math.max(
20,
...top.map((c) => String(c.label ?? "").length),
);
const header =
`${padCell("#", 3)} ${padCell("chunk", labelWidth)} ` +
`${padCell("parsed", 12)} ${padCell("gzip", 12)} ${padCell("stat", 12)}`;
console.log(
`Top ${top.length} client chunks by parsed size (of ${chunks.length} total):`,
);
console.log(header);
console.log("-".repeat(header.length));
top.forEach((chunk, idx) => {
console.log(
`${padCell(idx + 1, 3)} ${padCell(chunk.label ?? "(unnamed)", labelWidth)} ` +
`${padCell(formatKib(chunk.parsedSize), 12)} ` +
`${padCell(formatKib(chunk.gzipSize), 12)} ` +
`${padCell(formatKib(chunk.statSize), 12)}`,
);
});
console.log("-".repeat(header.length));
console.log(
`${padCell("Σ", 3)} ${padCell("all chunks", labelWidth)} ` +
`${padCell(formatKib(totals.parsed), 12)} ` +
`${padCell(formatKib(totals.gzip), 12)} ` +
`${padCell(formatKib(totals.stat), 12)}`,
);
}
main().catch((err) => {
console.error("bundle-report: unexpected failure:", err);
process.exit(1);
});