From 4b905465197db40531e8c29f8864c5085d05ec63 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Sun, 21 Dec 2025 11:16:35 -0800 Subject: [PATCH] Add a tabular bundlewatch script (#41957) * Script for better bundlewatch locally * Fix linter --- .bundlewatch.config.json | 22 ++++----- build/bundlewatch-table.mjs | 93 +++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 build/bundlewatch-table.mjs diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 9e8e216ace..52a4dcfb6d 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -2,43 +2,43 @@ "files": [ { "path": "./dist/css/bootstrap-grid.css", - "maxSize": "10.0 kB" + "maxSize": "9.5 kB" }, { "path": "./dist/css/bootstrap-grid.min.css", - "maxSize": "9.0 kB" + "maxSize": "8.5 kB" }, { "path": "./dist/css/bootstrap-reboot.css", - "maxSize": "5.5 kB" + "maxSize": "5.25 kB" }, { "path": "./dist/css/bootstrap-reboot.min.css", - "maxSize": "4.5 kB" + "maxSize": "4.25 kB" }, { "path": "./dist/css/bootstrap-utilities.css", - "maxSize": "15.25 kB" + "maxSize": "14.5 kB" }, { "path": "./dist/css/bootstrap-utilities.min.css", - "maxSize": "13.5 kB" + "maxSize": "12.75 kB" }, { "path": "./dist/css/bootstrap.css", - "maxSize": "37.75 kB" + "maxSize": "35.5 kB" }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "33.75 kB" + "maxSize": "32.0 kB" }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "44.0 kB" + "maxSize": "43.5 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", - "maxSize": "23.5 kB" + "maxSize": "22.5 kB" }, { "path": "./dist/js/bootstrap.esm.js", @@ -50,7 +50,7 @@ }, { "path": "./dist/js/bootstrap.js", - "maxSize": "30.5 kB" + "maxSize": "30.25 kB" }, { "path": "./dist/js/bootstrap.min.js", diff --git a/build/bundlewatch-table.mjs b/build/bundlewatch-table.mjs new file mode 100644 index 0000000000..e4759881ba --- /dev/null +++ b/build/bundlewatch-table.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import { execSync } from 'node:child_process' + +// Run bundlewatch and capture output +let stdout +let exitCode = 0 + +try { + stdout = execSync('npx bundlewatch --config .bundlewatch.config.json', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'] + }) +} catch (error) { + stdout = error.stdout || '' + exitCode = error.status || 1 +} + +// Parse lines that contain PASS or FAIL +const lines = stdout.split('\n').filter(l => l.startsWith('PASS') || l.startsWith('FAIL')) + +if (lines.length === 0) { + console.log(stdout) + process.exit(exitCode) +} + +// Parse size string to number (KB) +const parseSize = str => Number.parseFloat(str.replace('KB', '')) + +// Calculate column widths and headroom +const rows = lines.map(line => { + const match = line.match(/(PASS|FAIL)\s+(.+?):\s+([\d.]+KB)\s+([<>])\s+([\d.]+KB)/) + if (match) { + const sizeNum = parseSize(match[3]) + const maxNum = parseSize(match[5]) + const headroomNum = maxNum - sizeNum + const headroom = `${headroomNum.toFixed(2)}KB` + return { + status: match[1], + file: match[2], + size: match[3], + max: match[5], + headroomNum, + headroom: match[1] === 'PASS' ? `+${headroom}` : `-${Math.abs(headroomNum).toFixed(2)}KB` + } + } + + return null +}).filter(Boolean) + +const maxFileLen = Math.max(...rows.map(r => r.file.length), 4) +const maxSizeLen = Math.max(...rows.map(r => r.size.length), 4) +const maxMaxLen = Math.max(...rows.map(r => r.max.length), 3) +const maxHeadroomLen = Math.max(...rows.map(r => r.headroom.length), 8) + +// Build table +const hr = `+-${'-'.repeat(maxFileLen)}-+-${'-'.repeat(maxSizeLen)}-+-${'-'.repeat(maxMaxLen)}-+-${'-'.repeat(maxHeadroomLen)}-+` + +console.log('') +console.log('bundlewatch results') +console.log(hr) +console.log(`| ${'File'.padEnd(maxFileLen)} | ${'Size'.padStart(maxSizeLen)} | ${'Max'.padStart(maxMaxLen)} | ${'Headroom'.padStart(maxHeadroomLen)} |`) +console.log(hr) + +const green = '\u001B[32m' +const red = '\u001B[31m' +const reset = '\u001B[0m' + +for (const row of rows) { + const sizeColor = row.status === 'PASS' ? green : red + const coloredSize = `${sizeColor}${row.size.padStart(maxSizeLen)}${reset}` + const headroomColor = row.headroomNum > 0.25 ? red : '' + const headroomReset = row.headroomNum > 0.25 ? reset : '' + const coloredHeadroom = `${headroomColor}${row.headroom.padStart(maxHeadroomLen)}${headroomReset}` + console.log(`| ${row.file.padEnd(maxFileLen)} | ${coloredSize} | ${row.max.padStart(maxMaxLen)} | ${coloredHeadroom} |`) +} + +console.log(hr) + +// Summary +const passed = rows.filter(r => r.status === 'PASS').length +const failed = rows.filter(r => r.status === 'FAIL').length + +console.log('') +if (failed > 0) { + console.log(`\u001B[31mbundlewatch FAIL\u001B[0m - ${passed} passed, ${failed} failed`) +} else { + console.log(`\u001B[32mbundlewatch PASS\u001B[0m - ${passed}/${rows.length} files within limits`) +} + +console.log('') + +process.exit(exitCode) diff --git a/package.json b/package.json index 28912970c6..fe99cc2374 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "scripts": { "start": "npm-run-all --parallel watch docs-serve", "bundlewatch": "bundlewatch --config .bundlewatch.config.json", + "bundlewatch:table": "node build/bundlewatch-table.mjs", "css": "npm-run-all css-compile css-prefix css-minify css-docs", "css-compile": "sass --style expanded --source-map --embed-sources --no-error-css scss/bootstrap.scss:dist/css/bootstrap.css scss/bootstrap-grid.scss:dist/css/bootstrap-grid.css scss/bootstrap-reboot.scss:dist/css/bootstrap-reboot.css scss/bootstrap-utilities.scss:dist/css/bootstrap-utilities.css", "css-docs": "node build/generate-utilities-json.mjs",