diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 36fc7b5..9ed044f 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -27,159 +27,146 @@ jobs: - name: Install dependencies run: npm install - - name: Build Android bundle - run: | - npx expo export --platform android --output-dir ./bundle-android - env: - EXPO_NO_DOTENV: 1 - - - name: Build iOS bundle - run: | - npx expo export --platform ios --output-dir ./bundle-ios - env: - EXPO_NO_DOTENV: 1 - - - name: Calculate bundle sizes - id: bundle_sizes - run: | - # Calculate Android bundle size - ANDROID_SIZE=$(du -sb ./bundle-android | cut -f1) - ANDROID_SIZE_MB=$(echo "scale=2; $ANDROID_SIZE / 1048576" | bc) - - # Calculate iOS bundle size - IOS_SIZE=$(du -sb ./bundle-ios | cut -f1) - IOS_SIZE_MB=$(echo "scale=2; $IOS_SIZE / 1048576" | bc) - - # Total size - TOTAL_SIZE=$((ANDROID_SIZE + IOS_SIZE)) - TOTAL_SIZE_MB=$(echo "scale=2; $TOTAL_SIZE / 1048576" | bc) - - echo "android_size=$ANDROID_SIZE" >> $GITHUB_OUTPUT - echo "android_size_mb=$ANDROID_SIZE_MB" >> $GITHUB_OUTPUT - echo "ios_size=$IOS_SIZE" >> $GITHUB_OUTPUT - echo "ios_size_mb=$IOS_SIZE_MB" >> $GITHUB_OUTPUT - echo "total_size=$TOTAL_SIZE" >> $GITHUB_OUTPUT - echo "total_size_mb=$TOTAL_SIZE_MB" >> $GITHUB_OUTPUT - - echo "### šŸ“¦ Bundle Size Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Platform | Size (MB) | Size (bytes) |" >> $GITHUB_STEP_SUMMARY - echo "|----------|-----------|--------------|" >> $GITHUB_STEP_SUMMARY - echo "| Android | ${ANDROID_SIZE_MB} | ${ANDROID_SIZE} |" >> $GITHUB_STEP_SUMMARY - echo "| iOS | ${IOS_SIZE_MB} | ${IOS_SIZE} |" >> $GITHUB_STEP_SUMMARY - echo "| **Total** | **${TOTAL_SIZE_MB}** | **${TOTAL_SIZE}** |" >> $GITHUB_STEP_SUMMARY - - - name: Download previous bundle size - uses: actions/cache@v4 + - name: Build web bundle with stats + run: npx expo export --platform web --output-dir ./dist --stats-output ./dist/stats.json + + - name: Upload bundle stats artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 with: - path: ./previous-bundle-size - key: bundle-size-${{ github.sha }} - restore-keys: | - bundle-size- - - - name: Compare with previous bundle size - id: compare - run: | - CURRENT_TOTAL=${{ steps.bundle_sizes.outputs.total_size }} - - if [ -f "./previous-bundle-size/total_size.txt" ]; then - PREVIOUS_TOTAL=$(cat ./previous-bundle-size/total_size.txt) - DIFF=$((CURRENT_TOTAL - PREVIOUS_TOTAL)) - DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $PREVIOUS_TOTAL" | bc) - - echo "previous_size=$PREVIOUS_TOTAL" >> $GITHUB_OUTPUT - echo "diff=$DIFF" >> $GITHUB_OUTPUT - echo "diff_percent=$DIFF_PERCENT" >> $GITHUB_OUTPUT - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### šŸ“Š Comparison with Previous Build" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Previous Total**: $(echo "scale=2; $PREVIOUS_TOTAL / 1048576" | bc) MB" >> $GITHUB_STEP_SUMMARY - echo "- **Difference**: $(echo "scale=2; $DIFF / 1048576" | bc) MB ($DIFF_PERCENT%)" >> $GITHUB_STEP_SUMMARY - - # Alert if size increased by more than 10% - if (( $(echo "$DIFF_PERCENT > 10" | bc -l) )); then - echo "" >> $GITHUB_STEP_SUMMARY - echo "āš ļø **WARNING**: Bundle size increased by more than 10%!" >> $GITHUB_STEP_SUMMARY - echo "::warning::Bundle size increased by ${DIFF_PERCENT}% (from $(echo "scale=2; $PREVIOUS_TOTAL / 1048576" | bc) MB to $(echo "scale=2; $CURRENT_TOTAL / 1048576" | bc) MB)" - fi - - # Alert if size increased by more than 5MB - DIFF_MB=$(echo "scale=2; $DIFF / 1048576" | bc) - if (( $(echo "$DIFF_MB > 5" | bc -l) )); then - echo "" >> $GITHUB_STEP_SUMMARY - echo "āš ļø **WARNING**: Bundle size increased by more than 5 MB!" >> $GITHUB_STEP_SUMMARY - echo "::warning::Bundle size increased by ${DIFF_MB} MB" - fi - else - echo "No previous bundle size found. This is the first measurement." >> $GITHUB_STEP_SUMMARY - fi - - - name: Save current bundle size - run: | - mkdir -p ./previous-bundle-size - echo "${{ steps.bundle_sizes.outputs.total_size }}" > ./previous-bundle-size/total_size.txt - echo "${{ steps.bundle_sizes.outputs.android_size }}" > ./previous-bundle-size/android_size.txt - echo "${{ steps.bundle_sizes.outputs.ios_size }}" > ./previous-bundle-size/ios_size.txt - echo "${{ github.sha }}" > ./previous-bundle-size/sha.txt - - - name: Check bundle size limits - run: | - TOTAL_SIZE_MB=${{ steps.bundle_sizes.outputs.total_size_mb }} - - # Alert if total bundle size exceeds 50MB - if (( $(echo "$TOTAL_SIZE_MB > 50" | bc -l) )); then - echo "::warning::Total bundle size (${TOTAL_SIZE_MB} MB) exceeds 50 MB limit" - echo "" >> $GITHUB_STEP_SUMMARY - echo "āš ļø **WARNING**: Total bundle size (${TOTAL_SIZE_MB} MB) exceeds 50 MB limit!" >> $GITHUB_STEP_SUMMARY - fi - - # Alert if total bundle size exceeds 100MB (critical) - if (( $(echo "$TOTAL_SIZE_MB > 100" | bc -l) )); then - echo "::error::Total bundle size (${TOTAL_SIZE_MB} MB) exceeds 100 MB critical limit!" - echo "" >> $GITHUB_STEP_SUMMARY - echo "🚨 **CRITICAL**: Total bundle size (${TOTAL_SIZE_MB} MB) exceeds 100 MB critical limit!" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Comment PR with bundle size + name: bundle-stats-${{ github.sha }} + path: ./dist/stats.json + retention-days: 7 + + - name: Store bundle size history + if: github.ref == 'refs/heads/main' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + const { Octokit } = require("@octokit/rest"); + const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const historyFile = 'bundle-size-history.json'; + + let history = []; + try { + const { data: artifact } = await octokit.actions.listArtifactsForRepo({ + owner, + repo, + name: 'bundle-size-history', + }).then(res => res.data.artifacts[0]); + + if (artifact) { + const download = await octokit.actions.downloadArtifact({ + owner, + repo, + artifact_id: artifact.id, + archive_format: 'zip', + }); + const AdmZip = require('adm-zip'); + const zip = new AdmZip(Buffer.from(download.data)); + history = JSON.parse(zip.readAsText(historyFile)); + } + } catch (error) { + console.log('No existing history artifact found, creating a new one.'); + } + + const stats = JSON.parse(fs.readFileSync('./dist/stats.json', 'utf8')); + const totalSize = stats.assets.reduce((sum, a) => sum + a.size, 0); + + history.push({ + sha: context.sha, + date: new Date().toISOString(), + totalSize, + assets: stats.assets.map(a => ({ name: a.name, size: a.size })), + }); + + fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); + + - name: Upload bundle size history + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: bundle-size-history + path: bundle-size-history.json + retention-days: 90 + + - name: Download base branch bundle size + if: github.event_name == 'pull_request' + uses: actions/download-artifact@v4 + with: + name: bundle-stats-${{ github.event.pull_request.base.sha }} + path: ./base-bundle + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.pull_request.base.repo.id }}-${{ github.event.pull_request.base.sha }} + + + - name: Compare bundle sizes and comment if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | - const androidSize = '${{ steps.bundle_sizes.outputs.android_size_mb }}'; - const iosSize = '${{ steps.bundle_sizes.outputs.ios_size_mb }}'; - const totalSize = '${{ steps.bundle_sizes.outputs.total_size_mb }}'; - const previousSize = '${{ steps.compare.outputs.previous_size }}'; - const diff = '${{ steps.compare.outputs.diff }}'; - const diffPercent = '${{ steps.compare.outputs.diff_percent }}'; - + const fs = require('fs'); + const path = require('path'); + + function parseStats(filePath) { + if (!fs.existsSync(filePath)) return null; + const stats = JSON.parse(fs.readFileSync(filePath, 'utf8')); + const mainBundle = stats.assets.find(a => a.name === 'main.js'); + return { + totalSize: stats.assets.reduce((sum, a) => sum + a.size, 0), + mainBundleSize: mainBundle ? mainBundle.size : 0, + assets: stats.assets, + }; + } + + const baseStats = parseStats('./base-bundle/stats.json'); + const headStats = parseStats('./dist/stats.json'); + + if (!headStats) { + console.log('Could not find head bundle stats. Skipping comparison.'); + return; + } + let body = `## šŸ“¦ Bundle Size Report\n\n`; - body += `| Platform | Size (MB) |\n`; - body += `|----------|-----------|\n`; - body += `| Android | ${androidSize} |\n`; - body += `| iOS | ${iosSize} |\n`; - body += `| **Total** | **${totalSize}** |\n`; - - if (previousSize && diff) { - const diffMB = (parseFloat(diff) / 1048576).toFixed(2); - const diffPercentVal = parseFloat(diffPercent).toFixed(2); - const emoji = diff > 0 ? 'šŸ“ˆ' : 'šŸ“‰'; - - body += `\n### ${emoji} Comparison with Base Branch\n\n`; - body += `- **Change**: ${diffMB} MB (${diffPercentVal}%)\n`; - - if (parseFloat(diffPercent) > 10) { - body += `\nāš ļø **Warning**: Bundle size increased by more than 10%!\n`; + body += `| Asset | Size (KB) |\n`; + body += `|---|---|\n`; + headStats.assets.forEach(asset => { + body += `| ${asset.name} | ${(asset.size / 1024).toFixed(2)} |\n`; + }); + body += `| **Total** | **${(headStats.totalSize / 1024).toFixed(2)}** |\n`; + + if (baseStats) { + const totalDiff = headStats.totalSize - baseStats.totalSize; + const mainDiff = headStats.mainBundleSize - baseStats.mainBundleSize; + const totalDiffPercent = (totalDiff / baseStats.totalSize * 100).toFixed(2); + const mainDiffPercent = (mainDiff / baseStats.mainBundleSize * 100).toFixed(2); + + const emoji = totalDiff > 0 ? 'šŸ“ˆ' : 'šŸ“‰'; + + body += `\n### ${emoji} Comparison with base branch\n\n`; + body += `| Asset | Base (KB) | Head (KB) | Diff (KB) | Diff (%) |\n`; + body += `|---|---|---|---|---|\n`; + body += `| main.js | ${(baseStats.mainBundleSize / 1024).toFixed(2)} | ${(headStats.mainBundleSize / 1024).toFixed(2)} | ${(mainDiff / 1024).toFixed(2)} | ${mainDiffPercent}% |\n`; + body += `| **Total** | **${(baseStats.totalSize / 1024).toFixed(2)}** | **${(headStats.totalSize / 1024).toFixed(2)}** | **${(totalDiff / 1024).toFixed(2)}** | **${totalDiffPercent}%** |\n`; + + if (Math.abs(mainDiff) > 50 * 1024) { + core.setFailed(`Main bundle size changed by more than 50KB.`); + body += `\n\n**Error:** Main bundle size changed by more than 50KB.`; + } + if (Math.abs(totalDiff) > 100 * 1024) { + core.setFailed(`Total bundle size changed by more than 100KB.`); + body += `\n\n**Error:** Total bundle size changed by more than 100KB.`; } } - try { - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }); - } catch (error) { - console.warn('Skipping PR commenting due to permission limits (e.g. fork PRs):', error.message); - } + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06295fd..213f076 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,6 +163,7 @@ jobs: ${{ runner.os }}-eslint- - name: Lint + run: npm run lint -- --max-warnings=250 run: npm run lint -- --cache --cache-location .eslintcache - name: Format check diff --git a/eslint.config.js b/eslint.config.js index 0962a40..bdaca4a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -68,12 +68,12 @@ module.exports = defineConfig([ 'import/no-unresolved': 'off', // Prevent inline component definitions that defeat memoization - 'react/no-unstable-nested-components': ['warn', { allowAsProps: false }], + 'react/no-unstable-nested-components': ['error', { allowAsProps: false }], - 'jsx-a11y/alt-text': 'warn', - 'jsx-a11y/aria-props': 'warn', - 'jsx-a11y/aria-proptypes': 'warn', - 'jsx-a11y/aria-unsupported-elements': 'warn', + 'jsx-a11y/alt-text': 'error', + 'jsx-a11y/aria-props': 'error', + 'jsx-a11y/aria-proptypes': 'error', + 'jsx-a11y/aria-unsupported-elements': 'error', }, }, ]);