ci(ci): add bundle size reporting workflow (#511)

This commit is contained in:
rahim
2026-02-12 21:39:33 +11:00
committed by GitHub
parent 9f1c9a946d
commit 98ee4e8405
4 changed files with 264 additions and 0 deletions
+139
View File
@@ -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 = '<!-- bundle-size-report -->';
const body = [
marker,
'### 📦 Bundle Size Report',
'',
'| Package | Base | PR | Delta | |',
'|---|---|---|---|---|',
...rows,
`| **Total** | **${base.length ? formatBytes(baseTotal) : '—'}** | **${formatBytes(total)}** | **${base.length ? formatDelta(total, baseTotal) : '—'}** | |`,
'',
'<details>',
'<summary>️ How to interpret</summary>',
'',
'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.',
'</details>',
].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,
});
}
+37
View File
@@ -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": "*"
}
]
+3
View File
@@ -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",
+85
View File
@@ -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: