From 4b5a852061e73f966c999bcb293ceed9a4738c70 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 2 Jun 2026 20:57:36 -0500 Subject: [PATCH 1/6] feat: add npm size measurement script --- tasks/measure-size.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tasks/measure-size.ts diff --git a/tasks/measure-size.ts b/tasks/measure-size.ts new file mode 100644 index 0000000..a1a6462 --- /dev/null +++ b/tasks/measure-size.ts @@ -0,0 +1,18 @@ +import { gzipSync } from "node:zlib"; + +const dir = "build/npm/esm"; +const results: Array<{ file: string; raw: number; gzip: number }> = []; + +for await (const entry of Deno.readDir(dir)) { + if (!entry.isFile) continue; + const path = `${dir}/${entry.name}`; + const data = await Deno.readFile(path); + results.push({ + file: entry.name, + raw: data.byteLength, + gzip: gzipSync(data).byteLength, + }); +} + +results.sort((a, b) => a.file.localeCompare(b.file)); +console.log(JSON.stringify(results)); From 30fa63fdac031225d58d31a85f12880083264b32 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 2 Jun 2026 20:58:26 -0500 Subject: [PATCH 2/6] feat: add size-report CI workflow --- .github/workflows/size-report.yml | 118 ++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .github/workflows/size-report.yml diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml new file mode 100644 index 0000000..202ff98 --- /dev/null +++ b/.github/workflows/size-report.yml @@ -0,0 +1,118 @@ +name: Size Report + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + size-report: + name: Size Report + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + with: + submodules: true + fetch-depth: 0 + persist-credentials: false + + - name: setup deno + uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 + with: + deno-version: v2.x + + - name: setup node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4 + with: + node-version: 24 + + - name: build (head) + run: make && deno task build:npm 0.0.0 + + - name: measure sizes (head) + run: | + cp tasks/measure-size.ts /tmp/measure-size.ts + deno run --allow-read /tmp/measure-size.ts > /tmp/head-sizes.json + + - name: find merge base + id: base + run: | + SHA=$(git merge-base HEAD origin/${{ github.base_ref }}) + echo "sha=$SHA" >> $GITHUB_OUTPUT + + - name: checkout base commit + run: | + git checkout ${{ steps.base.outputs.sha }} + git submodule update --init --recursive + + - name: build (base) + run: make && deno task build:npm 0.0.0 + + - name: measure sizes (base) + run: deno run --allow-read /tmp/measure-size.ts > /tmp/base-sizes.json + + - name: post size report + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const fs = require('fs'); + const head = JSON.parse(fs.readFileSync('/tmp/head-sizes.json', 'utf8')); + const base = JSON.parse(fs.readFileSync('/tmp/base-sizes.json', 'utf8')); + + const total = (arr) => arr.reduce( + (acc, f) => ({ raw: acc.raw + f.raw, gzip: acc.gzip + f.gzip }), + { raw: 0, gzip: 0 } + ); + const headTotal = total(head); + const baseTotal = total(base); + + const deltaRaw = headTotal.raw - baseTotal.raw; + const deltaGzip = headTotal.gzip - baseTotal.gzip; + + const fmt = (n) => `${(n / 1024).toFixed(1)} KB`; + const fmtDelta = (n) => + n === 0 ? '±0 KB' : `${n > 0 ? '+' : ''}${(n / 1024).toFixed(1)} KB`; + + const takeaway = + deltaGzip < 0 ? 'Size Reduced' : + deltaGzip > 0 ? 'Size Increased' : + 'No Change to Size'; + + const body = [ + '', + `**${takeaway}** — ${fmtDelta(deltaGzip)} gz, ${fmtDelta(deltaRaw)} raw`, + '', + `${fmt(headTotal.gzip)} gz (${fmt(headTotal.raw)} raw)`, + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body.startsWith('')); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } From 8e205c38df773d92d115e4effc79b9d24e3fe7d0 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 2 Jun 2026 21:43:40 -0500 Subject: [PATCH 3/6] fix lint --- tasks/measure-size.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/measure-size.ts b/tasks/measure-size.ts index a1a6462..714b160 100644 --- a/tasks/measure-size.ts +++ b/tasks/measure-size.ts @@ -5,8 +5,8 @@ const results: Array<{ file: string; raw: number; gzip: number }> = []; for await (const entry of Deno.readDir(dir)) { if (!entry.isFile) continue; - const path = `${dir}/${entry.name}`; - const data = await Deno.readFile(path); + let path = `${dir}/${entry.name}`; + let data = await Deno.readFile(path); results.push({ file: entry.name, raw: data.byteLength, From 64d95e6af7e68036b992eab0df352f56241884ce Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 2 Jun 2026 21:48:34 -0500 Subject: [PATCH 4/6] fix: only post size report comment when size changes --- .github/workflows/size-report.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index 202ff98..4a810b7 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -81,10 +81,9 @@ jobs: const fmtDelta = (n) => n === 0 ? '±0 KB' : `${n > 0 ? '+' : ''}${(n / 1024).toFixed(1)} KB`; - const takeaway = - deltaGzip < 0 ? 'Size Reduced' : - deltaGzip > 0 ? 'Size Increased' : - 'No Change to Size'; + if (deltaGzip === 0 && deltaRaw === 0) return; + + const takeaway = deltaGzip < 0 ? 'Size Reduced' : 'Size Increased'; const body = [ '', From 809d70243935c1fdf9a2d4603a7a5a8f38a294e5 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 2 Jun 2026 21:55:14 -0500 Subject: [PATCH 5/6] fix: skip comment update when head sizes unchanged from previous push --- .github/workflows/size-report.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index 4a810b7..84ac49c 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -83,23 +83,33 @@ jobs: if (deltaGzip === 0 && deltaRaw === 0) return; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => c.body.startsWith('')); + + // Skip if the head sizes haven't changed since the last push + if (existing) { + const match = existing.body.match(//); + if (match) { + const prev = JSON.parse(match[1]); + if (prev.raw === headTotal.raw && prev.gzip === headTotal.gzip) return; + } + } + const takeaway = deltaGzip < 0 ? 'Size Reduced' : 'Size Increased'; const body = [ '', + ``, `**${takeaway}** — ${fmtDelta(deltaGzip)} gz, ${fmtDelta(deltaRaw)} raw`, '', `${fmt(headTotal.gzip)} gz (${fmt(headTotal.raw)} raw)`, ].join('\n'); - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existing = comments.find(c => c.body.startsWith('')); - if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, From e6c156da23ddd37497340a798507a1ecdae8bc8a Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 2 Jun 2026 22:00:50 -0500 Subject: [PATCH 6/6] fix: drop gzip, report unpacked install size only --- .github/workflows/size-report.yml | 27 ++++++++++----------------- tasks/measure-size.ts | 12 +++--------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml index 84ac49c..1d8ce71 100644 --- a/.github/workflows/size-report.yml +++ b/.github/workflows/size-report.yml @@ -67,21 +67,17 @@ jobs: const head = JSON.parse(fs.readFileSync('/tmp/head-sizes.json', 'utf8')); const base = JSON.parse(fs.readFileSync('/tmp/base-sizes.json', 'utf8')); - const total = (arr) => arr.reduce( - (acc, f) => ({ raw: acc.raw + f.raw, gzip: acc.gzip + f.gzip }), - { raw: 0, gzip: 0 } - ); + const total = (arr) => arr.reduce((acc, f) => acc + f.size, 0); const headTotal = total(head); const baseTotal = total(base); - const deltaRaw = headTotal.raw - baseTotal.raw; - const deltaGzip = headTotal.gzip - baseTotal.gzip; + const delta = headTotal - baseTotal; const fmt = (n) => `${(n / 1024).toFixed(1)} KB`; const fmtDelta = (n) => - n === 0 ? '±0 KB' : `${n > 0 ? '+' : ''}${(n / 1024).toFixed(1)} KB`; + `${n > 0 ? '+' : ''}${(n / 1024).toFixed(1)} KB`; - if (deltaGzip === 0 && deltaRaw === 0) return; + if (delta === 0) return; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, @@ -93,21 +89,18 @@ jobs: // Skip if the head sizes haven't changed since the last push if (existing) { - const match = existing.body.match(//); - if (match) { - const prev = JSON.parse(match[1]); - if (prev.raw === headTotal.raw && prev.gzip === headTotal.gzip) return; - } + const match = existing.body.match(//); + if (match && Number(match[1]) === headTotal) return; } - const takeaway = deltaGzip < 0 ? 'Size Reduced' : 'Size Increased'; + const takeaway = delta < 0 ? 'Size Reduced' : 'Size Increased'; const body = [ '', - ``, - `**${takeaway}** — ${fmtDelta(deltaGzip)} gz, ${fmtDelta(deltaRaw)} raw`, + ``, + `**${takeaway}** — ${fmtDelta(delta)}`, '', - `${fmt(headTotal.gzip)} gz (${fmt(headTotal.raw)} raw)`, + `${fmt(headTotal)} unpacked`, ].join('\n'); if (existing) { diff --git a/tasks/measure-size.ts b/tasks/measure-size.ts index 714b160..bb55a2b 100644 --- a/tasks/measure-size.ts +++ b/tasks/measure-size.ts @@ -1,17 +1,11 @@ -import { gzipSync } from "node:zlib"; - const dir = "build/npm/esm"; -const results: Array<{ file: string; raw: number; gzip: number }> = []; +const results: Array<{ file: string; size: number }> = []; for await (const entry of Deno.readDir(dir)) { if (!entry.isFile) continue; let path = `${dir}/${entry.name}`; - let data = await Deno.readFile(path); - results.push({ - file: entry.name, - raw: data.byteLength, - gzip: gzipSync(data).byteLength, - }); + let { size } = await Deno.stat(path); + results.push({ file: entry.name, size }); } results.sort((a, b) => a.file.localeCompare(b.file));