Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/scripts/check-bundle-size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')

const repoRoot = process.cwd()
const appDir = path.join(repoRoot, 'app')
const staticDir = path.join(appDir, '.next', 'static')
const outDir = path.join(appDir, '.next', 'analyze')
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true })

function walk(dir, files = []) {
if (!fs.existsSync(dir)) return files
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name)
const stat = fs.statSync(p)
if (stat.isDirectory()) walk(p, files)
else files.push(p)
}
return files
}

const jsFiles = walk(staticDir).filter((f) => f.endsWith('.js'))
let total = 0
let largest = { file: null, size: 0 }
for (const f of jsFiles) {
const s = fs.statSync(f).size
total += s
if (s > largest.size) {
largest.size = s
largest.file = path.relative(repoRoot, f)
}
}

const summary = {
totalBytes: total,
largestFile: largest.file,
largestBytes: largest.size,
fileCount: jsFiles.length,
budgetTotal: Number(process.env.BUNDLE_BUDGET_TOTAL || 0),
budgetLargest: Number(process.env.BUNDLE_BUDGET_PER_LARGEST || 0),
}

fs.writeFileSync(path.join(outDir, 'summary.json'), JSON.stringify(summary, null, 2))
console.log('Bundle summary:', summary)

if (summary.budgetTotal > 0 && total > summary.budgetTotal) {
console.error(`Total bundle size ${total} exceeds budget ${summary.budgetTotal}`)
process.exit(1)
}

if (summary.budgetLargest > 0 && largest.size > summary.budgetLargest) {
console.error(`Largest file ${largest.size} exceeds per-file budget ${summary.budgetLargest}`)
process.exit(1)
}

process.exit(0)
67 changes: 67 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ on:
pull_request:
branches: [main]

permissions:
contents: read
issues: write
pull-requests: write

jobs:
quality:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -113,3 +118,65 @@ jobs:
flags: app
name: app-coverage
fail_ci_if_error: false

bundle-analysis:
runs-on: ubuntu-latest
needs: quality

steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install app dependencies
run: cd app && npm install

- name: Build app with analysis
run: cd app && npm run analyze

- name: List analyzer output
run: ls -la app/.next || true; ls -la app/.next/analyze || true

- name: Check bundle sizes
run: node .github/scripts/check-bundle-size.js
env:
BUNDLE_BUDGET_TOTAL: 5000000
BUNDLE_BUDGET_PER_LARGEST: 500000

- name: Upload analyzer artifacts
uses: actions/upload-artifact@v4
with:
name: app-bundle-analysis
path: app/.next/analyze

- name: Upload bundle summary
uses: actions/upload-artifact@v4
with:
name: app-bundle-summary
path: app/.next/analyze/summary.json

- name: Comment PR with summary
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v6
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const path = 'app/.next/analyze/summary.json';
if (fs.existsSync(path)) {
const summary = JSON.parse(fs.readFileSync(path, 'utf8'));
const body = `Bundle analysis summary:\n\n- Total JS size: ${summary.totalBytes} bytes\n- Largest file: ${summary.largestFile} (${summary.largestBytes} bytes)\n\nArtifacts: app-bundle-analysis`;
github.rest.issues.createComment({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
} else {
console.log('No summary available')
}
8 changes: 8 additions & 0 deletions app/app/.next/analyze/summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"totalBytes": 0,
"largestFile": null,
"largestBytes": 0,
"fileCount": 0,
"budgetTotal": 0,
"budgetLargest": 0
}
8 changes: 7 additions & 1 deletion app/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
/** @type {import('next').NextConfig} */
import withBundleAnalyzer from '@next/bundle-analyzer'

const withBundleAnalyzerConfig = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})

const nextConfig = {
output: "standalone",

Expand All @@ -7,4 +13,4 @@ const nextConfig = {
},
}

export default nextConfig
export default withBundleAnalyzerConfig(nextConfig)
3 changes: 3 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"scripts": {
"build": "next build",
"analyze": "cross-env ANALYZE=true npm run build",
"dev": "next dev",
"lint": "eslint .",
"start": "next start",
Expand Down Expand Up @@ -83,6 +84,8 @@
"zod": "3.25.76"
},
"devDependencies": {
"@next/bundle-analyzer": "16.2.9",
"cross-env": "^7.0.3",
"@tailwindcss/postcss": "^4.1.9",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
Expand Down
Loading
Loading