#!/usr/bin/env node // Bundle size budget (Gitea #96). The CI "Report bundle sizes" step printed // numbers with nothing to compare them against, so a bundle could balloon // silently. This script compares dist/assets gzip sizes against a small // checked-in budget (scripts/bundle-budget.json) and prints the same report. // // Mode is passed via argv: `pull_request` fails the build over budget, // anything else (e.g. `push`) only warns — a push has already merged, so // blocking it can't prevent the regression, only delay the deploy of an // otherwise-good commit; the pull_request gate is where this should be caught. import { appendFileSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { gzipSync } from 'node:zlib'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const mode = process.argv[2] || 'push'; const distAssetsDir = path.join(__dirname, '..', 'dist', 'assets'); const budgetPath = path.join(__dirname, 'bundle-budget.json'); function formatKb(bytes) { return `${(bytes / 1024).toFixed(1)} kB`; } function main() { const budget = JSON.parse(readFileSync(budgetPath, 'utf-8')); const jsFiles = readdirSync(distAssetsDir) .filter((f) => f.endsWith('.js') && !f.endsWith('.map')) .sort(); if (jsFiles.length === 0) { console.error( `[check-bundle-size] No .js files found in ${distAssetsDir} — did the build run?`, ); process.exitCode = 1; return; } const rows = jsFiles.map((name) => { const filePath = path.join(distAssetsDir, name); const size = statSync(filePath).size; const gzipSize = gzipSync(readFileSync(filePath)).length; return { name, size, gzipSize }; }); const totalGzipBytes = rows.reduce((sum, r) => sum + r.gzipSize, 0); const largest = rows.reduce((max, r) => (r.gzipSize > max.gzipSize ? r : max), rows[0]); const summaryLines = [ '### Bundle sizes', '', '| File | Size | Gzip |', '|------|------|------|', ...rows.map((r) => `| ${r.name} | ${formatKb(r.size)} | ${formatKb(r.gzipSize)} |`), '', `**Total gzip:** ${formatKb(totalGzipBytes)} (budget ${formatKb(budget.totalGzipBytes)})`, `**Largest chunk gzip:** ${largest.name} — ${formatKb(largest.gzipSize)} (budget ${formatKb( budget.largestChunkGzipBytes, )})`, ]; const summaryText = summaryLines.join('\n'); console.log(summaryText); if (process.env.GITHUB_STEP_SUMMARY) { // Gitea Actions/act_runner honors the same GITHUB_STEP_SUMMARY convention. appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summaryText}\n`); } const overBudget = []; if (totalGzipBytes > budget.totalGzipBytes) { overBudget.push( `total gzip ${formatKb(totalGzipBytes)} exceeds budget ${formatKb(budget.totalGzipBytes)}`, ); } if (largest.gzipSize > budget.largestChunkGzipBytes) { overBudget.push( `largest chunk (${largest.name}) gzip ${formatKb(largest.gzipSize)} exceeds budget ${formatKb( budget.largestChunkGzipBytes, )}`, ); } if (overBudget.length > 0) { const message = `[check-bundle-size] Over budget: ${overBudget.join('; ')}`; if (mode === 'pull_request') { console.error(message); process.exitCode = 1; } else { console.warn(`${message} (warning only on "${mode}")`); } } else { console.log('[check-bundle-size] Within budget.'); } } main();