diff --git a/.github/scripts/create-glcanvas-entry.mts b/.github/scripts/create-glcanvas-entry.mts new file mode 100644 index 0000000..5c53a03 --- /dev/null +++ b/.github/scripts/create-glcanvas-entry.mts @@ -0,0 +1,11 @@ +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, `export { glCanvas } from "@radiancejs/gl";`); 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..f34f60d --- /dev/null +++ b/.github/scripts/update-size-comment.mts @@ -0,0 +1,80 @@ +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 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) => + `| ${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`", "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..62884ef 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,77 @@ 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 .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 .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 .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