diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index da27c08ecc..d287ea7176 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -205,3 +205,162 @@ jobs:
with:
dep-cache-key: ${{ needs.build.outputs.dep-cache-key }}
submodule-cache-key: ${{ needs.checkout-submodules.outputs.submodule-cache-key }}
+
+ bundle-size:
+ needs: [build]
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ if: github.event_name == 'pull_request' || github.event_name == 'push'
+ # This job runs as a side task to monitor bundle sizes without blocking the workflow
+
+ steps:
+ - name: Checkout current branch
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - uses: actions/cache/restore@v4
+ id: dep-cache
+ with:
+ path: ${{github.workspace}}
+ key: ${{ needs.build.outputs.dep-cache-key }}
+
+ - name: Use Node.js 24
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: 'npm'
+
+ - name: Install Dependencies (if not restored from cache)
+ if: steps.dep-cache.outputs.cache-hit != 'true'
+ run: npm ci
+ working-directory: ${{ github.workspace }}
+
+ - name: Checkout baseline branch (for PR comparison)
+ if: github.event_name == 'pull_request'
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.base_ref }}
+ path: baseline-workspace
+
+ - name: Use Node.js 24 (baseline)
+ if: github.event_name == 'pull_request'
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ cache: 'npm'
+
+ - name: Install dependencies in baseline
+ if: github.event_name == 'pull_request'
+ working-directory: baseline-workspace
+ run: npm ci || true
+ continue-on-error: true
+
+ - name: Build baseline packages
+ if: github.event_name == 'pull_request'
+ working-directory: baseline-workspace
+ run: npm run build --workspaces --if-present || true
+ continue-on-error: true
+
+ - name: Analyze baseline bundle sizes
+ if: github.event_name == 'pull_request'
+ run: |
+ # Copy the analysis script to baseline workspace if it doesn't exist there
+ # (since this is a new script, it might not be in the base branch yet)
+ if [ ! -f baseline-workspace/scripts/analyze-bundle-size.js ]; then
+ mkdir -p baseline-workspace/scripts
+ cp scripts/analyze-bundle-size.js baseline-workspace/scripts/ || true
+ fi
+ # Run analysis in baseline workspace
+ cd baseline-workspace
+ node scripts/analyze-bundle-size.js --save --output=../baseline-results.json || true
+ continue-on-error: true
+
+ - name: Build all packages for bundle analysis
+ run: npm run build --workspaces --if-present
+ working-directory: ${{ github.workspace }}
+
+ - name: Analyze Bundle Sizes (with comparison if PR)
+ if: github.event_name == 'pull_request'
+ id: bundle-analysis
+ run: |
+ # Ensure script exists
+ if [ ! -f scripts/analyze-bundle-size.js ]; then
+ echo "ā Error: scripts/analyze-bundle-size.js not found"
+ exit 1
+ fi
+ if [ -f baseline-results.json ]; then
+ echo "š Comparing bundle sizes with baseline (${{ github.base_ref }})..."
+ node scripts/analyze-bundle-size.js --compare --baseline=baseline-results.json --summary --baseline-ref=${{ github.base_ref }} > bundle-output.txt 2>&1 || true
+ # Extract summary if present
+ if grep -q "---SUMMARY_START---" bundle-output.txt; then
+ sed -n '/---SUMMARY_START---/,/---SUMMARY_END---/p' bundle-output.txt | sed '1d;$d' > bundle-summary.md
+ fi
+ # Show full output
+ cat bundle-output.txt
+ else
+ echo "š Analyzing bundle sizes (baseline comparison unavailable)..."
+ node scripts/analyze-bundle-size.js
+ fi
+ working-directory: ${{ github.workspace }}
+ continue-on-error: true
+
+ - name: Post Bundle Size Summary to PR
+ if: github.event_name == 'pull_request' && (steps.bundle-analysis.outcome == 'success' || steps.bundle-analysis.outcome == 'failure')
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs');
+ const path = require('path');
+
+ const summaryPath = path.join(process.env.GITHUB_WORKSPACE, 'bundle-summary.md');
+
+ if (fs.existsSync(summaryPath)) {
+ const summary = fs.readFileSync(summaryPath, 'utf-8');
+
+ // Find existing comment
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const existingComment = comments.data.find(
+ c => c.user.type === 'Bot' && c.body.includes('š¦ Bundle Size Analysis')
+ );
+
+ const commentBody = summary + '\n\nGenerated by [bundle-size](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow';
+
+ if (existingComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existingComment.id,
+ body: commentBody,
+ });
+ console.log('ā
Updated PR comment');
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: commentBody,
+ });
+ console.log('ā
Posted PR comment');
+ }
+ } else {
+ console.log('No bundle summary found, skipping PR comment');
+ }
+
+ - name: Analyze Bundle Sizes (push to master)
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ run: |
+ if [ ! -f scripts/analyze-bundle-size.js ]; then
+ echo "ā Error: scripts/analyze-bundle-size.js not found"
+ exit 1
+ fi
+ node scripts/analyze-bundle-size.js
+ working-directory: ${{ github.workspace }}
diff --git a/.gitignore b/.gitignore
index f6a429995e..0a944fefb6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,15 +106,16 @@ Temporary Items
# End of https://www.toptal.com/developers/gitignore/api/node
-# ethereumjs
+# ethereumjs
datadir*
## Vite
stats.html
*bundle*.js
+!scripts/analyze-bundle-size.js
## Vitest
__snapshots__
# CSpell
-.cspellcache
\ No newline at end of file
+.cspellcache
diff --git a/package.json b/package.json
index ce899901f4..27703bfd08 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,9 @@
"spellcheck": "npm run spellcheck:ts && npm run spellcheck:md",
"spellcheck:ts": "npx cspell --gitignore -e \"./packages/ethereum-tests\" -e \"./packages/wallet/test\" -e \"./packages/client/archive\" -c ./config/cspell-ts.json \"./packages/**/*.ts\" --cache --show-suggestions --show-context",
"spellcheck:md": "npx cspell --gitignore -e \"./packages/ethereum-tests\" -e \"./packages/client/withdrawals-testnet/**\" -e \"./packages/**/docs\" -c ./config/cspell-md.json \"**.md\" --cache --show-suggestions --show-context",
+ "bundle:analyze": "node scripts/analyze-bundle-size.js",
+ "bundle:save": "node scripts/analyze-bundle-size.js --save",
+ "bundle:compare": "node scripts/analyze-bundle-size.js --compare --baseline=bundle-size-baseline.json",
"tsc": "npm run tsc --workspaces --if-present"
},
"devDependencies": {
diff --git a/scripts/BUNDLE_SIZE_ANALYSIS.md b/scripts/BUNDLE_SIZE_ANALYSIS.md
new file mode 100644
index 0000000000..76c876b9fd
--- /dev/null
+++ b/scripts/BUNDLE_SIZE_ANALYSIS.md
@@ -0,0 +1,102 @@
+# Bundle Size Analysis
+
+This document describes the bundle size analysis tooling for the EthereumJS monorepo.
+
+## Overview
+
+The bundle size analysis script (`scripts/analyze-bundle-size.js`) analyzes the compiled bundle sizes for all packages in the monorepo. It calculates both raw and gzipped sizes for ESM and CJS builds.
+
+## Usage
+
+### Basic Analysis
+
+Analyze current bundle sizes:
+
+```bash
+npm run bundle:analyze
+```
+
+### Save Baseline
+
+Save current bundle sizes as a baseline for comparison:
+
+```bash
+npm run bundle:save
+# Or with custom output file:
+node scripts/analyze-bundle-size.js --save --output=my-baseline.json
+```
+
+### Compare with Baseline
+
+Compare current bundle sizes with a saved baseline:
+
+```bash
+npm run bundle:compare
+# Or with custom baseline file:
+node scripts/analyze-bundle-size.js --compare --baseline=my-baseline.json
+```
+
+## CI Integration
+
+The bundle size analysis runs automatically in CI on:
+- **Pull Requests**: Compares bundle sizes against the base branch (e.g., master)
+- **Pushes to master**: Analyzes bundle sizes without comparison
+
+The CI job:
+- Runs as a non-blocking side task (`continue-on-error: true`)
+- Shows comparison results highlighting significant changes (>5% or >10KB)
+- Does not fail the workflow if bundle sizes increase
+
+## Output Format
+
+### Standard Analysis
+
+```
+š Bundle Size Analysis Results
+
+Package ESM Raw ESM Gzip CJS Raw CJS Gzip Total Raw Total Gzip
+genesis 723.09 KB 259.54 KB 723.68 KB 259.98 KB 1.41 MB 519.53 KB
+...
+```
+
+### Comparison Mode
+
+```
+š Bundle Size Comparison (Current vs Baseline)
+
+Package Baseline Raw Current Raw Diff Raw Baseline Gzip Current Gzip Diff Gzip
+genesis 1.41 MB 1.42 MB +10.5 KB (+0.7%) 519.53 KB 521.2 KB +1.67 KB (+0.3%)
+```
+
+**Indicators:**
+- ā ļø Significant increase (>5% or >10KB)
+- ā
Significant decrease (>5% or >10KB)
+- (no indicator) No significant change
+
+## For Breaking Releases
+
+As mentioned by the PM, for breaking releases you should:
+1. Run bundle analysis before the release
+2. Review the results to ensure bundle sizes are acceptable
+3. Use the comparison feature to track changes from the previous release
+
+## Manual Comparison Workflow
+
+To manually compare your changes with master:
+
+```bash
+# 1. Checkout master and build
+git checkout master
+npm ci
+npm run build --workspaces --if-present
+
+# 2. Save baseline
+npm run bundle:save -- --output=master-baseline.json
+
+# 3. Checkout your branch and build
+git checkout your-branch
+npm run build --workspaces --if-present
+
+# 4. Compare
+npm run bundle:analyze -- --compare --baseline=master-baseline.json
+```
diff --git a/scripts/analyze-bundle-size.js b/scripts/analyze-bundle-size.js
new file mode 100755
index 0000000000..c318ba93e6
--- /dev/null
+++ b/scripts/analyze-bundle-size.js
@@ -0,0 +1,442 @@
+#!/usr/bin/env node
+/**
+ * Bundle size analysis script for EthereumJS monorepo
+ * Analyzes the dist folder sizes for all packages and reports bundle sizes
+ * Supports comparison mode to compare against a baseline
+ */
+
+import { readdir, stat, writeFile, readFile, mkdir } from 'fs/promises'
+import { createGzip } from 'zlib'
+import { createReadStream } from 'fs'
+import { join } from 'path'
+import { fileURLToPath } from 'url'
+import { dirname } from 'path'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+const rootDir = join(__dirname, '..')
+const packagesDir = join(rootDir, 'packages')
+
+/**
+ * Get gzipped size of a file
+ */
+async function getGzipSize(filePath) {
+ return new Promise((resolve, reject) => {
+ const chunks = []
+ const gzip = createGzip()
+ const readStream = createReadStream(filePath)
+
+ readStream
+ .pipe(gzip)
+ .on('data', (chunk) => chunks.push(chunk))
+ .on('end', () => {
+ const size = chunks.reduce((acc, chunk) => acc + chunk.length, 0)
+ resolve(size)
+ })
+ .on('error', reject)
+ })
+}
+
+/**
+ * Recursively get all files in a directory
+ */
+async function getAllFiles(dir, fileList = []) {
+ const files = await readdir(dir, { withFileTypes: true })
+
+ for (const file of files) {
+ const filePath = join(dir, file.name)
+
+ if (file.isDirectory()) {
+ await getAllFiles(filePath, fileList)
+ } else if (file.isFile() && (file.name.endsWith('.js') || file.name.endsWith('.mjs'))) {
+ fileList.push(filePath)
+ }
+ }
+
+ return fileList
+}
+
+/**
+ * Calculate total size of a directory
+ */
+async function calculateDirSize(dir) {
+ try {
+ const files = await getAllFiles(dir)
+ let totalSize = 0
+ let totalGzipSize = 0
+
+ for (const file of files) {
+ const stats = await stat(file)
+ totalSize += stats.size
+ totalGzipSize += await getGzipSize(file)
+ }
+
+ return { raw: totalSize, gzip: totalGzipSize, fileCount: files.length }
+ } catch (error) {
+ // Directory doesn't exist or is empty
+ return { raw: 0, gzip: 0, fileCount: 0 }
+ }
+}
+
+/**
+ * Format bytes to human readable format
+ */
+function formatBytes(bytes) {
+ if (bytes === 0) return '0 B'
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
+}
+
+/**
+ * Format difference with sign and color indicators
+ */
+function formatDiff(current, baseline) {
+ if (baseline === 0) return current > 0 ? '+NEW' : '0 B'
+ const diff = current - baseline
+ const percent = ((diff / baseline) * 100).toFixed(1)
+ const sign = diff >= 0 ? '+' : ''
+ return `${sign}${formatBytes(diff)} (${sign}${percent}%)`
+}
+
+/**
+ * Analyze bundle sizes for all packages
+ */
+async function analyzeBundleSizes(packagesDirPath = packagesDir) {
+ const packages = await readdir(packagesDirPath, { withFileTypes: true })
+ const results = []
+
+ for (const pkg of packages) {
+ if (!pkg.isDirectory()) continue
+
+ const packagePath = join(packagesDirPath, pkg.name)
+ const distPath = join(packagePath, 'dist')
+
+ // Check if dist folder exists
+ try {
+ await stat(distPath)
+ } catch {
+ // No dist folder, skip
+ continue
+ }
+
+ // Calculate sizes for both ESM and CJS builds
+ const esmPath = join(distPath, 'esm')
+ const cjsPath = join(distPath, 'cjs')
+
+ const esmSize = await calculateDirSize(esmPath)
+ const cjsSize = await calculateDirSize(cjsPath)
+
+ const totalRaw = esmSize.raw + cjsSize.raw
+ const totalGzip = esmSize.gzip + cjsSize.gzip
+
+ results.push({
+ package: pkg.name,
+ esm: esmSize,
+ cjs: cjsSize,
+ total: { raw: totalRaw, gzip: totalGzip },
+ })
+ }
+
+ // Sort by total size (descending)
+ results.sort((a, b) => b.total.raw - a.total.raw)
+
+ return results
+}
+
+/**
+ * Print analysis results
+ */
+function printResults(results, title = 'š Bundle Size Analysis Results') {
+ console.log(`\n${title}\n`)
+ console.log('Package'.padEnd(20) + 'ESM Raw'.padEnd(12) + 'ESM Gzip'.padEnd(12) + 'CJS Raw'.padEnd(12) + 'CJS Gzip'.padEnd(12) + 'Total Raw'.padEnd(12) + 'Total Gzip')
+ console.log('-'.repeat(100))
+
+ for (const result of results) {
+ if (result.total.raw === 0) continue
+
+ const esmRaw = formatBytes(result.esm.raw)
+ const esmGzip = formatBytes(result.esm.gzip)
+ const cjsRaw = formatBytes(result.cjs.raw)
+ const cjsGzip = formatBytes(result.cjs.gzip)
+ const totalRaw = formatBytes(result.total.raw)
+ const totalGzip = formatBytes(result.total.gzip)
+
+ console.log(
+ result.package.padEnd(20) +
+ esmRaw.padEnd(12) +
+ esmGzip.padEnd(12) +
+ cjsRaw.padEnd(12) +
+ cjsGzip.padEnd(12) +
+ totalRaw.padEnd(12) +
+ totalGzip
+ )
+ }
+}
+
+/**
+ * Compare current results with baseline
+ */
+function compareResults(current, baseline) {
+ const baselineMap = new Map(baseline.map(r => [r.package, r]))
+ const comparison = []
+
+ // Get all unique packages
+ const allPackages = new Set([...current.map(r => r.package), ...baseline.map(r => r.package)])
+
+ for (const pkg of allPackages) {
+ const currentResult = current.find(r => r.package === pkg)
+ const baselineResult = baselineMap.get(pkg)
+
+ if (!currentResult && !baselineResult) continue
+
+ const currentTotal = currentResult?.total || { raw: 0, gzip: 0 }
+ const baselineTotal = baselineResult?.total || { raw: 0, gzip: 0 }
+
+ comparison.push({
+ package: pkg,
+ current: currentTotal,
+ baseline: baselineTotal,
+ diff: {
+ raw: currentTotal.raw - baselineTotal.raw,
+ gzip: currentTotal.gzip - baselineTotal.gzip,
+ },
+ })
+ }
+
+ // Sort by absolute difference (descending)
+ comparison.sort((a, b) => Math.abs(b.diff.raw) - Math.abs(a.diff.raw))
+
+ return comparison
+}
+
+/**
+ * Print comparison results
+ */
+function printComparison(comparison) {
+ console.log('\nš Bundle Size Comparison (Current vs Baseline)\n')
+ console.log(
+ 'Package'.padEnd(20) +
+ 'Baseline Raw'.padEnd(15) +
+ 'Current Raw'.padEnd(15) +
+ 'Diff Raw'.padEnd(20) +
+ 'Baseline Gzip'.padEnd(15) +
+ 'Current Gzip'.padEnd(15) +
+ 'Diff Gzip'
+ )
+ console.log('-'.repeat(120))
+
+ let hasChanges = false
+ for (const comp of comparison) {
+ if (comp.current.raw === 0 && comp.baseline.raw === 0) continue
+
+ const baselineRaw = formatBytes(comp.baseline.raw)
+ const currentRaw = formatBytes(comp.current.raw)
+ const diffRaw = formatDiff(comp.current.raw, comp.baseline.raw)
+ const baselineGzip = formatBytes(comp.baseline.gzip)
+ const currentGzip = formatBytes(comp.current.gzip)
+ const diffGzip = formatDiff(comp.current.gzip, comp.baseline.gzip)
+
+ // Highlight significant changes (>5% or >10KB)
+ const rawChangePercent = comp.baseline.raw > 0 ? (comp.diff.raw / comp.baseline.raw) * 100 : 0
+ const significantChange = Math.abs(rawChangePercent) > 5 || Math.abs(comp.diff.raw) > 10240
+
+ if (significantChange && comp.diff.raw !== 0) {
+ hasChanges = true
+ const indicator = comp.diff.raw > 0 ? 'ā ļø ' : 'ā
'
+ console.log(
+ indicator + comp.package.padEnd(18) +
+ baselineRaw.padEnd(15) +
+ currentRaw.padEnd(15) +
+ diffRaw.padEnd(20) +
+ baselineGzip.padEnd(15) +
+ currentGzip.padEnd(15) +
+ diffGzip
+ )
+ } else {
+ console.log(
+ ' ' + comp.package.padEnd(18) +
+ baselineRaw.padEnd(15) +
+ currentRaw.padEnd(15) +
+ diffRaw.padEnd(20) +
+ baselineGzip.padEnd(15) +
+ currentGzip.padEnd(15) +
+ diffGzip
+ )
+ }
+ }
+
+ if (hasChanges) {
+ console.log('\nā ļø Significant changes detected (>5% or >10KB)')
+ }
+}
+
+/**
+ * Save results to JSON file
+ */
+/**
+ * Generate markdown summary for PR comment
+ */
+function generateSummary(comparison, baselineRef = 'baseline') {
+ if (!comparison || comparison.length === 0) {
+ return '## š¦ Bundle Size Analysis\n\nNo bundle size data available.'
+ }
+
+ // Calculate totals
+ let totalBaselineRaw = 0
+ let totalCurrentRaw = 0
+ let totalBaselineGzip = 0
+ let totalCurrentGzip = 0
+
+ for (const comp of comparison) {
+ totalBaselineRaw += comp.baseline.raw
+ totalCurrentRaw += comp.current.raw
+ totalBaselineGzip += comp.baseline.gzip
+ totalCurrentGzip += comp.current.gzip
+ }
+
+ const totalDiffRaw = totalCurrentRaw - totalBaselineRaw
+ const totalDiffGzip = totalCurrentGzip - totalBaselineGzip
+ const totalDiffPercentRaw = totalBaselineRaw > 0 ? ((totalDiffRaw / totalBaselineRaw) * 100).toFixed(2) : '0.00'
+ const totalDiffPercentGzip = totalBaselineGzip > 0 ? ((totalDiffGzip / totalBaselineGzip) * 100).toFixed(2) : '0.00'
+
+ const signRaw = totalDiffRaw >= 0 ? '+' : ''
+ const signGzip = totalDiffGzip >= 0 ? '+' : ''
+
+ let summary = `## š¦ Bundle Size Analysis\n\n`
+ summary += `**Total Bundle Size:** ${formatBytes(totalCurrentRaw)} (${formatBytes(totalCurrentGzip)} gzipped)\n`
+ summary += `**Change:** ${signRaw}${formatBytes(totalDiffRaw)} (${signRaw}${totalDiffPercentRaw}%) / ${signGzip}${formatBytes(totalDiffGzip)} (${signGzip}${totalDiffPercentGzip}%) gzipped\n\n`
+ summary += `Compared to ${baselineRef}\n\n`
+
+ // Table header
+ summary += `| Package | Baseline | Current | Change |\n`
+ summary += `|---------|----------|---------|--------|\n`
+
+ // Add rows for packages with changes
+ let hasSignificantChanges = false
+ for (const comp of comparison) {
+ if (comp.current.raw === 0 && comp.baseline.raw === 0) continue
+
+ const rawChangePercent = comp.baseline.raw > 0 ? (comp.diff.raw / comp.baseline.raw) * 100 : 0
+ const significantChange = Math.abs(rawChangePercent) > 5 || Math.abs(comp.diff.raw) > 10240
+
+ if (Math.abs(comp.diff.raw) > 0 || Math.abs(comp.diff.gzip) > 0) {
+ const baselineGzip = formatBytes(comp.baseline.gzip)
+ const currentGzip = formatBytes(comp.current.gzip)
+ const diffGzip = formatDiff(comp.current.gzip, comp.baseline.gzip)
+
+ const indicator = significantChange ? (comp.diff.raw > 0 ? 'ā ļø ' : 'ā
') : ''
+ const changeText = `${formatDiff(comp.current.raw, comp.baseline.raw)} / ${diffGzip}`
+
+ summary += `| ${indicator}**${comp.package}** | ${baselineGzip} | ${currentGzip} | ${changeText} |\n`
+
+ if (significantChange) {
+ hasSignificantChanges = true
+ }
+ }
+ }
+
+ if (hasSignificantChanges) {
+ summary += `\nā ļø **Significant changes detected** (>5% or >10KB)\n`
+ }
+
+ return summary
+}
+
+async function saveResults(results, filePath) {
+ await writeFile(filePath, JSON.stringify(results, null, 2))
+}
+
+/**
+ * Load results from JSON file
+ */
+async function loadResults(filePath) {
+ try {
+ const data = await readFile(filePath, 'utf-8')
+ return JSON.parse(data)
+ } catch {
+ return null
+ }
+}
+
+/**
+ * Main function
+ */
+async function main() {
+ const args = process.argv.slice(2)
+ const compareMode = args.includes('--compare') || args.includes('-c')
+ const baselineFile = args.find(arg => arg.startsWith('--baseline='))?.split('=')[1] ||
+ args.find(arg => arg.startsWith('-b='))?.split('=')[1]
+
+ if (compareMode && baselineFile) {
+ // Comparison mode
+ console.log('š¦ Analyzing current bundle sizes...')
+ const currentResults = await analyzeBundleSizes()
+
+ console.log('š¦ Loading baseline results...')
+ const baselineResults = await loadResults(baselineFile)
+
+ if (!baselineResults) {
+ console.error(`ā Baseline file not found: ${baselineFile}`)
+ process.exit(1)
+ }
+
+ printResults(baselineResults, 'š Baseline Bundle Sizes')
+ printResults(currentResults, 'š Current Bundle Sizes')
+
+ const comparison = compareResults(currentResults, baselineResults)
+ printComparison(comparison)
+
+ // Generate summary if requested
+ if (args.includes('--summary') || args.includes('--summary-file')) {
+ const summaryFile = args.find(arg => arg.startsWith('--summary-file='))?.split('=')[1]
+ const baselineRef = args.find(arg => arg.startsWith('--baseline-ref='))?.split('=')[1] || 'baseline'
+ const summary = generateSummary(comparison, baselineRef)
+
+ if (summaryFile) {
+ await writeFile(summaryFile, summary)
+ console.log(`
+š Summary saved to ${summaryFile}`)
+ } else {
+ // Output to stdout for CI to capture
+ console.log('
+---SUMMARY_START---')
+ console.log(summary)
+ console.log('---SUMMARY_END---')
+ }
+ }
+
+ console.log('
+ā
Bundle size comparison complete')
+ } else if (args.includes('--save') || args.includes('-s')) {
+ // Save mode - save current results to file
+ const outputFile = args.find(arg => arg.startsWith('--output='))?.split('=')[1] ||
+ args.find(arg => arg.startsWith('-o='))?.split('=')[1] ||
+ 'bundle-size-baseline.json'
+
+ console.log('š¦ Analyzing bundle sizes...')
+ const results = await analyzeBundleSizes()
+
+ await saveResults(results, outputFile)
+ console.log(`\nā
Results saved to ${outputFile}`)
+ printResults(results)
+ } else {
+ // Normal mode - just analyze and print
+ console.log('š¦ Analyzing bundle sizes...\n')
+ const results = await analyzeBundleSizes()
+ printResults(results)
+ console.log('\nā
Bundle size analysis complete')
+ console.log('\nš” Tip: Use --save to save baseline, --compare --baseline= to compare')
+ }
+}
+
+// Run if called directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main().catch((error) => {
+ console.error('ā Error:', error)
+ process.exit(1)
+ })
+}
+
+export { analyzeBundleSizes, compareResults, saveResults, loadResults }
diff --git a/scripts/analyze-bundle-size.js.bak b/scripts/analyze-bundle-size.js.bak
new file mode 100755
index 0000000000..3bf4df3b94
--- /dev/null
+++ b/scripts/analyze-bundle-size.js.bak
@@ -0,0 +1,353 @@
+#!/usr/bin/env node
+/**
+ * Bundle size analysis script for EthereumJS monorepo
+ * Analyzes the dist folder sizes for all packages and reports bundle sizes
+ * Supports comparison mode to compare against a baseline
+ */
+
+import { readdir, stat, writeFile, readFile, mkdir } from 'fs/promises'
+import { createGzip } from 'zlib'
+import { createReadStream } from 'fs'
+import { join } from 'path'
+import { fileURLToPath } from 'url'
+import { dirname } from 'path'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+const rootDir = join(__dirname, '..')
+const packagesDir = join(rootDir, 'packages')
+
+/**
+ * Get gzipped size of a file
+ */
+async function getGzipSize(filePath) {
+ return new Promise((resolve, reject) => {
+ const chunks = []
+ const gzip = createGzip()
+ const readStream = createReadStream(filePath)
+
+ readStream
+ .pipe(gzip)
+ .on('data', (chunk) => chunks.push(chunk))
+ .on('end', () => {
+ const size = chunks.reduce((acc, chunk) => acc + chunk.length, 0)
+ resolve(size)
+ })
+ .on('error', reject)
+ })
+}
+
+/**
+ * Recursively get all files in a directory
+ */
+async function getAllFiles(dir, fileList = []) {
+ const files = await readdir(dir, { withFileTypes: true })
+
+ for (const file of files) {
+ const filePath = join(dir, file.name)
+
+ if (file.isDirectory()) {
+ await getAllFiles(filePath, fileList)
+ } else if (file.isFile() && (file.name.endsWith('.js') || file.name.endsWith('.mjs'))) {
+ fileList.push(filePath)
+ }
+ }
+
+ return fileList
+}
+
+/**
+ * Calculate total size of a directory
+ */
+async function calculateDirSize(dir) {
+ try {
+ const files = await getAllFiles(dir)
+ let totalSize = 0
+ let totalGzipSize = 0
+
+ for (const file of files) {
+ const stats = await stat(file)
+ totalSize += stats.size
+ totalGzipSize += await getGzipSize(file)
+ }
+
+ return { raw: totalSize, gzip: totalGzipSize, fileCount: files.length }
+ } catch (error) {
+ // Directory doesn't exist or is empty
+ return { raw: 0, gzip: 0, fileCount: 0 }
+ }
+}
+
+/**
+ * Format bytes to human readable format
+ */
+function formatBytes(bytes) {
+ if (bytes === 0) return '0 B'
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
+}
+
+/**
+ * Format difference with sign and color indicators
+ */
+function formatDiff(current, baseline) {
+ if (baseline === 0) return current > 0 ? '+NEW' : '0 B'
+ const diff = current - baseline
+ const percent = ((diff / baseline) * 100).toFixed(1)
+ const sign = diff >= 0 ? '+' : ''
+ return `${sign}${formatBytes(diff)} (${sign}${percent}%)`
+}
+
+/**
+ * Analyze bundle sizes for all packages
+ */
+async function analyzeBundleSizes(packagesDirPath = packagesDir) {
+ const packages = await readdir(packagesDirPath, { withFileTypes: true })
+ const results = []
+
+ for (const pkg of packages) {
+ if (!pkg.isDirectory()) continue
+
+ const packagePath = join(packagesDirPath, pkg.name)
+ const distPath = join(packagePath, 'dist')
+
+ // Check if dist folder exists
+ try {
+ await stat(distPath)
+ } catch {
+ // No dist folder, skip
+ continue
+ }
+
+ // Calculate sizes for both ESM and CJS builds
+ const esmPath = join(distPath, 'esm')
+ const cjsPath = join(distPath, 'cjs')
+
+ const esmSize = await calculateDirSize(esmPath)
+ const cjsSize = await calculateDirSize(cjsPath)
+
+ const totalRaw = esmSize.raw + cjsSize.raw
+ const totalGzip = esmSize.gzip + cjsSize.gzip
+
+ results.push({
+ package: pkg.name,
+ esm: esmSize,
+ cjs: cjsSize,
+ total: { raw: totalRaw, gzip: totalGzip },
+ })
+ }
+
+ // Sort by total size (descending)
+ results.sort((a, b) => b.total.raw - a.total.raw)
+
+ return results
+}
+
+/**
+ * Print analysis results
+ */
+function printResults(results, title = 'š Bundle Size Analysis Results') {
+ console.log(`\n${title}\n`)
+ console.log('Package'.padEnd(20) + 'ESM Raw'.padEnd(12) + 'ESM Gzip'.padEnd(12) + 'CJS Raw'.padEnd(12) + 'CJS Gzip'.padEnd(12) + 'Total Raw'.padEnd(12) + 'Total Gzip')
+ console.log('-'.repeat(100))
+
+ for (const result of results) {
+ if (result.total.raw === 0) continue
+
+ const esmRaw = formatBytes(result.esm.raw)
+ const esmGzip = formatBytes(result.esm.gzip)
+ const cjsRaw = formatBytes(result.cjs.raw)
+ const cjsGzip = formatBytes(result.cjs.gzip)
+ const totalRaw = formatBytes(result.total.raw)
+ const totalGzip = formatBytes(result.total.gzip)
+
+ console.log(
+ result.package.padEnd(20) +
+ esmRaw.padEnd(12) +
+ esmGzip.padEnd(12) +
+ cjsRaw.padEnd(12) +
+ cjsGzip.padEnd(12) +
+ totalRaw.padEnd(12) +
+ totalGzip
+ )
+ }
+}
+
+/**
+ * Compare current results with baseline
+ */
+function compareResults(current, baseline) {
+ const baselineMap = new Map(baseline.map(r => [r.package, r]))
+ const comparison = []
+
+ // Get all unique packages
+ const allPackages = new Set([...current.map(r => r.package), ...baseline.map(r => r.package)])
+
+ for (const pkg of allPackages) {
+ const currentResult = current.find(r => r.package === pkg)
+ const baselineResult = baselineMap.get(pkg)
+
+ if (!currentResult && !baselineResult) continue
+
+ const currentTotal = currentResult?.total || { raw: 0, gzip: 0 }
+ const baselineTotal = baselineResult?.total || { raw: 0, gzip: 0 }
+
+ comparison.push({
+ package: pkg,
+ current: currentTotal,
+ baseline: baselineTotal,
+ diff: {
+ raw: currentTotal.raw - baselineTotal.raw,
+ gzip: currentTotal.gzip - baselineTotal.gzip,
+ },
+ })
+ }
+
+ // Sort by absolute difference (descending)
+ comparison.sort((a, b) => Math.abs(b.diff.raw) - Math.abs(a.diff.raw))
+
+ return comparison
+}
+
+/**
+ * Print comparison results
+ */
+function printComparison(comparison) {
+ console.log('\nš Bundle Size Comparison (Current vs Baseline)\n')
+ console.log(
+ 'Package'.padEnd(20) +
+ 'Baseline Raw'.padEnd(15) +
+ 'Current Raw'.padEnd(15) +
+ 'Diff Raw'.padEnd(20) +
+ 'Baseline Gzip'.padEnd(15) +
+ 'Current Gzip'.padEnd(15) +
+ 'Diff Gzip'
+ )
+ console.log('-'.repeat(120))
+
+ let hasChanges = false
+ for (const comp of comparison) {
+ if (comp.current.raw === 0 && comp.baseline.raw === 0) continue
+
+ const baselineRaw = formatBytes(comp.baseline.raw)
+ const currentRaw = formatBytes(comp.current.raw)
+ const diffRaw = formatDiff(comp.current.raw, comp.baseline.raw)
+ const baselineGzip = formatBytes(comp.baseline.gzip)
+ const currentGzip = formatBytes(comp.current.gzip)
+ const diffGzip = formatDiff(comp.current.gzip, comp.baseline.gzip)
+
+ // Highlight significant changes (>5% or >10KB)
+ const rawChangePercent = comp.baseline.raw > 0 ? (comp.diff.raw / comp.baseline.raw) * 100 : 0
+ const significantChange = Math.abs(rawChangePercent) > 5 || Math.abs(comp.diff.raw) > 10240
+
+ if (significantChange && comp.diff.raw !== 0) {
+ hasChanges = true
+ const indicator = comp.diff.raw > 0 ? 'ā ļø ' : 'ā
'
+ console.log(
+ indicator + comp.package.padEnd(18) +
+ baselineRaw.padEnd(15) +
+ currentRaw.padEnd(15) +
+ diffRaw.padEnd(20) +
+ baselineGzip.padEnd(15) +
+ currentGzip.padEnd(15) +
+ diffGzip
+ )
+ } else {
+ console.log(
+ ' ' + comp.package.padEnd(18) +
+ baselineRaw.padEnd(15) +
+ currentRaw.padEnd(15) +
+ diffRaw.padEnd(20) +
+ baselineGzip.padEnd(15) +
+ currentGzip.padEnd(15) +
+ diffGzip
+ )
+ }
+ }
+
+ if (hasChanges) {
+ console.log('\nā ļø Significant changes detected (>5% or >10KB)')
+ }
+}
+
+/**
+ * Save results to JSON file
+ */
+async function saveResults(results, filePath) {
+ await writeFile(filePath, JSON.stringify(results, null, 2))
+}
+
+/**
+ * Load results from JSON file
+ */
+async function loadResults(filePath) {
+ try {
+ const data = await readFile(filePath, 'utf-8')
+ return JSON.parse(data)
+ } catch {
+ return null
+ }
+}
+
+/**
+ * Main function
+ */
+async function main() {
+ const args = process.argv.slice(2)
+ const compareMode = args.includes('--compare') || args.includes('-c')
+ const baselineFile = args.find(arg => arg.startsWith('--baseline='))?.split('=')[1] ||
+ args.find(arg => arg.startsWith('-b='))?.split('=')[1]
+
+ if (compareMode && baselineFile) {
+ // Comparison mode
+ console.log('š¦ Analyzing current bundle sizes...')
+ const currentResults = await analyzeBundleSizes()
+
+ console.log('š¦ Loading baseline results...')
+ const baselineResults = await loadResults(baselineFile)
+
+ if (!baselineResults) {
+ console.error(`ā Baseline file not found: ${baselineFile}`)
+ process.exit(1)
+ }
+
+ printResults(baselineResults, 'š Baseline Bundle Sizes')
+ printResults(currentResults, 'š Current Bundle Sizes')
+
+ const comparison = compareResults(currentResults, baselineResults)
+ printComparison(comparison)
+
+ console.log('\nā
Bundle size comparison complete')
+ } else if (args.includes('--save') || args.includes('-s')) {
+ // Save mode - save current results to file
+ const outputFile = args.find(arg => arg.startsWith('--output='))?.split('=')[1] ||
+ args.find(arg => arg.startsWith('-o='))?.split('=')[1] ||
+ 'bundle-size-baseline.json'
+
+ console.log('š¦ Analyzing bundle sizes...')
+ const results = await analyzeBundleSizes()
+
+ await saveResults(results, outputFile)
+ console.log(`\nā
Results saved to ${outputFile}`)
+ printResults(results)
+ } else {
+ // Normal mode - just analyze and print
+ console.log('š¦ Analyzing bundle sizes...\n')
+ const results = await analyzeBundleSizes()
+ printResults(results)
+ console.log('\nā
Bundle size analysis complete')
+ console.log('\nš” Tip: Use --save to save baseline, --compare --baseline= to compare')
+ }
+}
+
+// Run if called directly
+if (import.meta.url === `file://${process.argv[1]}`) {
+ main().catch((error) => {
+ console.error('ā Error:', error)
+ process.exit(1)
+ })
+}
+
+export { analyzeBundleSizes, compareResults, saveResults, loadResults }