From 98ee4e840554821209406524dfb9bce9719ab7b0 Mon Sep 17 00:00:00 2001 From: rahim Date: Thu, 12 Feb 2026 21:39:33 +1100 Subject: [PATCH] ci(ci): add bundle size reporting workflow (#511) --- .github/workflows/bundle-size.yml | 139 ++++++++++++++++++++++++++++++ .size-limit.json | 37 ++++++++ package.json | 3 + pnpm-lock.yaml | 85 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 .github/workflows/bundle-size.yml create mode 100644 .size-limit.json diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml new file mode 100644 index 00000000..df1c8388 --- /dev/null +++ b/.github/workflows/bundle-size.yml @@ -0,0 +1,139 @@ +name: Bundle Size + +on: + pull_request: + branches: + - main + +permissions: + pull-requests: write + +jobs: + size: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR + uses: actions/checkout@v5 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: pnpm + + - name: Install and build PR + run: pnpm install && pnpm build:packages + + - name: Measure PR bundle size + run: pnpm -w exec size-limit --json > /tmp/pr-size.json + + - name: Measure base bundle size + run: | + git fetch origin ${{ github.base_ref }} --depth=1 + git checkout -f FETCH_HEAD + + pnpm install + + if [ -f .size-limit.json ]; then + pnpm build:packages + pnpm -w exec size-limit --json > /tmp/base-size.json + else + echo '[]' > /tmp/base-size.json + fi + + - name: Report + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const pr = JSON.parse(fs.readFileSync('/tmp/pr-size.json', 'utf8')); + const base = JSON.parse(fs.readFileSync('/tmp/base-size.json', 'utf8')); + + const baseMap = Object.fromEntries(base.map(e => [e.name, e.size])); + + function formatBytes(bytes) { + if (bytes < 1024) return `${bytes} B`; + return `${(bytes / 1024).toFixed(2)} kB`; + } + + function formatDelta(current, previous) { + if (previous === undefined) return '—'; + const diff = current - previous; + if (diff === 0) return '0 B'; + const sign = diff > 0 ? '+' : ''; + const pct = ((diff / previous) * 100).toFixed(1); + return `${sign}${formatBytes(diff)} (${sign}${pct}%)`; + } + + function statusIcon(current, previous) { + if (previous === undefined) return '🆕'; + const diff = current - previous; + if (diff === 0) return '✅'; + if (diff < 0) return 'đŸ”Ŋ'; + const pct = (diff / previous) * 100; + return pct > 10 ? '🔴' : 'đŸ”ē'; + } + + const rows = pr.map(entry => { + const prev = baseMap[entry.name]; + return `| ${entry.name} | ${prev !== undefined ? formatBytes(prev) : '—'} | ${formatBytes(entry.size)} | ${formatDelta(entry.size, prev)} | ${statusIcon(entry.size, prev)} |`; + }); + + const total = pr.reduce((sum, e) => sum + e.size, 0); + const baseTotal = base.reduce((sum, e) => sum + e.size, 0); + + const marker = ''; + const body = [ + marker, + '### đŸ“Ļ Bundle Size Report', + '', + '| Package | Base | PR | Delta | |', + '|---|---|---|---|---|', + ...rows, + `| **Total** | **${base.length ? formatBytes(baseTotal) : '—'}** | **${formatBytes(total)}** | **${base.length ? formatDelta(total, baseTotal) : '—'}** | |`, + '', + '
', + 'â„šī¸ How to interpret', + '', + 'Sizes are minified + brotli compressed, measured via [size-limit](https://github.com/ai/size-limit) with esbuild tree-shaking.', + '', + '| Icon | Meaning |', + '|---|---|', + '| ✅ | No change |', + '| đŸ”ē | Size increased ≤ 10% |', + '| 🔴 | Size increased > 10% |', + '| đŸ”Ŋ | Size decreased |', + '| 🆕 | New entry (no baseline) |', + '', + 'Run `pnpm size` locally to check current sizes.', + '
', + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body?.startsWith(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/.size-limit.json b/.size-limit.json new file mode 100644 index 00000000..9f36a8bd --- /dev/null +++ b/.size-limit.json @@ -0,0 +1,37 @@ +[ + { + "name": "@videojs/store", + "path": "packages/store/dist/default/index.js", + "import": "*" + }, + { + "name": "@videojs/store/lit", + "path": "packages/store/dist/default/lit.js", + "import": "*" + }, + { + "name": "@videojs/store/react", + "path": "packages/store/dist/default/react.js", + "import": "*" + }, + { + "name": "@videojs/core", + "path": "packages/core/dist/default/index.js", + "import": "*" + }, + { + "name": "@videojs/core/dom", + "path": "packages/core/dist/default/dom.js", + "import": "*" + }, + { + "name": "@videojs/react", + "path": "packages/react/dist/default/index.js", + "import": "*" + }, + { + "name": "@videojs/html", + "path": "packages/html/dist/default/index.js", + "import": "*" + } +] diff --git a/package.json b/package.json index ee73056e..f5b72a57 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,12 @@ "lint:fix:file": "biome check --write", "link:opencode": "[ -e .opencode ] || ln -s .claude .opencode", "link:agents": "[ -e AGENTS.md ] || ln -s CLAUDE.md AGENTS.md; [ -e agents ] || ln -s .claude agents", + "size": "size-limit", "test": "turbo run test", "typecheck": "tsc --build" }, "devDependencies": { + "@size-limit/preset-small-lib": "^12.0.0", "@biomejs/biome": "^2.3.11", "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", @@ -43,6 +45,7 @@ "react": "^18.0.0", "react-compiler-runtime": "^1.0.0", "react-dom": "^18.0.0", + "size-limit": "^12.0.0", "simple-git-hooks": "^2.13.1", "tsx": "^4.21.0", "turbo": "^2.5.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3a9946e..555245d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@commitlint/format': specifier: ^20.0.0 version: 20.2.0 + '@size-limit/preset-small-lib': + specifier: ^12.0.0 + version: 12.0.0(size-limit@12.0.0(jiti@2.6.1)) '@types/node': specifier: ^22.18.6 version: 22.19.3 @@ -38,6 +41,9 @@ importers: simple-git-hooks: specifier: ^2.13.1 version: 2.13.1 + size-limit: + specifier: ^12.0.0 + version: 12.0.0(jiti@2.6.1) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -2500,6 +2506,23 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@size-limit/esbuild@12.0.0': + resolution: {integrity: sha512-r9i+HrtunIu7wAPtqD3t4DqfYin3kxPoMAv8cidkzlCS69IYCe3EG2UbQa10AdvQyaHTEK+MPkr9ifUd3W29og==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + size-limit: 12.0.0 + + '@size-limit/file@12.0.0': + resolution: {integrity: sha512-OzKYpDzWJ2jo6cAIzVsaPuvzZTmMLDoVCViEvsctmImxpXzwJZcuBEpPohFKKdgVdZuNTU8WstmvywPq55Njdw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + size-limit: 12.0.0 + + '@size-limit/preset-small-lib@12.0.0': + resolution: {integrity: sha512-HHHVQjZmj+8vg7qsHs1dd3Hmn8ygUsE5O2CfxnbCbHOGyUw7VodZGERh/+5ogVrF2DYza/DIo2PnCJZZETdTRA==} + peerDependencies: + size-limit: 12.0.0 + '@so-ric/colorspace@1.1.6': resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} @@ -3274,6 +3297,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bytes-iec@3.1.1: + resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -4706,6 +4733,10 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5130,6 +5161,14 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + nanospinner@1.2.2: + resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==} + nanostores@1.1.0: resolution: {integrity: sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==} engines: {node: ^20.0.0 || >=22.0.0} @@ -5879,6 +5918,16 @@ packages: engines: {node: '>=14.0.0', npm: '>=6.0.0'} hasBin: true + size-limit@12.0.0: + resolution: {integrity: sha512-JBG8dioIs0m2kHOhs9jD6E/tZKD08vmbf2bfqj/rJyNWqJxk/ZcakixjhYtsqdbi+AKVbfPkt3g2RRZiKaizYA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + jiti: ^2.0.0 + peerDependenciesMeta: + jiti: + optional: true + slashes@3.0.12: resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} @@ -9075,6 +9124,22 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@size-limit/esbuild@12.0.0(size-limit@12.0.0(jiti@2.6.1))': + dependencies: + esbuild: 0.27.2 + nanoid: 5.1.6 + size-limit: 12.0.0(jiti@2.6.1) + + '@size-limit/file@12.0.0(size-limit@12.0.0(jiti@2.6.1))': + dependencies: + size-limit: 12.0.0(jiti@2.6.1) + + '@size-limit/preset-small-lib@12.0.0(size-limit@12.0.0(jiti@2.6.1))': + dependencies: + '@size-limit/esbuild': 12.0.0(size-limit@12.0.0(jiti@2.6.1)) + '@size-limit/file': 12.0.0(size-limit@12.0.0(jiti@2.6.1)) + size-limit: 12.0.0(jiti@2.6.1) + '@so-ric/colorspace@1.1.6': dependencies: color: 5.0.3 @@ -10095,6 +10160,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bytes-iec@3.1.1: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -11616,6 +11683,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} lint-staged@16.2.7: @@ -12297,6 +12366,12 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.6: {} + + nanospinner@1.2.2: + dependencies: + picocolors: 1.1.1 + nanostores@1.1.0: {} neotraverse@0.6.18: {} @@ -13163,6 +13238,16 @@ snapshots: arg: 5.0.2 sax: 1.4.3 + size-limit@12.0.0(jiti@2.6.1): + dependencies: + bytes-iec: 3.1.1 + lilconfig: 3.1.3 + nanospinner: 1.2.2 + picocolors: 1.1.1 + tinyglobby: 0.2.15 + optionalDependencies: + jiti: 2.6.1 + slashes@3.0.12: {} slice-ansi@7.1.2: