diff --git a/.github/scripts/bundle-size-report.js b/.github/scripts/bundle-size-report.js index a9d3bf13..b2dfbc86 100644 --- a/.github/scripts/bundle-size-report.js +++ b/.github/scripts/bundle-size-report.js @@ -7,8 +7,8 @@ * When --base is omitted, generates a local report showing current sizes. * When --base is provided, generates a comparison report with diffs. * - * Reads JSON arrays of { name, size, type, category?, format } entries - * produced by bundle-size.js. + * Reads JSON arrays of { name, size, type, category?, format, standaloneSize? } + * entries produced by bundle-size.js. */ import { readFileSync } from 'node:fs'; @@ -175,8 +175,24 @@ function generateFlatBreakdown(entries, pkg) { function generateComparisonReport(current, base) { const baseMap = Object.fromEntries(base.map((e) => [e.name, e.size])); + const currentMap = Object.fromEntries(current.map((e) => [e.name, e.size])); + + // Standalone size lookups — used to gate UI component diffs. + // UI marginal sizes shift when root content changes (brotli compression is + // non-linear), so we compare standalone sizes to decide IF a component + // actually changed, then display the marginal diff. + const baseStandaloneMap = Object.fromEntries( + base.map((e) => [e.name, e.standaloneSize ?? e.size]), + ); + const currentStandaloneMap = Object.fromEntries( + current.map((e) => [e.name, e.standaloneSize ?? e.size]), + ); + const groups = groupByPackage(current); + // Build a set of all base entries grouped by package so we can detect removals + const baseGroups = groupByPackage(base); + const lines = []; const pkgIcons = { core: '🧩', @@ -191,19 +207,38 @@ function generateComparisonReport(current, base) { lines.push('# 📦 Bundle Size Report'); lines.push(''); - for (const [pkg, entries] of groups) { + // Collect all package names from both sides + const allPackages = new Set([...groups.keys(), ...baseGroups.keys()]); + const orderedPackages = []; + for (const pkg of PACKAGE_ORDER) { + if (allPackages.has(pkg)) orderedPackages.push(pkg); + } + for (const pkg of allPackages) { + if (!orderedPackages.includes(pkg)) orderedPackages.push(pkg); + } + + for (const pkg of orderedPackages) { + const entries = groups.get(pkg) ?? []; + const baseEntries = baseGroups.get(pkg) ?? []; const pkgIcon = pkgIcons[pkg] ?? '📦'; lines.push(`## ${pkgIcon} @videojs/${pkg}`); lines.push(''); - // Only show entries with a meaningful size change (>300 B, must exist in both) + // Entries with meaningful size changes (>300 B threshold). + // For UI components, gate on standalone size to filter out phantom diffs + // caused by brotli compression shifts when root content changes. const changed = entries.filter((e) => { - const prev = baseMap[e.name]; - if (prev === undefined) return false; - return Math.abs(e.size - prev) > 300; + const prevStandalone = baseStandaloneMap[e.name]; + // New entry — always surface + if (prevStandalone === undefined) return true; + const curStandalone = currentStandaloneMap[e.name]; + return Math.abs(curStandalone - prevStandalone) > 300; }); - if (changed.length === 0) { + // Entries that existed in base but are missing in PR (removed) + const removed = baseEntries.filter((e) => currentMap[e.name] === undefined); + + if (changed.length === 0 && removed.length === 0) { lines.push('(no changes)'); lines.push(''); } else { @@ -221,6 +256,13 @@ function generateComparisonReport(current, base) { ); } + for (const entry of removed) { + const el = entryLabel(entry.name, pkg); + lines.push( + `| ${el} | ${formatBytes(entry.size)} | — | −${formatBytes(entry.size)} | −100% | 🗑️ |`, + ); + } + lines.push(''); } diff --git a/.github/scripts/bundle-size.js b/.github/scripts/bundle-size.js index 589dd42c..0d2b80ea 100644 --- a/.github/scripts/bundle-size.js +++ b/.github/scripts/bundle-size.js @@ -151,6 +151,7 @@ function buildPresetEntry(pkgShortName, config, distDir) { * @property {'root' | 'subpath'} type * @property {string} [category] - preset, media, player, skin, ui, feature (only for html/react) * @property {'js' | 'css'} format + * @property {number} [standaloneSize] - For UI components: standalone size used for stable diff gating */ /** Bundle entry points with esbuild and return the minified + brotli size. */ @@ -475,12 +476,16 @@ async function main() { continue; } - // UI components are marginal over root (they share base element classes). - // Everything else is standalone. + // UI components are measured as marginal over root (size) for display, + // plus standalone (standaloneSize) for stable cross-build diff gating. + // Marginal sizes shift when root content changes due to brotli + // compression non-linearity, so diffs must gate on standalone. let size; + let standaloneSize; if (cat === 'ui') { const combined = await measure([pkg.rootPath, sub.path], pkg.external); - size = combined - rootSize; + size = Math.max(0, combined - rootSize); + standaloneSize = await measure([sub.path], pkg.external); } else { size = await measure([sub.path], pkg.external); } @@ -491,6 +496,7 @@ async function main() { type: 'subpath', ...(cat ? { category: cat } : {}), format: 'js', + ...(standaloneSize !== undefined ? { standaloneSize } : {}), }); } diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 434ca8fd..4007da03 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -29,9 +29,9 @@ jobs: uses: actions/cache@v5 with: path: .turbo - key: ${{ runner.os }}-turbo-${{ github.sha }} + key: ${{ runner.os }}-turbo-pr-${{ github.sha }} restore-keys: | - ${{ runner.os }}-turbo- + ${{ runner.os }}-turbo-pr- - name: Install and build run: pnpm install && pnpm build:packages @@ -67,9 +67,9 @@ jobs: uses: actions/cache@v5 with: path: .turbo - key: ${{ runner.os }}-turbo-${{ github.event.pull_request.base.sha }} + key: ${{ runner.os }}-turbo-base-${{ github.event.pull_request.base.sha }} restore-keys: | - ${{ runner.os }}-turbo- + ${{ runner.os }}-turbo-base- - name: Install and build run: |