121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
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<Record<BundleBudgetKind, number>>;
|
|
largestChunkLimit?: number;
|
|
};
|
|
|
|
export type BundleScanResult = {
|
|
assets: BundleAsset[];
|
|
largestChunks: BundleAsset[];
|
|
budgetViolations: BundleBudgetViolation[];
|
|
warnings: ReturnType<typeof classifyBuildWarning>[];
|
|
unknownWarnings: ReturnType<typeof classifyBuildWarning>[];
|
|
};
|
|
|
|
export const DEFAULT_BUNDLE_BUDGETS: Record<BundleBudgetKind, number> = {
|
|
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");
|
|
}
|