138 lines
4.4 KiB
JavaScript
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);
|
|
});
|
|
}
|