import { type BuildWarningInput, classifyBuildWarning } from "./buildWarningTriage"; export type BundleAsset = { name: string; sizeKb: number; }; export type BundleBudgetKind = "editor" | "vendor" | "app"; export type BundleBudget = { kind: BundleBudgetKind; thresholdKb: number; }; export type BundleBudgetViolation = BundleAsset & { thresholdKb: number; budgetKind: BundleBudgetKind; }; export type BundleScanOptions = { budgets?: Partial>; largestChunkLimit?: number; }; export type BundleScanResult = { assets: BundleAsset[]; largestChunks: BundleAsset[]; budgetViolations: BundleBudgetViolation[]; warnings: ReturnType[]; unknownWarnings: ReturnType[]; }; export const DEFAULT_BUNDLE_BUDGETS: Record = { app: 350, vendor: 500, editor: 850, }; const EDITOR_ASSET_PATTERN = /(?:monaco|editor|codemirror)/i; const VENDOR_ASSET_PATTERN = /(?:vendor|node_modules|react|radix|tiptap|recharts|xyflow)/i; function getBudgetKind(assetName: string): BundleBudgetKind { if (EDITOR_ASSET_PATTERN.test(assetName)) { return "editor"; } if (VENDOR_ASSET_PATTERN.test(assetName)) { return "vendor"; } return "app"; } function sortLargestFirst(left: BundleAsset, right: BundleAsset) { if (right.sizeKb !== left.sizeKb) { return right.sizeKb - left.sizeKb; } return left.name.localeCompare(right.name); } export function analyzeBundleScan( input: { assets: BundleAsset[]; warnings?: BuildWarningInput[] }, options: BundleScanOptions = {}, ): BundleScanResult { const budgets = { ...DEFAULT_BUNDLE_BUDGETS, ...options.budgets }; const assets = [...input.assets].sort((left, right) => left.name.localeCompare(right.name)); const warnings = (input.warnings ?? []).map((warning) => classifyBuildWarning(warning)); const largestChunkLimit = options.largestChunkLimit ?? 5; const budgetViolations = assets .map((asset) => { const budgetKind = getBudgetKind(asset.name); const thresholdKb = budgets[budgetKind]; return { ...asset, thresholdKb, budgetKind }; }) .filter((asset) => asset.sizeKb > asset.thresholdKb) .sort((left, right) => { if (right.sizeKb !== left.sizeKb) { return right.sizeKb - left.sizeKb; } return left.name.localeCompare(right.name); }); return { assets, largestChunks: [...assets].sort(sortLargestFirst).slice(0, largestChunkLimit), budgetViolations, warnings, unknownWarnings: warnings.filter((warning) => warning.class === "unknown"), }; } export function formatBundleScanSummary(result: BundleScanResult): string { const lines = [ `Bundle scan: ${result.assets.length} assets, ${result.budgetViolations.length} budget violations, ${result.unknownWarnings.length} unknown warnings.`, "Largest chunks:", ...result.largestChunks.map((asset) => `- ${asset.name}: ${asset.sizeKb} kB`), ]; if (result.budgetViolations.length > 0) { lines.push( "Budget violations:", ...result.budgetViolations.map( (asset) => `- ${asset.name}: ${asset.sizeKb} kB exceeds ${asset.thresholdKb} kB ${asset.budgetKind} budget`, ), ); } if (result.unknownWarnings.length > 0) { lines.push( "Unknown warnings:", ...result.unknownWarnings.map((warning) => `- ${warning.code ?? "NO_CODE"}: ${warning.message}`), ); } return lines.join("\n"); }