diff --git a/README.md b/README.md index 4cb2295..9deecf7 100644 --- a/README.md +++ b/README.md @@ -37,21 +37,21 @@ jobs: ## Inputs -| Name | Description | Required | Default | -| ---------------------- | ------------------------------------------------------------------------------ | -------- | ----------------------------------------- | -| `base-ref` | Base ref to compare against (defaults to main or PR target) | No | Auto-detected from PR or `main` | -| `github-token` | The GitHub token for authentication | Yes | `${{ github.token }}` | -| `pr-number` | The number of the pull request to comment on | Yes | `${{ github.event.pull_request.number }}` | -| `dependency-threshold` | Threshold for warning about significant increase in number of dependencies | No | `10` | -| `size-threshold` | Threshold (in bytes) for warning about significant increase in package size | No | `100000` | -| `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` | -| `detect-replacements` | Detect modules which have community suggested alternatives | No | `true` | -| `working-directory` | Working directory to scan for package lock file | No | None | -| `mode` | Run mode: `comment`, `artifact`, or `comment-from-artifact` | No | `comment` | -| `artifact-path` | Path to the artifact JSON file (for `comment-from-artifact` mode) | No | None | +| Name | Description | Required | Default | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------- | +| `base-ref` | Base ref to compare against (defaults to main or PR target) | No | Auto-detected from PR or `main` | +| `github-token` | The GitHub token for authentication | Yes | `${{ github.token }}` | +| `pr-number` | The number of the pull request to comment on | Yes | `${{ github.event.pull_request.number }}` | +| `dependency-threshold` | Threshold for warning about significant increase in number of dependencies | No | `10` | +| `size-threshold` | Threshold (in bytes) for warning about significant increase in package size | No | `100000` | +| `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. 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 | +| `mode` | Run mode: `comment`, `artifact`, or `comment-from-artifact` | No | `comment` | +| `artifact-path` | Path to the artifact JSON file (for `comment-from-artifact` mode) | No | None | ## Example with custom inputs @@ -93,11 +93,23 @@ 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 > [!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. +### Always Report Bundle Size Changes + +To always report bundle size changes, set `pack-size-threshold` to `-1`. This will display bundle size differences even if they are reductions, giving you full visibility into how your changes affect the published package size. + +```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 190a36e..1e5fc1f 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/build/main.js b/build/main.js index 8dd934b..1e3815f 100644 --- a/build/main.js +++ b/build/main.js @@ -24393,7 +24393,7 @@ function comparePackSizes(basePacks, sourcePacks, threshold) { const baseSize = basePack?.size ?? null; 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, baseSize, @@ -24815,26 +24815,39 @@ ${provenanceRows.join("\n")}` } // src/checks/bundle-size.ts +function formatBytesSigned(bytes) { + return `${bytes > 0 ? "+" : ""}${formatBytes(bytes)}`; +} async function scanForBundleSize(messages, basePacks, sourcePacks, threshold) { if (basePacks.length === 0 && sourcePacks.length === 0) { return; } const comparison = comparePackSizes(basePacks, sourcePacks, threshold); const packWarnings = comparison.packChanges.filter( - (change) => change.exceedsThreshold && change.sizeChange > 0 + (change) => change.exceedsThreshold ); + if (threshold === -1 && packWarnings.length === 0) { + messages.push(`## \u{1F4E6} Package Bundle Size + +No bundle size changes.`); + return; + } if (packWarnings.length > 0) { + const hasDecreases = packWarnings.some((c) => c.sizeChange < 0); + const hasIncreases = packWarnings.some((c) => c.sizeChange > 0); + const heading = hasDecreases && hasIncreases ? "## \u{1F4E6} Package Bundle Size Changes" : hasDecreases ? "## \u{1F389} Package Size Decrease" : "## \u26A0\uFE0F Package Size Increase"; const packRows = packWarnings.map((change) => { const baseSize = change.baseSize ? formatBytes(change.baseSize) : "New"; 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"); + const thresholdText = threshold === -1 ? "" : ` +These packages exceed the size change threshold of ${formatBytes(threshold)}: +`; messages.push( - `## \u26A0\uFE0F Package Size Increase - -These packages exceed the size increase threshold of ${formatBytes(threshold)}: - + `${heading} +${thresholdText} | \u{1F4E6} Package | \u{1F4CF} Base Size | \u{1F4CF} Source Size | \u{1F4C8} Size Change | | --- | --- | --- | --- | ${packRows}` @@ -24984,7 +24997,9 @@ async function analyzeAndComment() { `Parsed current lockfile with ${parsedCurrentLock.packages.length} packages` ); } catch (err) { - throw new Error(`Failed to parse current lockfile: ${err}`); + throw new Error(`Failed to parse current lockfile: ${err}`, { + cause: err + }); } try { parsedBaseLock = await parse2( @@ -24996,7 +25011,9 @@ async function analyzeAndComment() { `Parsed base lockfile with ${parsedBaseLock.packages.length} packages` ); } catch (err) { - throw new Error(`Failed to parse base lockfile: ${err}`); + throw new Error(`Failed to parse base lockfile: ${err}`, { + cause: err + }); } const currentDeps = computeDependencyVersions(parsedCurrentLock); const baseDeps = computeDependencyVersions(parsedBaseLock); diff --git a/src/checks/bundle-size.ts b/src/checks/bundle-size.ts index 8400e4e..cd1ef6d 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,27 +15,44 @@ export async function scanForBundleSize( return; } const comparison = comparePackSizes(basePacks, sourcePacks, threshold); + const packWarnings = comparison.packChanges.filter( - (change) => change.exceedsThreshold && change.sizeChange > 0 + (change) => change.exceedsThreshold ); + if (threshold === -1 && packWarnings.length === 0) { + messages.push(`## 📦 Package Bundle Size\n\nNo bundle size changes.`); + return; + } + if (packWarnings.length > 0) { + const hasDecreases = packWarnings.some((c) => c.sizeChange < 0); + const hasIncreases = packWarnings.some((c) => c.sizeChange > 0); + const heading = + hasDecreases && hasIncreases + ? '## 📦 Package Bundle Size Changes' + : hasDecreases + ? '## 🎉 Package Size Decrease' + : '## ⚠️ Package Size Increase'; const packRows = packWarnings .map((change) => { const baseSize = change.baseSize ? formatBytes(change.baseSize) : 'New'; 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'); - messages.push( - `## ⚠️ Package Size Increase - -These packages exceed the size increase threshold of ${formatBytes(threshold)}: + const thresholdText = + threshold === -1 + ? '' + : `\nThese packages exceed the size change threshold of ${formatBytes(threshold)}:\n`; + messages.push( + `${heading} +${thresholdText} | 📦 Package | 📏 Base Size | 📏 Source Size | 📈 Size Change | | --- | --- | --- | --- | ${packRows}` 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..af71a71 --- /dev/null +++ b/test/checks/__snapshots__/bundle-size_test.ts.snap @@ -0,0 +1,84 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +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 report 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 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 new packages when threshold is -1 1`] = ` +[ + "## ⚠️ Package Size Increase + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📈 Size Change | +| --- | --- | --- | --- | +| new-package | New | 50 kB | +50 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 show removed packages when threshold is -1 1`] = ` +[ + "## 🎉 Package Size Decrease + +| 📦 Package | 📏 Base Size | 📏 Source Size | 📈 Size Change | +| --- | --- | --- | --- | +| old-package | 50 kB | Removed | -50 kB |", +] +`; + +exports[`scanForBundleSize > should warn about an increase regardless of other packages decreasing 1`] = ` +[ + "## ⚠️ Package Size Increase + +These packages exceed the size change 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 change 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..a9a691e --- /dev/null +++ b/test/checks/bundle-size_test.ts @@ -0,0 +1,131 @@ +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).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 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 regardless of other packages decreasing', 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, 50000); + + expect(messages).toMatchSnapshot(); + }); + + it('should report 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).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(); + }); + + it('should show new packages when threshold is -1', async () => { + const messages: string[] = []; + const basePacks: PackInfo[] = []; + const sourcePacks = [makePack('new-package', 50000)]; + + await scanForBundleSize(messages, basePacks, sourcePacks, -1); + + expect(messages).toMatchSnapshot(); + }); + + it('should show removed packages when threshold is -1', async () => { + const messages: string[] = []; + const basePacks = [makePack('old-package', 50000)]; + const sourcePacks: PackInfo[] = []; + + await scanForBundleSize(messages, basePacks, sourcePacks, -1); + + expect(messages).toMatchSnapshot(); + }); +});