import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { gzipSync } from 'node:zlib'; import { basename, join, resolve } from 'node:path'; const root = resolve(process.cwd()); const dist = resolve(root, 'dist'); const assets = join(dist, 'assets'); const budgets = { initialJsGzip: 350 * 1024, initialCssGzip: 30 * 1024, fontsBytes: 2 * 1024 * 1024, moduleMediaBytes: 1 * 1024 * 1024, }; function readAsset(relativePath) { return readFileSync(join(dist, relativePath)); } function gzipBytes(bytes) { return gzipSync(bytes, { level: 9 }).byteLength; } function formatBytes(bytes) { return `${(bytes / 1024).toFixed(1)} KiB`; } if (!existsSync(join(dist, 'index.html')) || !existsSync(assets)) { console.error('性能预算检查需要先运行 pnpm run build:vite。'); process.exit(1); } const html = readFileSync(join(dist, 'index.html'), 'utf8'); const initialJs = [...html.matchAll(/]+src="\.\/([^\"]+\.js)"/g)] .map((match) => match[1]) .reduce((total, relativePath) => total + gzipBytes(readAsset(relativePath)), 0); const initialCss = [...html.matchAll(/]+href="\.\/([^\"]+\.css)"/g)] .map((match) => match[1]) .reduce((total, relativePath) => total + gzipBytes(readAsset(relativePath)), 0); const assetNames = readdirSync(assets); const fontBytes = assetNames .filter((name) => /\.(woff2?|otf|ttf)$/i.test(name)) .reduce((total, name) => total + statSync(join(assets, name)).size, 0); const moduleMediaBytes = assetNames .filter((name) => /^module-.*\.(avif|webp|png|jpe?g)$/i.test(name)) .reduce((total, name) => total + statSync(join(assets, name)).size, 0); const metrics = { initialJsGzip: initialJs, initialCssGzip: initialCss, fontsBytes: fontBytes, moduleMediaBytes }; console.log(JSON.stringify({ metrics, budgets }, null, 2)); const failures = Object.entries(budgets) .filter(([key, budget]) => metrics[key] > budget) .map(([key, budget]) => `${key} ${formatBytes(metrics[key])} > ${formatBytes(budget)}`); if (failures.length > 0) { console.error(`性能预算超标:${failures.join('; ')}`); process.exit(1); }