From 55a980cac8db7c202e127e5abcecc0e8c1bf526a Mon Sep 17 00:00:00 2001 From: Arno V Date: Wed, 26 Nov 2025 15:53:55 -0500 Subject: [PATCH 1/2] feat(bundlesize): add threshold support to bundle size reporting Introduces a configurable threshold for reporting file size changes in bundle stats. Changes below the threshold are ignored in the report, with tests and fixtures added to verify correct behavior. The default threshold is set to 0 bytes to be backward compatible. --- .npmrc | 1 - .../configuration/with-threshold-zero.js | 21 ++++++++ .../fixtures/configuration/with-threshold.js | 21 ++++++++ .../fixtures/stats/current-threshold.json | 22 +++++++++ .../fixtures/stats/previous-threshold.json | 22 +++++++++ .../src/__tests__/reportStats.test.ts | 48 +++++++++++++++++++ packages/bundlesize/src/defaults.ts | 2 + packages/bundlesize/src/reportStats.ts | 15 +++++- pnpm-workspace.yaml | 4 +- 9 files changed, 152 insertions(+), 4 deletions(-) delete mode 100644 .npmrc create mode 100644 packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold-zero.js create mode 100644 packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold.js create mode 100644 packages/bundlesize/src/__tests__/fixtures/stats/current-threshold.json create mode 100644 packages/bundlesize/src/__tests__/fixtures/stats/previous-threshold.json diff --git a/.npmrc b/.npmrc deleted file mode 100644 index bf2e7648..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -shamefully-hoist=true diff --git a/packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold-zero.js b/packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold-zero.js new file mode 100644 index 00000000..170e9a98 --- /dev/null +++ b/packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold-zero.js @@ -0,0 +1,21 @@ +export default { + report: { + previous: "../stats/previous-threshold.json", + current: "../stats/current-threshold.json", + threshold: 0, + }, + sizes: [ + { + path: "../data/**/file.txt", + limit: "1.5 kB", + }, + { + path: "../data/**/file-small-change.txt", + limit: "1.5 kB", + }, + { + path: "../data/**/file-no-change.txt", + limit: "1.5 kB", + }, + ], +}; diff --git a/packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold.js b/packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold.js new file mode 100644 index 00000000..7cfd424b --- /dev/null +++ b/packages/bundlesize/src/__tests__/fixtures/configuration/with-threshold.js @@ -0,0 +1,21 @@ +export default { + report: { + previous: "../stats/previous-threshold.json", + current: "../stats/current-threshold.json", + threshold: 5, + }, + sizes: [ + { + path: "../data/**/file.txt", + limit: "1.5 kB", + }, + { + path: "../data/**/file-small-change.txt", + limit: "1.5 kB", + }, + { + path: "../data/**/file-no-change.txt", + limit: "1.5 kB", + }, + ], +}; diff --git a/packages/bundlesize/src/__tests__/fixtures/stats/current-threshold.json b/packages/bundlesize/src/__tests__/fixtures/stats/current-threshold.json new file mode 100644 index 00000000..edceb7e9 --- /dev/null +++ b/packages/bundlesize/src/__tests__/fixtures/stats/current-threshold.json @@ -0,0 +1,22 @@ +{ + "0.0.8": { + "data/file.txt": { + "fileSize": 281, + "fileSizeGzip": 1003, + "limit": "1.5 kB", + "passed": true + }, + "data/file-small-change.txt": { + "fileSize": 281, + "fileSizeGzip": 1004, + "limit": "1.5 kB", + "passed": true + }, + "data/file-no-change.txt": { + "fileSize": 281, + "fileSizeGzip": 1000, + "limit": "1.5 kB", + "passed": true + } + } +} diff --git a/packages/bundlesize/src/__tests__/fixtures/stats/previous-threshold.json b/packages/bundlesize/src/__tests__/fixtures/stats/previous-threshold.json new file mode 100644 index 00000000..0b5aba13 --- /dev/null +++ b/packages/bundlesize/src/__tests__/fixtures/stats/previous-threshold.json @@ -0,0 +1,22 @@ +{ + "0.0.7": { + "data/file.txt": { + "fileSize": 281, + "fileSizeGzip": 1000, + "limit": "1.5 kB", + "passed": true + }, + "data/file-small-change.txt": { + "fileSize": 281, + "fileSizeGzip": 1000, + "limit": "1.5 kB", + "passed": true + }, + "data/file-no-change.txt": { + "fileSize": 281, + "fileSizeGzip": 1000, + "limit": "1.5 kB", + "passed": true + } + } +} diff --git a/packages/bundlesize/src/__tests__/reportStats.test.ts b/packages/bundlesize/src/__tests__/reportStats.test.ts index 73829031..a59cbc0d 100644 --- a/packages/bundlesize/src/__tests__/reportStats.test.ts +++ b/packages/bundlesize/src/__tests__/reportStats.test.ts @@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url"; import kleur from "kleur"; import { describe, expect, it } from "vitest"; +import { DEFAULT_THRESHOLD } from "../defaults.js"; import { reportStats } from "../reportStats.js"; import { STDOUT } from "../utilities.js"; @@ -10,6 +11,53 @@ kleur.enabled = false; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +describe("when testing for reportStats with threshold", () => { + it("should ignore changes below the configured threshold", async () => { + // file.txt: 1000 -> 1003 (diff = 3 bytes, below threshold of 5) + // file-small-change.txt: 1000 -> 1004 (diff = 4 bytes, below threshold of 5) + // file-no-change.txt: 1000 -> 1000 (diff = 0 bytes) + const result = await reportStats({ + flags: { + configuration: path.join( + __dirname, + "fixtures/configuration/with-threshold.js", + ), + }, + }); + + expect(result.exitCode).toBe(0); + // All diffs should be ignored since they are below threshold (5 bytes) + expect(result.data).not.toContain("+3 B"); + expect(result.data).not.toContain("+4 B"); + // Check that sizes are still reported + expect(result.data).toContain("1000 B"); + expect(result.data).toContain("1003 B"); + expect(result.data).toContain("1004 B"); + }); + + it("should show all changes when threshold is set to 0", async () => { + // Same files but threshold is 0, so all changes should be shown + const result = await reportStats({ + flags: { + configuration: path.join( + __dirname, + "fixtures/configuration/with-threshold-zero.js", + ), + }, + }); + + expect(result.exitCode).toBe(0); + // With threshold 0, all non-zero diffs should be shown + expect(result.data).toContain("+3 B"); + expect(result.data).toContain("+4 B"); + }); + + it("should use default threshold when not specified in config", async () => { + // Basic config doesn't specify threshold, should use DEFAULT_THRESHOLD (0) + expect(DEFAULT_THRESHOLD).toBe(0); + }); +}); + describe("when testing for reportStats with errors", () => { it("should report there is a missing configuration file", async () => { const result = await reportStats({ diff --git a/packages/bundlesize/src/defaults.ts b/packages/bundlesize/src/defaults.ts index 12c93e5c..c081c935 100644 --- a/packages/bundlesize/src/defaults.ts +++ b/packages/bundlesize/src/defaults.ts @@ -7,3 +7,5 @@ export const defaultFlags = { type: "size", force: false, }; + +export const DEFAULT_THRESHOLD = 0; diff --git a/packages/bundlesize/src/reportStats.ts b/packages/bundlesize/src/reportStats.ts index 83f0707d..90974ba4 100644 --- a/packages/bundlesize/src/reportStats.ts +++ b/packages/bundlesize/src/reportStats.ts @@ -1,5 +1,6 @@ import { basename, dirname, join } from "node:path"; import bytes from "bytes"; +import { DEFAULT_THRESHOLD } from "./defaults.js"; import type { FooterProperties } from "./utilities.js"; import { addMDrow, @@ -17,6 +18,7 @@ type ReportConfiguration = { header?: string; footer?: (arguments_: FooterProperties) => string; columns?: { [key: string]: string }[]; + threshold?: number; }; interface HeaderSizeEntry { @@ -88,6 +90,7 @@ export const reportStats = async ({ flags }): Promise => { let overallDiff = 0; let totalGzipSize = 0; + const threshold = configuration.report.threshold ?? DEFAULT_THRESHOLD; const headerString = configuration.report.header || "## Bundle Size"; const footerFormatter = configuration.report.footer || formatFooter; const columns = configuration.report.columns || [ @@ -137,7 +140,11 @@ export const reportStats = async ({ flags }): Promise => { if (!hasSubGroup) { return; } - const diff = subGroupAccumulatedSize - subGroupAccumulatedPrevSize; + let diff = subGroupAccumulatedSize - subGroupAccumulatedPrevSize; + // Apply threshold: if the absolute diff is below threshold, treat as no change + if (Math.abs(diff) < threshold) { + diff = 0; + } let diffString = ""; if (diff !== 0 && subGroupAccumulatedPrevSize !== 0) { const sign = diff > 0 ? "+" : "-"; @@ -182,7 +189,11 @@ export const reportStats = async ({ flags }): Promise => { previousStats.data[previousVersion] && previousStats.data[previousVersion][key]; previousFileSizeGzip = previousFileStats?.fileSizeGzip || 0; - const diff = item.fileSizeGzip - previousFileSizeGzip; + let diff = item.fileSizeGzip - previousFileSizeGzip; + // Apply threshold: if the absolute diff is below threshold, treat as no change + if (Math.abs(diff) < threshold) { + diff = 0; + } overallDiff += diff; diffString = diff === 0 || diff === item.fileSizeGzip diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5fb0176c..e1580ab9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,5 +3,7 @@ packages: - examples/* onlyBuiltDependencies: - - '@swc/core' + - "@swc/core" - esbuild + +shamefullyHoist: true From 127c48efbe9f6c240bceddd3b71202f2f52a2507 Mon Sep 17 00:00:00 2001 From: Arno V Date: Wed, 26 Nov 2025 15:58:30 -0500 Subject: [PATCH 2/2] Update README.md --- packages/bundlesize/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/bundlesize/README.md b/packages/bundlesize/README.md index fb9c36f1..5ee40e24 100644 --- a/packages/bundlesize/README.md +++ b/packages/bundlesize/README.md @@ -26,6 +26,7 @@ For the report option, it must export an object named "report" which is an objec - `previous`: the previous path to the report to compare against (required, string) - `current`: the current path to the report to compare against (required, string) - `columns`: the columns to display (optional, array of objects) +- `threshold`: the minimum gzip size change in bytes to consider as a change (optional, number, defaults to 0). Changes below this threshold are treated as no change. ## Examples @@ -184,6 +185,20 @@ export default { }; ``` +#### Simple report with custom threshold + +By default, all gzip size changes are reported. You can set a threshold to ignore small changes: + +```js +export default { + report: { + threshold: 10, // ignore changes smaller than 10 bytes + prev: "stats/previous.json", + current: "stats/current.json" + } +}; +``` + #### Report with subgroup headers (multiple tables) You can interleave header objects inside the `sizes` array to break the report output into multiple markdown tables. Each header object must contain a `header` property (any markdown heading is allowed). A sub-bundle total line will be printed after each group along with its diff to previous stats (if available), followed by the overall bundle size and status at the end.