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/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. 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