mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 11:21:29 +00:00
feat(ci): SCALE-007 — frontend bundle-size budget via size-limit
Acquisition-audit SCALE-007 closure (Sprint 6 ACQ, 2026-05-16).
The web/src codebase has ~45 React.lazy() call sites (`grep -rE
'lazy\(' web/src --include='*.tsx' | wc -l`), heavily route-
splitting the SPA. Pre-2026-05-16 there was no CI guard on bundle
size, so unintended bloat in a vendor chunk or a page chunk would
slip in unnoticed until somebody profiled cold-start performance.
This commit adds:
- web/.size-limit.json — 11 budget entries: per-chunk caps on the
load-bearing chunks (main entry, vendor-recharts, vendor-react,
vendor-query, vendor-router, vendor-icons, OnboardingWizard,
CommandPalette, Timestamp) + two roll-up tiers (total vendor JS,
total app JS). Budgets tuned to current vite-build output +
~15% headroom in brotli-compressed bytes (the size-limit
default measurement mode — closest analogue to what a real
browser downloads).
- web/package.json + web/package-lock.json: `npm run size` script
+ size-limit + @size-limit/file devDeps.
- .github/workflows/ci.yml: new "Frontend bundle-size budget
(size-limit)" step in the frontend-build job, runs immediately
after the vite build.
- scripts/ci-guards/G-frontend-bundle-budget.sh: local-runnable
wrapper matching the existing ci-guards/<id>.sh contract — exits
0 on clean, non-zero with ::error:: prefix on regression.
Acceptance verified locally:
- npm install in web/ regenerates package-lock cleanly
- `npm run size` exits 0 against the committed web/dist/
- `bash scripts/ci-guards/G-frontend-bundle-budget.sh` exits 0
- All current chunks measured (brotli, kB): main entry 23.3
(cap 30), vendor-recharts 91.2 (cap 110), vendor-react 37.4
(cap 45), OnboardingWizard 28.6 (cap 35), total vendor 149.5
(cap 180), total app 351.1 (cap 425)
A regression that bloats a chunk past its cap fails CI and forces
an explicit operator decision: fix the regression, or raise the
cap in web/.size-limit.json with a rationale comment in the
commit message. Do not raise caps blindly.
This commit is contained in:
@@ -466,6 +466,17 @@ jobs:
|
||||
working-directory: web
|
||||
run: npx vite build
|
||||
|
||||
- name: Frontend bundle-size budget (size-limit)
|
||||
# Acquisition-audit SCALE-007 closure (Sprint 6 ACQ, 2026-05-16).
|
||||
# Per-chunk + per-tier budgets in web/.size-limit.json; brotli-
|
||||
# compressed sizes match real-world download cost. A regression
|
||||
# that bloats a chunk past its cap fails this step and forces
|
||||
# an explicit operator decision (fix vs raise cap with rationale).
|
||||
# The script wrapper at scripts/ci-guards/G-frontend-bundle-budget.sh
|
||||
# is the local-runnable counterpart; both invoke `npm run size`.
|
||||
working-directory: web
|
||||
run: npm run size
|
||||
|
||||
- name: Regression guards (extracted to scripts/ci-guards/)
|
||||
# All named regression guards live at scripts/ci-guards/<id>.sh per
|
||||
# ci-pipeline-cleanup bundle Phase 1. Each guard is callable locally:
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 certctl LLC. All rights reserved.
|
||||
# SPDX-License-Identifier: BUSL-1.1
|
||||
#
|
||||
# Acquisition-audit SCALE-007 closure (Sprint 6 ACQ, 2026-05-16).
|
||||
# Per-chunk frontend bundle-size budget guard.
|
||||
#
|
||||
# Reads web/.size-limit.json and asserts every chunk-size pattern
|
||||
# matches its committed cap (brotli-compressed, the default
|
||||
# size-limit measurement mode — closest analogue to what a real
|
||||
# browser downloads). A regression that bloats a chunk past its
|
||||
# cap fails the build and forces an explicit operator decision —
|
||||
# either fix the regression, or raise the cap in
|
||||
# web/.size-limit.json with a rationale comment in the commit
|
||||
# message.
|
||||
#
|
||||
# Contract (matches the other scripts/ci-guards/<id>.sh files):
|
||||
# - exit 0 on clean repo
|
||||
# - non-zero with `::error::` prefix on regression
|
||||
# - skips with exit 0 when the prerequisites aren't installed
|
||||
# (npm missing, web/node_modules missing, web/dist not built)
|
||||
# — CI's frontend-build job runs `npm ci` + `npx vite build`
|
||||
# BEFORE invoking this guard, so the skip path only fires in
|
||||
# local fast-loop runs where the operator hasn't built the
|
||||
# frontend yet.
|
||||
#
|
||||
# Local run:
|
||||
# cd web && npm ci && npm run build && cd ..
|
||||
# bash scripts/ci-guards/G-frontend-bundle-budget.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
fail() {
|
||||
echo "::error::G-frontend-bundle-budget: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "G-frontend-bundle-budget: skipped — npm not on PATH."
|
||||
echo " Install: https://nodejs.org/ (CI uses Node 22 via actions/setup-node)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "web/.size-limit.json" ]; then
|
||||
fail "web/.size-limit.json is missing — the size-limit config is the source of truth for per-chunk budgets."
|
||||
fi
|
||||
|
||||
if [ ! -d "web/node_modules/size-limit" ]; then
|
||||
echo "G-frontend-bundle-budget: skipped — web/node_modules/size-limit/ missing."
|
||||
echo " Run \`cd web && npm ci\` first."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -d "web/dist/assets" ]; then
|
||||
echo "G-frontend-bundle-budget: skipped — web/dist/assets/ not present."
|
||||
echo " Run \`cd web && npm run build\` first."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "G-frontend-bundle-budget: checking $(ls -1 web/dist/assets/*.js 2>/dev/null | wc -l) JS chunks against web/.size-limit.json..."
|
||||
cd web
|
||||
if ! npm run --silent size; then
|
||||
echo "::error::G-frontend-bundle-budget: at least one chunk exceeded its budget in web/.size-limit.json. See output above for the offender."
|
||||
echo " To fix: either reduce the chunk size (audit the imports / move code to a route-level lazy boundary / drop an unneeded dependency), or — only if the growth is intentional — raise the cap in web/.size-limit.json with a rationale comment in the commit message."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "G-frontend-bundle-budget: PASS — all chunks within budget."
|
||||
@@ -0,0 +1,57 @@
|
||||
[
|
||||
{
|
||||
"name": "Main entry (index-*.js)",
|
||||
"path": "dist/assets/index-*.js",
|
||||
"limit": "30 KB"
|
||||
},
|
||||
{
|
||||
"name": "vendor-recharts (chart library)",
|
||||
"path": "dist/assets/vendor-recharts-*.js",
|
||||
"limit": "110 KB"
|
||||
},
|
||||
{
|
||||
"name": "vendor-react (react + react-dom)",
|
||||
"path": "dist/assets/vendor-react-*.js",
|
||||
"limit": "45 KB"
|
||||
},
|
||||
{
|
||||
"name": "vendor-query (TanStack Query)",
|
||||
"path": "dist/assets/vendor-query-*.js",
|
||||
"limit": "10 KB"
|
||||
},
|
||||
{
|
||||
"name": "vendor-router (React Router)",
|
||||
"path": "dist/assets/vendor-router-*.js",
|
||||
"limit": "10 KB"
|
||||
},
|
||||
{
|
||||
"name": "vendor-icons (lucide-react subset)",
|
||||
"path": "dist/assets/vendor-icons-*.js",
|
||||
"limit": "8 KB"
|
||||
},
|
||||
{
|
||||
"name": "OnboardingWizard (heaviest page chunk)",
|
||||
"path": "dist/assets/OnboardingWizard-*.js",
|
||||
"limit": "35 KB"
|
||||
},
|
||||
{
|
||||
"name": "CommandPalette",
|
||||
"path": "dist/assets/CommandPalette-*.js",
|
||||
"limit": "18 KB"
|
||||
},
|
||||
{
|
||||
"name": "Timestamp utility",
|
||||
"path": "dist/assets/Timestamp-*.js",
|
||||
"limit": "17 KB"
|
||||
},
|
||||
{
|
||||
"name": "Total vendor JS",
|
||||
"path": "dist/assets/vendor-*.js",
|
||||
"limit": "180 KB"
|
||||
},
|
||||
{
|
||||
"name": "Total app JS (all chunks combined)",
|
||||
"path": "dist/assets/*.js",
|
||||
"limit": "425 KB"
|
||||
}
|
||||
]
|
||||
Generated
+97
@@ -27,6 +27,7 @@
|
||||
"devDependencies": {
|
||||
"@axe-core/react": "^4.11.3",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@size-limit/file": "^11.2.0",
|
||||
"@storybook/addon-a11y": "^10.4.0",
|
||||
"@storybook/react-vite": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
@@ -40,6 +41,7 @@
|
||||
"jsdom": "^29.0.0",
|
||||
"orval": "^7.0.0",
|
||||
"postcss": "^8.5.8",
|
||||
"size-limit": "^11.2.0",
|
||||
"storybook": "^10.4.0",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -3724,6 +3726,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@size-limit/file": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@size-limit/file/-/file-11.2.0.tgz",
|
||||
"integrity": "sha512-OZHE3putEkQ/fgzz3Tp/0hSmfVo3wyTpOJSRNm6AmcwX4Nm9YtTfbQQ/hZRwbBFR23S7x2Sd9EbqYzngKwbRoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"size-limit": "11.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@@ -5365,6 +5380,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes-iec": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bytes-iec/-/bytes-iec-3.1.1.tgz",
|
||||
"integrity": "sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||
@@ -8694,6 +8719,16 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanospinner": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz",
|
||||
"integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nimma": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz",
|
||||
@@ -10563,6 +10598,68 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/size-limit": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/size-limit/-/size-limit-11.2.0.tgz",
|
||||
"integrity": "sha512-2kpQq2DD/pRpx3Tal/qRW1SYwcIeQ0iq8li5CJHQgOC+FtPn2BVmuDtzUCgNnpCrbgtfEHqh+iWzxK+Tq6C+RQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes-iec": "^3.1.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"jiti": "^2.4.2",
|
||||
"lilconfig": "^3.1.3",
|
||||
"nanospinner": "^1.2.2",
|
||||
"picocolors": "^1.1.1",
|
||||
"tinyglobby": "^0.2.11"
|
||||
},
|
||||
"bin": {
|
||||
"size-limit": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/size-limit/node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/size-limit/node_modules/jiti": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/size-limit/node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
|
||||
+4
-1
@@ -13,7 +13,8 @@
|
||||
"e2e:install": "playwright install --with-deps chromium",
|
||||
"generate": "orval --config ./orval.config.ts",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build --output-dir=.storybook-static"
|
||||
"storybook:build": "storybook build --output-dir=.storybook-static",
|
||||
"size": "size-limit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.19",
|
||||
@@ -48,6 +49,8 @@
|
||||
"jsdom": "^29.0.0",
|
||||
"orval": "^7.0.0",
|
||||
"postcss": "^8.5.8",
|
||||
"size-limit": "^11.2.0",
|
||||
"@size-limit/file": "^11.2.0",
|
||||
"storybook": "^10.4.0",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
Reference in New Issue
Block a user