#!/usr/bin/env node /** * The bundle budget. * * Two numbers, both gzipped, both enforced: * * entry chunk <= 160 kB what every visitor downloads before they see * anything, on whatever connection the boardroom * wifi is having that morning * any demo <= 90 kB a demo is lazy, so this is what clicking one * costs — and it is per demo, so the tenth demo * cannot be paid for by the first nine * * The vendor chunks (`react`, `charts`, named in vite.config.ts) are printed * but not capped. They are shared, cached across every route, and capping them * here would only produce pressure to inline them into the entry, which is the * opposite of what we want. * * Gzip, not brotli, and not raw: gzip is the floor every host actually serves, * so it is the honest number. Brotli would flatter us by about 15%. * * Run after a build: `pnpm build && node scripts/bundle-budget.mjs`. */ import fs from 'node:fs'; import path from 'node:path'; import { abs, die, dim, exists, green, gzipSize, kb, red, rel, table, yellow } from './_lib.mjs'; const ENTRY_BUDGET = 160 * 1024; const DEMO_BUDGET = 90 * 1024; const DIST = abs('dist'); const ASSETS = path.join(DIST, 'assets'); const INDEX = path.join(DIST, 'index.html'); if (!exists(INDEX)) die(`${rel(INDEX)} does not exist. Run \`pnpm build\` first.`); if (!exists(ASSETS)) die(`${rel(ASSETS)} does not exist. The build produced no assets, which is itself the bug.`); /** * The entry is whatever `index.html` loads directly. * * Derived rather than matched by filename: Vite's entry is `index-.js` * today, but the moment someone renames the entry in rollupOptions, a * name-matching version of this script starts measuring nothing and passing. */ const indexHtml = fs.readFileSync(INDEX, 'utf8'); const entryNames = new Set( [...indexHtml.matchAll(/]+type=["']module["'][^>]+src=["']([^"']+)["']/g)].map((m) => path.basename(m[1])), ); if (entryNames.size === 0) { die(`no module