From a2a180941bb7b6915475f81ed6e7388ff1aedee1 Mon Sep 17 00:00:00 2001 From: Julien Sulpis Date: Wed, 12 Aug 2026 12:26:53 +0200 Subject: [PATCH 1/2] feat: track bundle size deltas --- .github/scripts/create-glcanvas-entry.mts | 20 ++++++ .github/scripts/measure-size.mts | 24 +++++++ .github/scripts/update-size-comment.mts | 73 +++++++++++++++++++++ .github/workflows/size.yml | 80 ++++++++++++++++++++--- benchmark/package.json | 2 +- pnpm-lock.yaml | 4 +- 6 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 .github/scripts/create-glcanvas-entry.mts create mode 100644 .github/scripts/measure-size.mts create mode 100644 .github/scripts/update-size-comment.mts diff --git a/.github/scripts/create-glcanvas-entry.mts b/.github/scripts/create-glcanvas-entry.mts new file mode 100644 index 0000000..7eb27d6 --- /dev/null +++ b/.github/scripts/create-glcanvas-entry.mts @@ -0,0 +1,20 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; + +const output = process.env.CONSUMER_ENTRY; + +if (!output) { + throw new Error("CONSUMER_ENTRY is required"); +} + +await mkdir(dirname(output), { recursive: true }); +await writeFile( + output, + `import { glCanvas } from "@radiancejs/gl"; + +glCanvas({ + canvas: "canvas", + fragment: await fetch("/shaders/fullscreen.frag").then((res) => res.text()), +}); +`, +); diff --git a/.github/scripts/measure-size.mts b/.github/scripts/measure-size.mts new file mode 100644 index 0000000..5091d4d --- /dev/null +++ b/.github/scripts/measure-size.mts @@ -0,0 +1,24 @@ +import { gzipSync } from "node:zlib"; +import { readFile, mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +const reportDir = process.env.REPORT_DIR; +const libraryDist = process.env.LIBRARY_DIST; +const consumerDist = process.env.CONSUMER_DIST; + +if (!reportDir || !libraryDist || !consumerDist) { + throw new Error("REPORT_DIR, LIBRARY_DIST, and CONSUMER_DIST are required"); +} + +const [library, consumer] = await Promise.all([ + readFile(join(libraryDist, "index.js")), + readFile(join(consumerDist, "glcanvas-entry.js")), +]); +const report = { + libraryGzipBytes: gzipSync(library).byteLength, + glCanvasGzipBytes: gzipSync(consumer).byteLength, +}; + +await mkdir(reportDir, { recursive: true }); +await writeFile(join(reportDir, "report.json"), `${JSON.stringify(report)}\n`); +console.log(report); diff --git a/.github/scripts/update-size-comment.mts b/.github/scripts/update-size-comment.mts new file mode 100644 index 0000000..924bb64 --- /dev/null +++ b/.github/scripts/update-size-comment.mts @@ -0,0 +1,73 @@ +import { readFile } from "node:fs/promises"; + +const token = process.env.GITHUB_TOKEN; +const repository = process.env.GITHUB_REPOSITORY; +const issueNumber = process.env.PR_NUMBER; +const prReportPath = process.env.PR_REPORT; +const baseReportPath = process.env.BASE_REPORT; + +if (!token || !repository || !issueNumber || !prReportPath || !baseReportPath) { + throw new Error("GITHUB_TOKEN, GITHUB_REPOSITORY, PR_NUMBER, PR_REPORT, and BASE_REPORT are required"); +} + +const [owner, repo] = repository.split("/"); +const pr = JSON.parse(await readFile(prReportPath, "utf8")); +let base = null; + +try { + base = JSON.parse(await readFile(baseReportPath, "utf8")); +} catch { + // The base report is optional when its main-branch cache is unavailable. +} + +const format = (bytes: number) => `${(bytes / 1024).toFixed(2)} kB`; +const delta = (current: number, previous?: number) => { + if (previous === undefined || previous === null) return ""; + const difference = current - previous; + const sign = difference > 0 ? "+" : ""; + const percentage = previous === 0 ? "" : ` (${sign}${((difference / previous) * 100).toFixed(1)}%)`; + return `${sign}${format(difference)}${percentage}`; +}; +const row = (name: string, key: keyof typeof pr) => + `| ${name} | ${base ? format(base[key]) : ""} | ${format(pr[key])} | ${delta(pr[key], base?.[key])} |`; +const marker = ""; +const body = [ + marker, + "## Bundle size", + "", + "| Bundle | Base | PR | Delta |", + "| --- | ---: | ---: | ---: |", + row("`lib/dist/index.js`", "libraryGzipBytes"), + row("`glCanvas` consumer", "glCanvasGzipBytes"), +].join("\n"); + +const api = `https://api.github.com/repos/${owner}/${repo}`; +const headers = { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28", +}; +const commentsResponse = await fetch(`${api}/issues/${issueNumber}/comments?per_page=100`, { headers }); + +if (!commentsResponse.ok) { + throw new Error(`Unable to list comments: ${commentsResponse.status} ${await commentsResponse.text()}`); +} + +const comments = await commentsResponse.json(); +const existing = comments.find((comment: { body?: string }) => comment.body?.includes(marker)); +const request = existing + ? fetch(`${api}/issues/comments/${existing.id}`, { + method: "PATCH", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify({ body }), + }) + : fetch(`${api}/issues/${issueNumber}/comments`, { + method: "POST", + headers: { ...headers, "Content-Type": "application/json" }, + body: JSON.stringify({ body }), + }); + +const response = await request; +if (!response.ok) { + throw new Error(`Unable to update comment: ${response.status} ${await response.text()}`); +} diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index 054f32f..1ac162f 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -1,6 +1,9 @@ name: Size on: + push: + branches: + - main pull_request: branches: - main @@ -16,17 +19,78 @@ env: jobs: size: runs-on: ubuntu-latest - env: - CI_JOB_NUMBER: 1 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: version: ${{ env.PNPM_VERSION }} - - run: pnpm --filter="./lib" install - - uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - directory: lib/ - build_script: build:lib - package_manager: pnpm + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Restore base size report + if: github.event_name == 'pull_request' + uses: actions/cache/restore@v4 + with: + path: ${{ runner.temp }}/base-size-report + key: bundle-size-${{ github.event.pull_request.base.sha }} + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build library + run: pnpm --filter="./lib" build:lib + + - name: Create glCanvas entry + env: + CONSUMER_ENTRY: ${{ github.workspace }}/benchmark/.size/glcanvas-entry.ts + run: node --experimental-strip-types .github/scripts/create-glcanvas-entry.mts + + - name: Transpile and minify bundles + env: + CONSUMER_ENTRY: ${{ github.workspace }}/benchmark/.size/glcanvas-entry.ts + LIBRARY_DIST: ${{ runner.temp }}/library-dist + CONSUMER_DIST: ${{ runner.temp }}/consumer-dist + run: | + pnpm --filter="./lib" exec tsdown dist/index.js \ + --no-config \ + --format esm \ + --platform browser \ + --target es2022 \ + --minify \ + --out-dir "$LIBRARY_DIST" + pnpm --filter="./lib" exec tsdown "$CONSUMER_ENTRY" \ + --no-config \ + --format esm \ + --platform browser \ + --target es2022 \ + --minify \ + --out-dir "$CONSUMER_DIST" + + - name: Measure gzip sizes + id: measure + env: + REPORT_DIR: ${{ runner.temp }}/size-report + LIBRARY_DIST: ${{ runner.temp }}/library-dist + CONSUMER_DIST: ${{ runner.temp }}/consumer-dist + run: | + node --experimental-strip-types .github/scripts/measure-size.mts + + - name: Save main size report + if: github.event_name == 'push' + uses: actions/cache/save@v4 + with: + path: ${{ runner.temp }}/size-report + key: bundle-size-${{ github.sha }} + + - name: Update pull request comment + if: github.event_name == 'pull_request' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_REPORT: ${{ runner.temp }}/size-report/report.json + BASE_REPORT: ${{ runner.temp }}/base-size-report/report.json + run: node --experimental-strip-types .github/scripts/update-size-comment.mts diff --git a/benchmark/package.json b/benchmark/package.json index 6ebc91e..c277e90 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -9,7 +9,7 @@ "measure": "node ./scripts/build-and-measure.mts" }, "dependencies": { - "@radiancejs/gl": "0.11.1", + "@radiancejs/gl": "workspace:*", "ogl": "1.0.11", "three": "0.185.1", "twgl.js": "7.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad82af8..d3b44bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: benchmark: dependencies: '@radiancejs/gl': - specifier: 0.11.1 - version: 0.11.1 + specifier: workspace:* + version: link:../lib ogl: specifier: 1.0.11 version: 1.0.11 From 600749eaf1adc74a38840680858b794616788c1b Mon Sep 17 00:00:00 2001 From: Julien Sulpis Date: Thu, 13 Aug 2026 22:52:00 +0200 Subject: [PATCH 2/2] ci: manual update --- .github/scripts/create-glcanvas-entry.mts | 11 +---------- .github/scripts/update-size-comment.mts | 17 ++++++++++++----- .github/workflows/size.yml | 7 +++---- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/scripts/create-glcanvas-entry.mts b/.github/scripts/create-glcanvas-entry.mts index 7eb27d6..5c53a03 100644 --- a/.github/scripts/create-glcanvas-entry.mts +++ b/.github/scripts/create-glcanvas-entry.mts @@ -8,13 +8,4 @@ if (!output) { } await mkdir(dirname(output), { recursive: true }); -await writeFile( - output, - `import { glCanvas } from "@radiancejs/gl"; - -glCanvas({ - canvas: "canvas", - fragment: await fetch("/shaders/fullscreen.frag").then((res) => res.text()), -}); -`, -); +await writeFile(output, `export { glCanvas } from "@radiancejs/gl";`); diff --git a/.github/scripts/update-size-comment.mts b/.github/scripts/update-size-comment.mts index 924bb64..f34f60d 100644 --- a/.github/scripts/update-size-comment.mts +++ b/.github/scripts/update-size-comment.mts @@ -7,7 +7,9 @@ const prReportPath = process.env.PR_REPORT; const baseReportPath = process.env.BASE_REPORT; if (!token || !repository || !issueNumber || !prReportPath || !baseReportPath) { - throw new Error("GITHUB_TOKEN, GITHUB_REPOSITORY, PR_NUMBER, PR_REPORT, and BASE_REPORT are required"); + throw new Error( + "GITHUB_TOKEN, GITHUB_REPOSITORY, PR_NUMBER, PR_REPORT, and BASE_REPORT are required", + ); } const [owner, repo] = repository.split("/"); @@ -25,7 +27,8 @@ const delta = (current: number, previous?: number) => { if (previous === undefined || previous === null) return ""; const difference = current - previous; const sign = difference > 0 ? "+" : ""; - const percentage = previous === 0 ? "" : ` (${sign}${((difference / previous) * 100).toFixed(1)}%)`; + const icon = difference > 0 ? " 🔺" : difference < 0 ? " 🎉" : ""; + const percentage = ` (${sign}${((difference / previous) * 100).toFixed(1)}%${icon})`; return `${sign}${format(difference)}${percentage}`; }; const row = (name: string, key: keyof typeof pr) => @@ -38,7 +41,7 @@ const body = [ "| Bundle | Base | PR | Delta |", "| --- | ---: | ---: | ---: |", row("`lib/dist/index.js`", "libraryGzipBytes"), - row("`glCanvas` consumer", "glCanvasGzipBytes"), + row("`glCanvas`", "glCanvasGzipBytes"), ].join("\n"); const api = `https://api.github.com/repos/${owner}/${repo}`; @@ -47,10 +50,14 @@ const headers = { Authorization: `Bearer ${token}`, "X-GitHub-Api-Version": "2022-11-28", }; -const commentsResponse = await fetch(`${api}/issues/${issueNumber}/comments?per_page=100`, { headers }); +const commentsResponse = await fetch(`${api}/issues/${issueNumber}/comments?per_page=100`, { + headers, +}); if (!commentsResponse.ok) { - throw new Error(`Unable to list comments: ${commentsResponse.status} ${await commentsResponse.text()}`); + throw new Error( + `Unable to list comments: ${commentsResponse.status} ${await commentsResponse.text()}`, + ); } const comments = await commentsResponse.json(); diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index 1ac162f..62884ef 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -47,7 +47,7 @@ jobs: - name: Create glCanvas entry env: CONSUMER_ENTRY: ${{ github.workspace }}/benchmark/.size/glcanvas-entry.ts - run: node --experimental-strip-types .github/scripts/create-glcanvas-entry.mts + run: node .github/scripts/create-glcanvas-entry.mts - name: Transpile and minify bundles env: @@ -76,8 +76,7 @@ jobs: REPORT_DIR: ${{ runner.temp }}/size-report LIBRARY_DIST: ${{ runner.temp }}/library-dist CONSUMER_DIST: ${{ runner.temp }}/consumer-dist - run: | - node --experimental-strip-types .github/scripts/measure-size.mts + run: node .github/scripts/measure-size.mts - name: Save main size report if: github.event_name == 'push' @@ -93,4 +92,4 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_REPORT: ${{ runner.temp }}/size-report/report.json BASE_REPORT: ${{ runner.temp }}/base-size-report/report.json - run: node --experimental-strip-types .github/scripts/update-size-comment.mts + run: node .github/scripts/update-size-comment.mts