scripts/check_plugin_worker_size.ts
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bun
/**
* Guard Cloudflare plugin worker bundle size against the committed baseline.
*
* Builds with `wrangler deploy --dry-run --minify` (same path as production
* plugin deploys) and compares Total Upload / gzip against
* `scripts/bench/plugin_worker_size_baseline.json`.
*
* - Increase >5%: warning on stdout (still exits 0)
* - Increase >15%: fails (exit 1) so PRs cannot silently bloat the hot-path worker
*
* Usage:
* bun run check:plugin-worker-size
* bun run check:plugin-worker-size -- --update-baseline
*/
import { spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
const ROOT = resolve(import.meta.dirname, '..')
const BASELINE_PATH = join(ROOT, 'scripts/bench/plugin_worker_size_baseline.json')
const WARN_PCT = 5
const FAIL_PCT = 15
// Keep these well under the unit-test SCRIPT_TIMEOUT_MS (120s) so CI gets a clean size failure, not a killed process.
const WRANGLER_TIMEOUT_MS = 90_000
const GIT_TIMEOUT_MS = 10_000
interface SizeBaseline {
generatedAt: string
gitHead: string
note?: string
uploadBytes: number
gzipBytes: number
}
interface MeasuredSize {
uploadBytes: number
gzipBytes: number
uploadKiB: number
gzipKiB: number
}
function parseArgs(argv: string[]) {
return { updateBaseline: argv.includes('--update-baseline') }
}
function gitHead(): string {
const result = spawnSync('git', ['rev-parse', 'HEAD'], {
cwd: ROOT,
encoding: 'utf8',
timeout: GIT_TIMEOUT_MS,
})
return result.status === 0 ? result.stdout.trim() : 'unknown'
}
function measurePluginWorkerSize(): MeasuredSize {
const outDir = mkdtempSync(join(tmpdir(), 'capgo-plugin-size-'))
try {
const result = spawnSync('bunx', [
'wrangler',
'deploy',
'--config',
'cloudflare_workers/plugin/wrangler.jsonc',
'--env=local',
'--dry-run',
'--minify',
`--outdir=${outDir}`,
], {
cwd: ROOT,
encoding: 'utf8',
maxBuffer: 32 * 1024 * 1024,
timeout: WRANGLER_TIMEOUT_MS,
})
if (result.signal)
throw new Error(`wrangler dry-run killed by signal ${result.signal} (timeout ${WRANGLER_TIMEOUT_MS}ms?)`)
if (result.error)
throw new Error(`wrangler dry-run failed to start: ${result.error.message}`)
if (result.status !== 0) {
throw new Error(`wrangler dry-run failed:\n${result.stdout}\n${result.stderr}`)
}
const uploadMatch = result.stdout.match(/Total Upload:\s*([\d.]+)\s*KiB\s*\/\s*gzip:\s*([\d.]+)\s*KiB/)
if (!uploadMatch) {
throw new Error(`Could not parse wrangler upload size from output:\n${result.stdout}`)
}
const uploadKiB = Number.parseFloat(uploadMatch[1])
const gzipKiB = Number.parseFloat(uploadMatch[2])
return {
uploadKiB,
gzipKiB,
uploadBytes: Math.round(uploadKiB * 1024),
gzipBytes: Math.round(gzipKiB * 1024),
}
}
finally {
rmSync(outDir, { recursive: true, force: true })
}
}
function loadBaseline(): SizeBaseline {
return JSON.parse(readFileSync(BASELINE_PATH, 'utf8')) as SizeBaseline
}
function pctChange(before: number, after: number): number {
if (before <= 0)
return after === 0 ? 0 : Infinity
return ((after - before) / before) * 100
}
function formatPct(value: number): string {
if (!Number.isFinite(value))
return 'n/a'
return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`
}
function kib(bytes: number): string {
return `${(bytes / 1024).toFixed(2)} KiB`
}
function writeBaseline(measured: MeasuredSize) {
mkdirSync(resolve(BASELINE_PATH, '..'), { recursive: true })
const baseline: SizeBaseline = {
generatedAt: new Date().toISOString(),
gitHead: gitHead(),
note: 'Committed CF plugin worker size budget (wrangler dry-run --minify, env=local). Update with: bun run check:plugin-worker-size -- --update-baseline',
uploadBytes: measured.uploadBytes,
gzipBytes: measured.gzipBytes,
}
writeFileSync(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`)
console.log(`Updated baseline at ${BASELINE_PATH}`)
}
function main() {
const args = parseArgs(process.argv.slice(2))
console.log('Building Cloudflare plugin worker (dry-run --minify)...')
const measured = measurePluginWorkerSize()
if (args.updateBaseline) {
writeBaseline(measured)
console.log(`upload ${kib(measured.uploadBytes)} / gzip ${kib(measured.gzipBytes)}`)
return
}
const baseline = loadBaseline()
const uploadDelta = pctChange(baseline.uploadBytes, measured.uploadBytes)
const gzipDelta = pctChange(baseline.gzipBytes, measured.gzipBytes)
const maxDelta = Math.max(uploadDelta, gzipDelta)
console.log('Plugin worker size vs baseline:')
console.log(` upload: ${kib(baseline.uploadBytes)} → ${kib(measured.uploadBytes)} (${formatPct(uploadDelta)})`)
console.log(` gzip: ${kib(baseline.gzipBytes)} → ${kib(measured.gzipBytes)} (${formatPct(gzipDelta)})`)
console.log(` baseline: ${BASELINE_PATH} (git ${baseline.gitHead})`)
console.log(` thresholds: warn >${WARN_PCT}% | fail >${FAIL_PCT}%`)
if (maxDelta > FAIL_PCT) {
console.error(`\nFAIL: plugin worker size grew ${formatPct(maxDelta)} (limit +${FAIL_PCT}%).`)
console.error('Keep the plugin isolate light. Revert heavy imports or raise the budget intentionally with:')
console.error(' bun run check:plugin-worker-size -- --update-baseline')
console.error('and justify the baseline bump in the PR.')
process.exit(1)
}
if (maxDelta > WARN_PCT) {
console.warn(`\nWARNING: plugin worker size grew ${formatPct(maxDelta)} (warn >${WARN_PCT}%).`)
console.warn('Prefer keeping growth near zero on the hot plugin worker path.')
}
else if (maxDelta > 0) {
console.log(`\nOK: size increase ${formatPct(maxDelta)} is under the ${WARN_PCT}% warning threshold.`)
}
else if (maxDelta < 0) {
console.log(`\nOK: plugin worker got smaller (${formatPct(maxDelta)}). Consider --update-baseline.`)
}
else {
console.log('\nOK: plugin worker size unchanged vs baseline.')
}
}
main()