Add bundle-size check action (#3454)
* feat: add scripts action, bundle-size checker * chore: trigger action * fix: update prefix logic
This commit is contained in:
parent
d92d28317f
commit
2f4ee560dd
7 changed files with 135 additions and 6 deletions
81
.github/scripts/bundle-size.mjs
vendored
Normal file
81
.github/scripts/bundle-size.mjs
vendored
Normal file
|
@ -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')) }) });
|
||||||
|
}, {});
|
||||||
|
}
|
48
.github/workflows/scripts.yml
vendored
Normal file
48
.github/workflows/scripts.yml
vendored
Normal file
|
@ -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 })
|
|
@ -1,7 +1,7 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
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
|
* (or after a short delay, if `requestIdleCallback`) isn't supported
|
||||||
*/
|
*/
|
||||||
export default async function onIdle(
|
export default async function onIdle(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrate this component immediately
|
* Hydrate this component immediately!
|
||||||
*/
|
*/
|
||||||
export default async function onLoad(
|
export default async function onLoad(
|
||||||
astroId: string,
|
astroId: string,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
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(
|
export default async function onMedia(
|
||||||
astroId: string,
|
astroId: string,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
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,
|
astroId: string,
|
||||||
options: HydrateOptions,
|
options: HydrateOptions,
|
||||||
getHydrateCallback: GetHydrateCallback
|
getHydrateCallback: GetHydrateCallback
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
|
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`
|
* We target the children because `astro-root` is set to `display: contents`
|
||||||
* which doesn't work with IntersectionObserver
|
* which doesn't work with IntersectionObserver
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue