#!/usr/bin/env node // Renders the markdown body for the Bundle-Budget PR comment. // // Inputs: // --current JSON produced by `verify-bundle-size.mjs --json` // --baseline Optional baseline JSON from main branch (for Δ column). // Missing/unreadable → Δ column shows "—". // --out Write rendered markdown here (default: bundle-comment.md) // // Output marker: the comment starts with `` 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(""); 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 [--baseline ] [--out ]"); 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); }); }