Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/render-bundle-budget-comment.mjs
T

138 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
// Renders the markdown body for the Bundle-Budget PR comment.
//
// Inputs:
// --current <path> JSON produced by `verify-bundle-size.mjs --json`
// --baseline <path> Optional baseline JSON from main branch (for Δ column).
// Missing/unreadable → Δ column shows "—".
// --out <path> Write rendered markdown here (default: bundle-comment.md)
//
// Output marker: the comment starts with `<!-- bundle-budget-report -->` so
// downstream upsert steps can identify and replace it without duplication.
import fs from "node:fs";
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--current") out.current = argv[++i];
else if (arg === "--baseline") out.baseline = argv[++i];
else if (arg === "--out") out.out = argv[++i];
}
return out;
}
export function readReport(filePath) {
if (!filePath) return null;
try {
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf8");
if (!raw.trim()) return null;
return JSON.parse(raw);
} catch {
return null;
}
}
const STATUS_BADGE = {
ok: "[OK]",
warning: "[WARN]",
exceeds: "[EXCEED]",
unbudgeted: "[?]",
};
const KIND_LABEL = {
route: "route",
vendor: "vendor",
};
export function formatKb(bytes) {
if (typeof bytes !== "number") return "—";
return `${(bytes / 1024).toFixed(1)} KB`;
}
export function formatDelta(currentBytes, baselineBytes) {
if (typeof baselineBytes !== "number") return "—";
const diff = currentBytes - baselineBytes;
const sign = diff >= 0 ? "+" : "";
return `${sign}${(diff / 1024).toFixed(1)} KB`;
}
export function indexBaseline(report) {
const map = new Map();
if (!report?.entries) return map;
for (const e of report.entries) {
map.set(e.key, e);
}
return map;
}
export function buildMarkdown(current, baseline) {
const lines = [];
lines.push("<!-- bundle-budget-report -->");
lines.push("## Bundle Budget Report");
lines.push("");
if (!current || current.skipped) {
lines.push("> No bundle artifacts measured (web build skipped or empty).");
return lines.join("\n");
}
const totals = current.totals ?? { ok: 0, warning: 0, exceeds: 0, unbudgeted: 0 };
lines.push(
`**Summary:** ok=${totals.ok} - warning=${totals.warning} - exceeds=${totals.exceeds} - unbudgeted=${totals.unbudgeted}`,
);
lines.push("");
if (totals.exceeds > 0) {
lines.push(
"> Some routes exceed their configured limit. This does **not** block the PR, but please check whether the growth is intentional.",
);
lines.push("");
}
const baselineIndex = indexBaseline(baseline);
const entries = [...(current.entries ?? [])];
// sort: exceeds → warning → unbudgeted → ok, then biggest first
const rank = { exceeds: 0, warning: 1, unbudgeted: 2, ok: 3 };
entries.sort((a, b) => {
if (rank[a.status] !== rank[b.status]) return rank[a.status] - rank[b.status];
return b.gzipSize - a.gzipSize;
});
lines.push("| Status | Kind | Route / Chunk | Size (gzip) | Limit | Δ from main |");
lines.push("|---|---|---|---|---|---|");
for (const e of entries) {
const status = STATUS_BADGE[e.status] ?? e.status;
const kind = KIND_LABEL[e.kind] ?? e.kind;
const limit = typeof e.limitKb === "number" ? `${e.limitKb} KB` : "default";
const base = baselineIndex.get(e.key);
const delta = formatDelta(e.gzipSize, base?.gzipSize);
lines.push(`| ${status} | ${kind} | \`${e.key}\` | ${formatKb(e.gzipSize)} | ${limit} | ${delta} |`);
}
lines.push("");
lines.push("_Sizes are gzip-compressed. Budgets configured in `scripts/bundle-budgets.json`. Docs: `docs/architecture/bundle-budgets.md`._");
return lines.join("\n");
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.current) {
console.error("Usage: render-bundle-budget-comment.mjs --current <path> [--baseline <path>] [--out <path>]");
process.exit(2);
}
const current = readReport(args.current);
const baseline = readReport(args.baseline);
const md = buildMarkdown(current, baseline);
const outPath = args.out ?? "bundle-comment.md";
fs.writeFileSync(outPath, `${md}\n`, "utf8");
// Also surface on stdout for log visibility.
process.stdout.write(`${md}\n`);
}
const isMain = import.meta.url === `file://${process.argv[1]}`;
if (isMain) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}