bundle-size.mjs 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import { build } from 'esbuild';
  2. import { existsSync } from 'node:fs';
  3. const CLIENT_RUNTIME_PATH = 'packages/astro/src/runtime/client/';
  4. function formatBytes(bytes, decimals = 2) {
  5. if (bytes === 0) return '0 B';
  6. const k = 1024;
  7. const dm = decimals < 0 ? 0 : decimals;
  8. const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  9. const i = Math.floor(Math.log(bytes) / Math.log(k));
  10. return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  11. }
  12. export default async function checkBundleSize({ github, context }) {
  13. const PR_NUM = context.payload.pull_request.number;
  14. const SHA = context.payload.pull_request.head.sha;
  15. const { data: files } = await github.rest.pulls.listFiles({
  16. ...context.repo,
  17. pull_number: PR_NUM,
  18. });
  19. const clientRuntimeFiles = files.filter((file) => {
  20. return file.filename.startsWith(CLIENT_RUNTIME_PATH) && file.status !== 'removed'
  21. });
  22. if (clientRuntimeFiles.length === 0) return;
  23. const table = [
  24. '| File | Old Size | New Size | Change |',
  25. '| ---- | -------- | -------- | ------ |',
  26. ];
  27. const output = await bundle(clientRuntimeFiles);
  28. for (let [filename, { oldSize, newSize, sourceFile }] of Object.entries(output)) {
  29. filename = ['idle', 'load', 'media', 'only', 'visible'].includes(filename) ? `client:${filename}` : filename;
  30. const prefix = (newSize - oldSize) === 0 ? '' : (newSize - oldSize) > 0 ? '+ ' : '- ';
  31. const change = `${prefix}${formatBytes(newSize - oldSize)}`;
  32. 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} |`);
  33. }
  34. const { data: comments } = await github.rest.issues.listComments({
  35. ...context.repo,
  36. issue_number: PR_NUM
  37. })
  38. const comment = comments.find(comment => comment.user.login === 'github-actions[bot]' && comment.body.includes('Bundle Size Check'));
  39. const method = comment ? 'updateComment' : 'createComment';
  40. const payload = comment ? { comment_id: comment.id } : { issue_number: PR_NUM };
  41. await github.rest.issues[method]({
  42. ...context.repo,
  43. ...payload,
  44. body: `### ⚖️ Bundle Size Check
  45. Latest commit: ${SHA}
  46. ${table.join('\n')}`,
  47. });
  48. }
  49. async function bundle(files) {
  50. const { metafile } = await build({
  51. entryPoints: [...files.map(({ filename }) => filename), ...files.map(({ filename }) => `main/${filename}`).filter(f => existsSync(f))],
  52. bundle: true,
  53. minify: true,
  54. sourcemap: false,
  55. target: ['es2018'],
  56. outdir: 'out',
  57. external: ['astro:*', 'aria-query', 'axobject-query'],
  58. metafile: true,
  59. })
  60. return Object.entries(metafile.outputs).reduce((acc, [filename, info]) => {
  61. filename = filename.slice('out/'.length);
  62. if (filename.startsWith('main/')) {
  63. filename = filename.slice('main/'.length).replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
  64. const oldSize = info.bytes;
  65. return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { oldSize }) });
  66. }
  67. filename = filename.replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
  68. const newSize = info.bytes;
  69. return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { newSize, sourceFile: Object.keys(info.inputs).find(src => src.endsWith('.ts')) }) });
  70. }, {});
  71. }