|
| 1 | +#!/usr/bin/env node |
| 2 | +// Bundle-size budget check. |
| 3 | +// Numbers are pre-gzip raw byte size; gzip ≈ 1/3 of these. Tuned so we |
| 4 | +// catch surprise regressions but don't fail on routine new features. |
| 5 | +// Bump deliberately when a real feature requires it. |
| 6 | + |
| 7 | +import { readdir, stat } from "node:fs/promises"; |
| 8 | +import { resolve } from "node:path"; |
| 9 | + |
| 10 | +const DIST = resolve(process.cwd(), "dist"); |
| 11 | + |
| 12 | +const BUDGETS = [ |
| 13 | + { glob: /^providers\/chatwoot\.mjs$/, max: 9216, label: "provider:chatwoot" }, |
| 14 | + { glob: /^providers\/.+\.mjs$/, max: 6144, label: "provider" }, |
| 15 | + { glob: /^index\.mjs$/, max: 4096, label: "core" }, |
| 16 | + { glob: /^facade\.mjs$/, max: 3072, label: "facade" }, |
| 17 | + { glob: /^adapters\/.+\.mjs$/, max: 2048, label: "adapter" }, |
| 18 | + { glob: /^csp\.mjs$/, max: 6144, label: "csp" }, |
| 19 | + { glob: /^server\.mjs$/, max: 1024, label: "server" }, |
| 20 | + { glob: /^capabilities\.mjs$/, max: 2048, label: "capabilities" }, |
| 21 | + { glob: /^diagnostics\.mjs$/, max: 3072, label: "diagnostics" }, |
| 22 | +]; |
| 23 | + |
| 24 | +async function* walk(dir, prefix = "") { |
| 25 | + const entries = await readdir(dir, { withFileTypes: true }); |
| 26 | + for (const entry of entries) { |
| 27 | + const fullPath = resolve(dir, entry.name); |
| 28 | + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; |
| 29 | + if (entry.isDirectory()) { |
| 30 | + yield* walk(fullPath, rel); |
| 31 | + } else if (entry.name.endsWith(".mjs")) { |
| 32 | + yield { path: fullPath, rel }; |
| 33 | + } |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +const failures = []; |
| 38 | +const summary = []; |
| 39 | + |
| 40 | +for await (const file of walk(DIST)) { |
| 41 | + const { size } = await stat(file.path); |
| 42 | + const budget = BUDGETS.find((b) => b.glob.test(file.rel)); |
| 43 | + if (!budget) continue; |
| 44 | + const ok = size <= budget.max; |
| 45 | + summary.push({ rel: file.rel, size, max: budget.max, label: budget.label, ok }); |
| 46 | + if (!ok) failures.push(`${file.rel} ${size}B > ${budget.max}B (${budget.label})`); |
| 47 | +} |
| 48 | + |
| 49 | +summary.sort((a, b) => b.size - a.size); |
| 50 | +console.log("\nBundle sizes (raw, pre-gzip):"); |
| 51 | +console.log("─".repeat(60)); |
| 52 | +for (const s of summary) { |
| 53 | + const status = s.ok ? "✓" : "✗"; |
| 54 | + const pct = Math.round((s.size / s.max) * 100); |
| 55 | + console.log(` ${status} ${s.rel.padEnd(40)} ${String(s.size).padStart(6)}B ${pct}%`); |
| 56 | +} |
| 57 | +console.log("─".repeat(60)); |
| 58 | + |
| 59 | +if (failures.length > 0) { |
| 60 | + console.error("\n❌ Bundle budget exceeded:"); |
| 61 | + for (const f of failures) console.error(` ${f}`); |
| 62 | + process.exit(1); |
| 63 | +} |
| 64 | +console.log("\n✅ All bundles within budget."); |
0 commit comments