Skip to content
Open
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
63 changes: 61 additions & 2 deletions src/checks/bundle-size.ts
Original file line number Diff line number Diff line change
@@ -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[],
Expand All @@ -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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in main, we basically check if any packages exceeded the threshold. this checks if the sum of packages exceeds the threshold.

are we sure this bit is needed?

maybe we could just do what we did originally:

  const packWarnings = comparison.packChanges.filter(
    (change) => change.exceedsThreshold && change.sizeChange > 0
  );

  if (packWarnings.length > 0) {

i.e. if any package exceeds the threshold, show the table.

otherwise this will show the table if no packages exceed the threshold, but their total change does.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, my brain went "that'd be nice as a guard rail" but no worries, I'll revert that bit 👌

return;
}

const packWarnings = comparison.packChanges.filter(
(change) => change.exceedsThreshold && change.sizeChange > 0
(change) => change.sizeChange > 0
);

if (packWarnings.length > 0) {
Expand All @@ -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');
Expand Down
3 changes: 2 additions & 1 deletion src/packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
64 changes: 64 additions & 0 deletions test/checks/__snapshots__/bundle-size_test.ts.snap
Original file line number Diff line number Diff line change
@@ -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 |",
]
`;
167 changes: 167 additions & 0 deletions test/checks/bundle-size_test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});