Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/build/bundleScan.ts
T

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");
}