diff --git a/README.md b/README.md index 072abd3..6cdbe9c 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ jobs: | `duplicate-threshold` | Threshold for warning about packages with multiple versions | No | `1` | | `base-packages` | Glob pattern for base branch pack files (e.g., `"./base-packs/*.tgz"`) | No | None | | `source-packages` | Glob pattern for source branch pack files (e.g., `"./source-packs/*.tgz"`) | No | None | -| `pack-size-threshold` | Threshold (in bytes) for warning about significant increase in total pack size | No | `50000` | +| `pack-size-threshold` | Threshold (in bytes) for warning about significant increase in total pack size. Set to `-1` to always report size changes. | No | `50000` | | `detect-replacements` | Detect modules which have community suggested alternatives | No | `true` | | `working-directory` | Working directory to scan for package lock file | No | None | @@ -90,11 +90,26 @@ The action accepts glob patterns to locate package tarballs for comparison: - **`base-packages`** - Glob pattern for base branch pack files (e.g., `"./base-packs/*.tgz"`) - **`source-packages`** - Glob pattern for source branch pack files (e.g., `"./source-packs/*.tgz"`) -- **`pack-size-threshold`** - Threshold in bytes for warning about significant pack size increases +- **`pack-size-threshold`** - Threshold in bytes for warning about significant pack size increases. Set to `-1` to always report bundle size changes. > [!NOTE] > Package bundle analysis only runs when both `base-packages` and `source-packages` are provided. If these inputs are not set, this feature is skipped entirely. +When the bundle size does not change between base and source, no message is posted. Set `pack-size-threshold` to `-1` to always report, including a confirmation when there is no change. + +### Always Report Bundle Size Changes + +To always report bundle size changes, set `pack-size-threshold` to `-1`. All changed packages are shown in a single table. When only decreases occur, the report is marked with 🎉: + +```yaml +- name: Create Diff + uses: e18e/action-dependency-diff@v1 + with: + base-packages: './base-packs/*.tgz' + source-packages: './source-packs/*.tgz' + pack-size-threshold: -1 +``` + You can see an example of how to set this up in the [bundle difference workflow](./recipes/bundle-diff.yml). ## Module Replacements diff --git a/action.yml b/action.yml index 9c4c9b4..658e4a9 100644 --- a/action.yml +++ b/action.yml @@ -33,7 +33,7 @@ inputs: description: 'Glob pattern for source branch pack files (e.g., "./source-packs/*.tgz")' required: false pack-size-threshold: - description: 'Threshold (in bytes) for warning about significant increase in total pack size' + description: 'Threshold (in bytes) for warning about significant increase in total pack size. Set to -1 to always report size changes.' required: false default: '50000' duplicate-threshold: diff --git a/src/checks/bundle-size.ts b/src/checks/bundle-size.ts index 8400e4e..476c3c5 100644 --- a/src/checks/bundle-size.ts +++ b/src/checks/bundle-size.ts @@ -1,6 +1,10 @@ import {formatBytes} from '../common.js'; import {comparePackSizes, type PackInfo} from '../packs.js'; +function formatBytesSigned(bytes: number): string { + return `${bytes > 0 ? '+' : ''}${formatBytes(bytes)}`; +} + export async function scanForBundleSize( messages: string[], basePacks: PackInfo[], @@ -11,8 +15,63 @@ export async function scanForBundleSize( return; } const comparison = comparePackSizes(basePacks, sourcePacks, threshold); + + if (threshold === -1) { + const changedPacks = comparison.packChanges.filter( + (change) => change.exceedsThreshold + ); + + if (changedPacks.length === 0) { + messages.push(`## 📦 Package Bundle Size\n\nNo bundle size changes.`); + return; + } + + if (changedPacks.length > 0) { + const hasDecreases = changedPacks.some((c) => c.sizeChange < 0); + const hasIncreases = changedPacks.some((c) => c.sizeChange > 0); + + const heading = + hasDecreases && hasIncreases + ? '## 📦 Package Bundle Size Changes' + : hasDecreases + ? '## 🎉 Package Size Decrease' + : '## ⚠️ Package Size Increase'; + + const packRows = changedPacks + .map((change) => { + const baseSize = change.baseSize + ? formatBytes(change.baseSize) + : 'New'; + const sourceSize = change.sourceSize + ? formatBytes(change.sourceSize) + : 'Removed'; + const sizeChange = formatBytesSigned(change.sizeChange); + return `| ${change.name} | ${baseSize} | ${sourceSize} | ${sizeChange} |`; + }) + .join('\n'); + + messages.push( + `${heading} + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📊 Size Change | +| --- | --- | --- | --- | +${packRows}` + ); + } + + return; + } + + const totalSizeChange = comparison.packChanges + .filter((change) => change.sizeChange > 0) + .reduce((sum, change) => sum + change.sizeChange, 0); + + if (totalSizeChange < threshold) { + return; + } + const packWarnings = comparison.packChanges.filter( - (change) => change.exceedsThreshold && change.sizeChange > 0 + (change) => change.sizeChange > 0 ); if (packWarnings.length > 0) { @@ -22,7 +81,7 @@ export async function scanForBundleSize( const sourceSize = change.sourceSize ? formatBytes(change.sourceSize) : 'Removed'; - const sizeChange = formatBytes(change.sizeChange); + const sizeChange = formatBytesSigned(change.sizeChange); return `| ${change.name} | ${baseSize} | ${sourceSize} | ${sizeChange} |`; }) .join('\n'); diff --git a/src/packs.ts b/src/packs.ts index 06d05cb..5955e37 100644 --- a/src/packs.ts +++ b/src/packs.ts @@ -175,7 +175,8 @@ export function comparePackSizes( const sourceSize = sourcePack?.size ?? null; const sizeChange = (sourceSize ?? 0) - (baseSize ?? 0); - const exceedsThreshold = sizeChange >= threshold; + const exceedsThreshold = + threshold === -1 ? sizeChange !== 0 : sizeChange >= threshold; packChanges.push({ name: packName, diff --git a/test/checks/__snapshots__/bundle-size_test.ts.snap b/test/checks/__snapshots__/bundle-size_test.ts.snap new file mode 100644 index 0000000..f44ec1f --- /dev/null +++ b/test/checks/__snapshots__/bundle-size_test.ts.snap @@ -0,0 +1,64 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`scanForBundleSize > should celebrate size decrease when threshold is -1 1`] = ` +[ + "## 🎉 Package Size Decrease + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📊 Size Change | +| --- | --- | --- | --- | +| my-package | 200 kB | 100 kB | -100 kB |", +] +`; + +exports[`scanForBundleSize > should report no bundle size change with threshold=-1 when diff is 0 1`] = ` +[ + "## 📦 Package Bundle Size + +No bundle size changes.", +] +`; + +exports[`scanForBundleSize > should show both decreases and increases when threshold is -1 1`] = ` +[ + "## 📦 Package Bundle Size Changes + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📊 Size Change | +| --- | --- | --- | --- | +| pkg-b | 50 kB | 150 kB | +100 kB | +| pkg-a | 200 kB | 100 kB | -100 kB |", +] +`; + +exports[`scanForBundleSize > should show only increases when threshold is -1 and no decreases 1`] = ` +[ + "## ⚠️ Package Size Increase + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📊 Size Change | +| --- | --- | --- | --- | +| my-package | 100 kB | 200 kB | +100 kB |", +] +`; + +exports[`scanForBundleSize > should warn about an increase even when a decrease in another package cancels it out in total 1`] = ` +[ + "## ⚠️ Package Size Increase + +These packages exceed the size increase threshold of 50 kB: + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📈 Size Change | +| --- | --- | --- | --- | +| pkg-b | 50 kB | 150 kB | +100 kB |", +] +`; + +exports[`scanForBundleSize > should warn about size increase exceeding threshold 1`] = ` +[ + "## ⚠️ Package Size Increase + +These packages exceed the size increase threshold of 50 kB: + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📈 Size Change | +| --- | --- | --- | --- | +| my-package | 100 kB | 200 kB | +100 kB |", +] +`; diff --git a/test/checks/bundle-size_test.ts b/test/checks/bundle-size_test.ts new file mode 100644 index 0000000..d94f5e6 --- /dev/null +++ b/test/checks/bundle-size_test.ts @@ -0,0 +1,167 @@ +import {describe, expect, it} from 'vitest'; +import {scanForBundleSize} from '../../src/checks/bundle-size.js'; +import type {PackInfo} from '../../src/packs.js'; + +function makePack(packageName: string, size: number): PackInfo { + return { + name: `${packageName}-1.0.0.tgz`, + packageName, + path: `/tmp/${packageName}-1.0.0.tgz`, + size + }; +} + +describe('scanForBundleSize', () => { + it('should do nothing when no packs are provided', async () => { + const messages: string[] = []; + await scanForBundleSize(messages, [], [], 50000); + expect(messages).toHaveLength(0); + }); + + it('should report no bundle size change when diff is 0', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 100000)]; + const sourcePacks = [makePack('my-package', 100000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(0); + }); + + it('should report no bundle size change with threshold=-1 when diff is 0', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 100000)]; + const sourcePacks = [makePack('my-package', 100000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, -1); + + expect(messages).toHaveLength(1); + expect(messages[0]).toContain('No bundle size changes'); + expect(messages).toMatchSnapshot(); + }); + + it('should not report anything when diff is 0 and threshold is not -1', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 100000)]; + const sourcePacks = [makePack('my-package', 100000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(0); + }); + + it('should never warn about a pure size decrease, no matter how large', async () => { + const messages: string[] = []; + // sizeChange is signed: (sourceSize - baseSize) = negative for a shrink + // A massive decrease should never cross a positive threshold + const basePacks = [makePack('my-package', 1000000)]; + const sourcePacks = [makePack('my-package', 1)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(0); + }); + + it('should warn even when net total change is negative due to a large decrease masking an increase', async () => { + const messages: string[] = []; + // pkg-a shrinks by 500 KB, pkg-b grows by 100 KB → net = -400 KB + // Without filtering to increases only, -400 KB < 50 KB threshold → silent (wrong) + const basePacks = [makePack('pkg-a', 500000), makePack('pkg-b', 50000)]; + const sourcePacks = [makePack('pkg-a', 0), makePack('pkg-b', 150000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(1); + expect(messages[0]).toContain('pkg-b'); + expect(messages[0]).not.toContain('pkg-a'); + }); + + it('should sum multiple increases when checking against the threshold', async () => { + const messages: string[] = []; + // Each increase is 30 KB (below the 50 KB threshold individually) + // but combined they are 60 KB (above threshold) → should warn + const basePacks = [makePack('pkg-a', 100000), makePack('pkg-b', 100000)]; + const sourcePacks = [makePack('pkg-a', 130000), makePack('pkg-b', 130000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(1); + expect(messages[0]).toContain('pkg-a'); + expect(messages[0]).toContain('pkg-b'); + }); + + it('should warn about size increase exceeding threshold', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 100000)]; + const sourcePacks = [makePack('my-package', 200000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toMatchSnapshot(); + }); + + it('should not warn about size increase below threshold', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 100000)]; + const sourcePacks = [makePack('my-package', 120000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(0); + }); + + it('should warn about an increase even when a decrease in another package cancels it out in total', async () => { + const messages: string[] = []; + // pkg-a shrinks by 100 KB, pkg-b grows by 100 KB → net = 0, but pkg-b exceeds threshold + const basePacks = [makePack('pkg-a', 200000), makePack('pkg-b', 50000)]; + const sourcePacks = [makePack('pkg-a', 100000), makePack('pkg-b', 150000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(1); + expect(messages[0]).toContain('pkg-b'); + expect(messages[0]).not.toContain('pkg-a'); + expect(messages).toMatchSnapshot(); + }); + + it('should not report no-change when changes exist but are below threshold', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 100000)]; + const sourcePacks = [makePack('my-package', 120000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, 50000); + + expect(messages).toHaveLength(0); + }); + + it('should celebrate size decrease when threshold is -1', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 200000)]; + const sourcePacks = [makePack('my-package', 100000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, -1); + + expect(messages).toMatchSnapshot(); + }); + + it('should show both decreases and increases when threshold is -1', async () => { + const messages: string[] = []; + const basePacks = [makePack('pkg-a', 200000), makePack('pkg-b', 50000)]; + const sourcePacks = [makePack('pkg-a', 100000), makePack('pkg-b', 150000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, -1); + + expect(messages).toHaveLength(1); + expect(messages).toMatchSnapshot(); + }); + + it('should show only increases when threshold is -1 and no decreases', async () => { + const messages: string[] = []; + const basePacks = [makePack('my-package', 100000)]; + const sourcePacks = [makePack('my-package', 200000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, -1); + + expect(messages).toMatchSnapshot(); + }); +});