From 2f4ee560dd29253dd086a1600fd1bafd13405006 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Thu, 26 May 2022 11:49:29 -0500 Subject: [PATCH] Add bundle-size check action (#3454) * feat: add scripts action, bundle-size checker * chore: trigger action * fix: update prefix logic --- .github/scripts/bundle-size.mjs | 81 ++++++++++++++++++++ .github/workflows/scripts.yml | 48 ++++++++++++ packages/astro/src/runtime/client/idle.ts | 2 +- packages/astro/src/runtime/client/load.ts | 2 +- packages/astro/src/runtime/client/media.ts | 2 +- packages/astro/src/runtime/client/only.ts | 4 +- packages/astro/src/runtime/client/visible.ts | 2 +- 7 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/bundle-size.mjs create mode 100644 .github/workflows/scripts.yml diff --git a/.github/scripts/bundle-size.mjs b/.github/scripts/bundle-size.mjs new file mode 100644 index 000000000..4166735a9 --- /dev/null +++ b/.github/scripts/bundle-size.mjs @@ -0,0 +1,81 @@ +import { build } from 'esbuild'; + +const CLIENT_RUNTIME_PATH = 'packages/astro/src/runtime/client/'; + +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +export default async function checkBundleSize({ github, context }) { + const PR_NUM = context.payload.pull_request.number; + const SHA = context.payload.pull_request.head.sha; + + const { data: files } = await github.rest.pulls.listFiles({ + ...context.repo, + pull_number: PR_NUM, + }); + const clientRuntimeFiles = files.filter(({ filename }) => filename.startsWith(CLIENT_RUNTIME_PATH)); + if (clientRuntimeFiles.length === 0) return; + + const table = [ + '| File | Old Size | New Size | Change |', + '| ---- | -------- | -------- | ------ |', + ]; + const output = await bundle(clientRuntimeFiles); + + for (let [filename, { oldSize, newSize, sourceFile }] of Object.entries(output)) { + filename = filename !== 'hmr' ? `client:${filename}` : filename; + const prefix = (newSize - oldSize) === 0 ? '' : (newSize - oldSize) > 0 ? '+ ' : '- '; + const change = `${prefix}${formatBytes(newSize - oldSize)}`; + table.push(`| [\`${filename}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${context.payload.pull_request.head.ref}/${sourceFile}) | ${formatBytes(oldSize)} | ${formatBytes(newSize)} | ${change} |`); + } + + const { data: comments } = await github.rest.issues.listComments({ + ...context.repo, + issue_number: PR_NUM + }) + const comment = comments.find(comment => comment.user.login === 'github-actions[bot]' && comment.body.includes('Bundle Size Check')); + const method = comment ? 'updateComment' : 'createComment'; + const payload = comment ? { comment_id: comment.id } : { issue_number: PR_NUM }; + await github.rest.issues[method]({ + ...context.repo, + ...payload, + body: `### ⚖️ Bundle Size Check + +Latest commit: ${SHA} + +${table.join('\n')}`, + }); +} + +async function bundle(files) { + const { metafile } = await build({ + entryPoints: [...files.map(({ filename }) => filename), ...files.map(({ filename }) => `main/${filename}`)], + bundle: true, + minify: true, + sourcemap: false, + target: ['es2018'], + outdir: 'out', + metafile: true, + }) + + return Object.entries(metafile.outputs).reduce((acc, [filename, info]) => { + filename = filename.slice('out/'.length); + if (filename.startsWith('main/')) { + filename = filename.slice('main/'.length).replace(CLIENT_RUNTIME_PATH, '').replace('.js', ''); + const oldSize = info.bytes; + return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? {}, { oldSize }) }); + } + filename = filename.replace(CLIENT_RUNTIME_PATH, '').replace('.js', ''); + const newSize = info.bytes; + return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? {}, { newSize, sourceFile: Object.keys(info.inputs).find(src => src.endsWith('.ts')) }) }); + }, {}); +} diff --git a/.github/workflows/scripts.yml b/.github/workflows/scripts.yml new file mode 100644 index 000000000..40a29ee61 --- /dev/null +++ b/.github/workflows/scripts.yml @@ -0,0 +1,48 @@ +name: Scripts + +on: + pull_request: + branches: + - 'main' + +# Automatically cancel in-progress actions on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + bundle: + name: Bundle Size + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Checkout Main into tmp + uses: actions/checkout@v2 + with: + ref: main + path: main + + - name: Setup PNPM + uses: pnpm/action-setup@v2.2.1 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Check Bundle Size + uses: actions/github-script@v6 + with: + script: | + const { default: script } = await import('${{ github.workspace }}/.github/scripts/bundle-size.mjs') + await script({ github, context }) diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index 627d896db..e1e1c5b2f 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -1,7 +1,7 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** - * Hydrate this component as soon as the main thread is free + * Hydrate this component as soon as the main thread is free! * (or after a short delay, if `requestIdleCallback`) isn't supported */ export default async function onIdle( diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index cf4cd83af..d969b4061 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -1,7 +1,7 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** - * Hydrate this component immediately + * Hydrate this component immediately! */ export default async function onLoad( astroId: string, diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index 32e883908..edaa9a433 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -1,7 +1,7 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** - * Hydrate this component when a matching media query is found + * Hydrate this component when a matching media query is found! */ export default async function onMedia( astroId: string, diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index 6400d44b8..04937c608 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -1,9 +1,9 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** - * Hydrate this component immediately + * Hydrate this component only on the client */ -export default async function onLoad( +export default async function onOnly( astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index e0c1fdc73..e9c3e3310 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -1,7 +1,7 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** - * Hydrate this component when one of it's children becomes visible. + * Hydrate this component when one of it's children becomes visible! * We target the children because `astro-root` is set to `display: contents` * which doesn't work with IntersectionObserver */