diff --git a/.github/workflows/bundle-stats.yml b/.github/workflows/bundle-stats.yml new file mode 100644 index 00000000..a9dcb0e5 --- /dev/null +++ b/.github/workflows/bundle-stats.yml @@ -0,0 +1,57 @@ +name: Bundle Stats +on: + workflow_call: + inputs: + mode: + required: true + type: string + description: "'store' or 'compare'" + branch: + required: true + type: string + outputs: + stats: + description: "Bundle stats comparison" + value: ${{ jobs.bundle-stats.outputs.stats }} + +jobs: + bundle-stats: + runs-on: ubuntu-latest + outputs: + stats: ${{ steps.gist-ops.outputs.stats }} + steps: + - uses: actions/github-script@v6 + id: gist-ops + with: + script: | + const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}'; + + async function getGistContent() { + const { data } = await github.rest.gists.get({ gist_id: gistId }); + return JSON.parse(data.files['bundle-stats.json'].content || '{}'); + } + + async function updateGistContent(content) { + await github.rest.gists.update({ + gist_id: gistId, + files: { + 'bundle-stats.json': { + content: JSON.stringify(content, null, 2) + } + } + }); + } + + if ('${{ inputs.mode }}' === 'store') { + const stats = require('/tmp/bundle-stats.json'); + const content = await getGistContent(); + content['${{ inputs.branch }}'] = stats; + await updateGistContent(content); + } else { + const content = await getGistContent(); + const baseStats = content['${{ inputs.branch }}']; + const newStats = require('/tmp/bundle-stats.json'); + + const comparison = `minecraft.html (normal build gzip)\n${baseStats.total}MB (${baseStats.gzipped}MB compressed) -> ${newStats.total}MB (${newStats.gzipped}MB compressed)`; + core.setOutput('stats', comparison); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92b7e7f3..46a2de6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,13 @@ jobs: uses: pnpm/action-setup@v4 - run: pnpm install - run: pnpm check-build + - name: Create zip package for size comparison + run: | + mkdir -p package + cp -r dist package/ + cd package + zip -r ../self-host.zip . + - run: pnpm build-single-file - run: pnpm build-playground - run: pnpm build-storybook - run: pnpm test-unit @@ -40,6 +47,58 @@ jobs: # if: ${{ github.event.pull_request.base.ref == 'release' }} # env: # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Parse Bundle Stats + run: | + SIZE_BYTES=$(du -s dist/single/minecraft.html 2>/dev/null | cut -f1) + GZIP_BYTES=$(du -s self-host.zip 2>/dev/null | cut -f1) + SIZE=$(echo "scale=2; $SIZE_BYTES/1024/1024" | bc) + GZIP_SIZE=$(echo "scale=2; $GZIP_BYTES/1024/1024" | bc) + echo "{\"total\": ${SIZE}, \"gzipped\": ${GZIP_SIZE}}" > /tmp/bundle-stats.json + + - name: Compare Bundle Stats + id: compare + uses: ./.github/workflows/bundle-stats + with: + mode: compare + branch: ${{ github.event.pull_request.base.ref }} + + - name: Store Bundle Stats + if: github.event.pull_request.base.ref == 'next' + uses: ./.github/workflows/bundle-stats + with: + mode: store + branch: ${{ github.event.pull_request.base.ref }} + + - name: Update PR Description + uses: actions/github-script@v6 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + let body = pr.body || ''; + const statsMarker = '### Bundle Size'; + const comparison = '${{ steps.compare.outputs.stats }}'; + + if (body.includes(statsMarker)) { + body = body.replace( + new RegExp(`${statsMarker}[^\n]*\n[^\n]*`), + `${statsMarker}\n${comparison}` + ); + } else { + body += `\n\n${statsMarker}\n${comparison}`; + } + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + body + }); # dedupe-check: # runs-on: ubuntu-latest # if: github.event.pull_request.head.ref == 'next' diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 1701fc5c..36ccb9b0 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -203,6 +203,12 @@ const appConfig = defineConfig({ }) build.onAfterBuild(async () => { if (SINGLE_FILE_BUILD) { + // check that only index.html is in the dist/single folder + const singleBuildFiles = fs.readdirSync('./dist/single') + if (singleBuildFiles.length !== 1 || singleBuildFiles[0] !== 'index.html') { + throw new Error('Single file build must only have index.html in the dist/single folder. Ensure workers are imported & built correctly.') + } + // process index.html const singleBuildHtml = './dist/single/index.html' let html = fs.readFileSync(singleBuildHtml, 'utf8')