#!/usr/bin/env node // Reports and enforces compressed Control UI asset budgets after a production build. import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { CONTROL_UI_ROUTE_PRELOAD_ATTRIBUTE, selectControlUiRoutePreloads, } from "../src/gateway/control-ui-route-preloads.ts"; import { reportLimitViolations } from "./lib/check-limits.mts"; import { CONTROL_UI_LOCALE_ENTRIES } from "./lib/control-ui-i18n-config.ts"; function isMetricsRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } const KIB = 1024; const STARTUP_JS_BASELINE_RATCHET_BYTES = 4096; const BASELINE_UPDATE_COMMAND = 'node --import ./scripts/tsx.mjs scripts/check-control-ui-performance.mts --update-baseline --reason ""'; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_STARTUP_BUDGET_BASELINE_PATH = path.resolve( SCRIPT_DIR, "../config/control-ui-startup-budget-baseline.json", ); // Each landed change can consume this much ratchet tolerance, so small increases // may accumulate. The fixed startup JS ceiling bounds that cumulative creep. const CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES = 512; const CONTROL_UI_STARTUP_JS_GZIP_BUILD_VARIANCE_BYTES = 64; const CONTROL_UI_STARTUP_CSS_GZIP_TARGET_BYTES = 45 * KIB; // Immediate Home and diagnostic frames approved in #147574, including shared header styles. const CONTROL_UI_CSS_GZIP_GROWTH_BYTES = 1.5 * KIB; // The opaque Mermaid sandbox loads one self-contained classic script only when // a diagram is viewed. Keep its size visible without relaxing ordinary chunks. const MERMAID_RENDERER_ASSET = /^assets\/mermaid\.min-[\w-]+\.js$/u; const MERMAID_RENDERER_GZIP_BYTES = 960 * KIB; // Locale catalogs are named Vite chunks loaded only after a language selection. // Base and config-hint chunks share one budget because they activate atomically. const CONTROL_UI_LOCALE_ASSET_PATTERNS = CONTROL_UI_LOCALE_ENTRIES.flatMap(({ locale }) => [ { locale, kind: "base" as const, pattern: new RegExp(`^assets/${escapeRegExp(locale)}-[^/]+\\.js$`, "u"), }, { locale, kind: "configHints" as const, pattern: new RegExp(`^assets/locale-config-hints-${escapeRegExp(locale)}-[^/]+\\.js$`, "u"), }, ]); const CONTROL_UI_LOCALE_GZIP_BYTES = 300 * KIB; // Small, explicit headroom over the optimized baseline. Budget changes should // accompany an intentional loading or chunking decision. const controlUiPerformanceBudgets = { startupJsRequests: 18, startupCssRequests: 1, // Approved measured upload-control baseline; retain the fixed growth and variance allowances. startupJsGzipBytes: 371_771, // Keep 45 KiB advisory: tiny integrated changes must not exhaust the budget. // The fixed 50 KiB ceiling bounds accumulation of small changes. startupCssGzipBytes: 50 * KIB, largestJsGzipBytes: 215 * KIB, // Composer multiline surface (stack #124301) legitimately grew boot CSS; // operator decision 2026-08-25 rejected boot splitting due to precedence risk. // 53.0 KiB was exhausted by organic growth (main sat at 99.94% by 2026-08-29); // bumped to 53.5 KiB with operator approval on PR #132054. 2026-09-02: side // panel, workboard chip, and Lobsterdex styles moved to their lazy owners, // measured boot sheet 52,337 B; ceiling lowered to keep ~1 KiB headroom. largestCssGzipBytes: 53_400, } satisfies Record; export const CONTROL_UI_PERFORMANCE_BUDGETS = Object.freeze(controlUiPerformanceBudgets); function controlUiAssetPathFromUrl(value: string): string | null { const normalized = value.split(/[?#]/u, 1)[0]?.replace(/\\/gu, "/") ?? ""; const markerIndex = normalized.lastIndexOf("assets/"); if (markerIndex === -1) { return null; } const assetPath = normalized.slice(markerIndex); if (assetPath.includes("../") || !/\.(?:css|js)$/u.test(assetPath)) { return null; } return assetPath; } export function extractControlUiStartupAssetPaths(html: string): string[] { const assets = new Set(); for (const tag of html.matchAll(/<(?:link|script)\b[^>]*>/giu)) { const attribute = tag[0].match(/\s(?:href|src)\s*=\s*["']([^"']+)["']/iu); const assetPath = attribute?.[1] ? controlUiAssetPathFromUrl(attribute[1]) : null; if (assetPath) { assets.add(assetPath); } } return [...assets].toSorted((left, right) => left.localeCompare(right)); } function readAssetMetrics(assetsDir: string, entry: fs.Dirent) { const file = `assets/${entry.name}`; const sourcePath = path.join(assetsDir, entry.name); const gzipPath = `${sourcePath}.gz`; const brotliPath = `${sourcePath}.br`; for (const sidecarPath of [gzipPath, brotliPath]) { if (!fs.existsSync(sidecarPath)) { throw new Error(`Control UI performance check missing ${path.basename(sidecarPath)}`); } } const type = entry.name.endsWith(".js") ? "js" : "css"; return { file, type, rawBytes: fs.statSync(sourcePath).size, gzipBytes: fs.statSync(gzipPath).size, brotliBytes: fs.statSync(brotliPath).size, }; } function summarizeAssets(assets: Array>) { return assets.reduce( (summary, asset) => ({ requests: summary.requests + 1, rawBytes: summary.rawBytes + asset.rawBytes, gzipBytes: summary.gzipBytes + asset.gzipBytes, brotliBytes: summary.brotliBytes + asset.brotliBytes, }), { requests: 0, rawBytes: 0, gzipBytes: 0, brotliBytes: 0 }, ); } function largestAsset(assets: Array>) { return assets.toSorted( (left, right) => right.gzipBytes - left.gzipBytes || left.file.localeCompare(right.file), )[0]!; } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } function controlUiLocaleAssetIdentity( file: string, ): { locale: string; kind: "base" | "configHints" } | null { const match = CONTROL_UI_LOCALE_ASSET_PATTERNS.find(({ pattern }) => pattern.test(file)); return match ? { locale: match.locale, kind: match.kind } : null; } function collectControlUiLocaleAssetGroups(assets: Array>) { const groups = new Map< string, { locale: string; base: Array>; configHints: Array>; } >(); for (const asset of assets) { const identity = controlUiLocaleAssetIdentity(asset.file); if (!identity) { continue; } const group = groups.get(identity.locale) ?? { locale: identity.locale, base: [], configHints: [], }; group[identity.kind].push(asset); groups.set(identity.locale, group); } return [...groups.values()]; } export function collectControlUiPerformanceMetrics(distDir: string) { const assetsDir = path.join(distDir, "assets"); const html = fs.readFileSync(path.join(distDir, "index.html"), "utf8"); const assets = fs .readdirSync(assetsDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && /\.(?:css|js)$/u.test(entry.name)) .map((entry) => readAssetMetrics(assetsDir, entry)); const assetsByFile = new Map(assets.map((asset) => [asset.file, asset])); const collectBootAssets = (document: string) => { const bootAssets = extractControlUiStartupAssetPaths(document).map((file) => { const asset = assetsByFile.get(file); if (!asset) { throw new Error(`Control UI performance check cannot find startup asset ${file}`); } return asset; }); return { js: summarizeAssets(bootAssets.filter((asset) => asset.type === "js")), css: summarizeAssets(bootAssets.filter((asset) => asset.type === "css")), assets: bootAssets, }; }; // Preserve the historical entry budget; route preloads also account for the // measured immediate dynamic imports that it never included. const startup = collectBootAssets(selectControlUiRoutePreloads(html, null)); const routeBoot = html.includes(CONTROL_UI_ROUTE_PRELOAD_ATTRIBUTE) ? { chat: collectBootAssets(selectControlUiRoutePreloads(html, "chat")), new: collectBootAssets(selectControlUiRoutePreloads(html, "new")), } : null; const jsAssets = assets.filter((asset) => asset.type === "js"); const mermaidRenderer = jsAssets.filter((asset) => MERMAID_RENDERER_ASSET.test(asset.file)); const localeCatalogs = jsAssets.filter( (asset) => controlUiLocaleAssetIdentity(asset.file) !== null, ); const ordinaryJsAssets = jsAssets.filter( (asset) => !MERMAID_RENDERER_ASSET.test(asset.file) && controlUiLocaleAssetIdentity(asset.file) === null, ); const cssAssets = assets.filter((asset) => asset.type === "css"); if (ordinaryJsAssets.length === 0 || cssAssets.length === 0 || startup.assets.length === 0) { throw new Error("Control UI performance check found an incomplete production bundle"); } return { schemaVersion: 1 as const, startup, routeBoot, total: { js: summarizeAssets(jsAssets), css: summarizeAssets(cssAssets), }, largest: { js: largestAsset(ordinaryJsAssets), css: largestAsset(cssAssets), }, mermaidRenderer, localeCatalogs, }; } export function evaluateControlUiPerformanceBudgets( metrics: ReturnType, budgets: Readonly = CONTROL_UI_PERFORMANCE_BUDGETS, startupBudgetBaseline: Readonly | null = null, startupJsTolerance = CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES, baseMetrics: ReturnType | null = null, ) { const baselineBytes = startupBudgetBaseline?.startupJsGzipBytes; const startupJsLimits = resolveControlUiStartupJsGzipLimits( budgets, startupBudgetBaseline, startupJsTolerance, ); const localeGroups = collectControlUiLocaleAssetGroups(metrics.localeCatalogs); const startupAssetFiles = new Set( [ ...metrics.startup.assets, ...Object.values(metrics.routeBoot ?? {}).flatMap((route) => route.assets), ].map((asset) => asset.file), ); const checks: Array<[string, number, number, "count" | "bytes"]> = [ ["startup JS requests", metrics.startup.js.requests, budgets.startupJsRequests, "count"], ["startup CSS requests", metrics.startup.css.requests, budgets.startupCssRequests, "count"], ["startup JS gzip", metrics.startup.js.gzipBytes, startupJsLimits.enforcementLimit, "bytes"], ["startup CSS gzip", metrics.startup.css.gzipBytes, budgets.startupCssGzipBytes, "bytes"], ["largest JS gzip", metrics.largest.js.gzipBytes, budgets.largestJsGzipBytes, "bytes"], ["largest CSS gzip", metrics.largest.css.gzipBytes, budgets.largestCssGzipBytes, "bytes"], ["isolated Mermaid JS assets", metrics.mermaidRenderer.length, 1, "count"], [ "isolated Mermaid JS gzip", summarizeAssets(metrics.mermaidRenderer).gzipBytes, MERMAID_RENDERER_GZIP_BYTES, "bytes", ], [ "startup Mermaid JS assets", [...startupAssetFiles].filter((file) => MERMAID_RENDERER_ASSET.test(file)).length, 0, "count", ], [ "locale catalog JS assets", metrics.localeCatalogs.length, CONTROL_UI_LOCALE_ENTRIES.length * 2, "count", ], [ "locale catalog base JS assets per locale", Math.max(0, ...localeGroups.map((group) => group.base.length)), 1, "count", ], [ "locale config-hint JS assets per locale", Math.max(0, ...localeGroups.map((group) => group.configHints.length)), 1, "count", ], [ "largest locale catalog pair JS gzip", Math.max( 0, ...localeGroups.map( (group) => summarizeAssets([...group.base, ...group.configHints]).gzipBytes, ), ), CONTROL_UI_LOCALE_GZIP_BYTES, "bytes", ], [ "startup locale catalog JS assets", [...startupAssetFiles].filter((file) => controlUiLocaleAssetIdentity(file) !== null).length, 0, "count", ], ]; const violations = checks.flatMap(([metric, actual, limit, unit]) => actual > limit ? [{ metric, actual, limit, unit }] : [], ); if (baseMetrics) { for (const area of ["startup", "largest"] as const) { const growth = metrics[area].css.gzipBytes - baseMetrics[area].css.gzipBytes; if (growth >= CONTROL_UI_CSS_GZIP_GROWTH_BYTES) { violations.push({ metric: `${area} CSS gzip growth`, actual: growth, limit: CONTROL_UI_CSS_GZIP_GROWTH_BYTES - 1, unit: "bytes", }); } } } if (baselineBytes !== undefined && baselineBytes > budgets.startupJsGzipBytes) { violations.unshift({ metric: "startup JS gzip baseline", actual: baselineBytes, limit: budgets.startupJsGzipBytes, unit: "bytes", }); } return violations; } function resolveControlUiStartupJsGzipLimits( budgets: Readonly, startupBudgetBaseline: Readonly | null, startupJsTolerance: number, ) { const baselineCap = budgets.startupJsGzipBytes; if (!startupBudgetBaseline) { return { baselineCap, growthLimit: baselineCap, buildVariance: 0, enforcementLimit: baselineCap, }; } const growthLimit = Math.min(startupBudgetBaseline.startupJsGzipBytes, baselineCap) + startupJsTolerance; return { baselineCap, growthLimit, buildVariance: CONTROL_UI_STARTUP_JS_GZIP_BUILD_VARIANCE_BYTES, enforcementLimit: growthLimit + CONTROL_UI_STARTUP_JS_GZIP_BUILD_VARIANCE_BYTES, }; } type ControlUiPerformanceBudgetViolation = ReturnType< typeof evaluateControlUiPerformanceBudgets >[number]; function formatControlUiPerformanceBytes(bytes: number): string { return bytes < KIB ? `${bytes} B` : `${(bytes / KIB).toFixed(1)} KiB`; } function formatRequestCount(count: number): string { return `${count} ${count === 1 ? "request" : "requests"}`; } function formatAssetSummary(summary: ReturnType): string { return `${formatRequestCount(summary.requests)}, ${formatControlUiPerformanceBytes(summary.gzipBytes)} gzip (${summary.gzipBytes} B), ${formatControlUiPerformanceBytes(summary.brotliBytes)} br, ${summary.rawBytes} B raw`; } function controlUiPerformanceWarnings( metrics: ReturnType, budgets: Readonly, ): string[] { const warnings: string[] = []; if (metrics.startup.css.gzipBytes > CONTROL_UI_STARTUP_CSS_GZIP_TARGET_BYTES) { warnings.push( `startup CSS gzip is above the ${CONTROL_UI_STARTUP_CSS_GZIP_TARGET_BYTES} B advisory target; remaining hard-ceiling headroom: ${budgets.startupCssGzipBytes - metrics.startup.css.gzipBytes} B`, ); } const largestCssHeadroom = budgets.largestCssGzipBytes - metrics.largest.css.gzipBytes; if (largestCssHeadroom < CONTROL_UI_CSS_GZIP_GROWTH_BYTES) { warnings.push(`largest CSS gzip has ${largestCssHeadroom} B of hard-ceiling headroom`); } return warnings; } function formatViolation(violation: ControlUiPerformanceBudgetViolation): string { const format = violation.unit === "bytes" ? formatControlUiPerformanceBytes : String; const actual = format(violation.actual); const limit = format(violation.limit); const exactBytes = violation.unit === "bytes" && actual === limit ? ` (${violation.actual} B vs ${violation.limit} B)` : ""; return `${violation.metric}: ${actual} exceeds ${limit}${exactBytes}`; } export function formatControlUiPerformanceReport( metrics: ReturnType, budgets: Readonly = CONTROL_UI_PERFORMANCE_BUDGETS, startupBudgetBaseline: Readonly | null = null, startupJsTolerance: number = CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES, baseMetrics: ReturnType | null = null, ): string { const startupJsLimits = resolveControlUiStartupJsGzipLimits( budgets, startupBudgetBaseline, startupJsTolerance, ); const violations = evaluateControlUiPerformanceBudgets( metrics, budgets, startupBudgetBaseline, startupJsTolerance, baseMetrics, ); const lines = [ "Control UI performance:", ` measurement: shipped gzip sidecars; Node ${process.version}`, ` startup JS: ${formatAssetSummary(metrics.startup.js)} (limits: ${formatRequestCount(budgets.startupJsRequests)}, ${formatControlUiPerformanceBytes(startupJsLimits.enforcementLimit)} gzip / ${startupJsLimits.enforcementLimit} B)`, ]; if (startupBudgetBaseline) { lines.push( ` startup JS gzip vs baseline: ${metrics.startup.js.gzipBytes} B (baseline ${startupBudgetBaseline.startupJsGzipBytes} B + growth allowance ${startupJsTolerance} B = growth limit ${startupJsLimits.growthLimit} B; build-variance allowance ${startupJsLimits.buildVariance} B; enforcement limit ${startupJsLimits.enforcementLimit} B; max committed baseline ${startupJsLimits.baselineCap} B)`, ); } lines.push( ` startup CSS: ${formatAssetSummary(metrics.startup.css)} (limits: ${formatRequestCount(budgets.startupCssRequests)}, advisory target ${CONTROL_UI_STARTUP_CSS_GZIP_TARGET_BYTES} B, hard ceiling ${budgets.startupCssGzipBytes} B; headroom ${budgets.startupCssGzipBytes - metrics.startup.css.gzipBytes} B)`, ` largest ordinary JS: ${metrics.largest.js.file}, ${formatControlUiPerformanceBytes(metrics.largest.js.gzipBytes)} gzip (limit: ${formatControlUiPerformanceBytes(budgets.largestJsGzipBytes)})`, ` largest CSS: ${metrics.largest.css.file}, ${metrics.largest.css.gzipBytes} B gzip (hard ceiling ${budgets.largestCssGzipBytes} B; headroom ${budgets.largestCssGzipBytes - metrics.largest.css.gzipBytes} B)`, ` all JS: ${formatAssetSummary(metrics.total.js)}`, ` all CSS: ${formatAssetSummary(metrics.total.css)}`, ); if (metrics.routeBoot) { lines.push( " route boot accounting: initial-entry assets plus measured immediate dynamic imports; existing initial-entry limits unchanged", ); for (const route of ["chat", "new"] as const) { const boot = metrics.routeBoot[route]; lines.push( ` ${route} boot JS: ${formatAssetSummary(boot.js)} (${boot.js.gzipBytes - metrics.startup.js.gzipBytes} B beyond initial-entry JS)`, ` ${route} boot CSS: ${formatAssetSummary(boot.css)}`, ); if (baseMetrics) { const baseBoot = baseMetrics.routeBoot?.[route]; if (baseBoot) { for (const type of ["js", "css"] as const) { const growth = boot[type].gzipBytes - baseBoot[type].gzipBytes; lines.push( ` ${route} boot ${type.toUpperCase()} gzip vs base: ${baseBoot[type].gzipBytes} B -> ${boot[type].gzipBytes} B (${growth >= 0 ? "+" : ""}${growth} B); requests ${baseBoot[type].requests} -> ${boot[type].requests}`, ); } } else { lines.push(` ${route} boot vs base: unavailable (base has no route preload templates)`); } } } } else { lines.push(" route boot accounting: unavailable (build has no route preload templates)"); } if (baseMetrics) { for (const area of ["startup", "largest"] as const) { const growth = metrics[area].css.gzipBytes - baseMetrics[area].css.gzipBytes; lines.push( ` ${area} CSS gzip vs base: ${baseMetrics[area].css.gzipBytes} B -> ${metrics[area].css.gzipBytes} B (${growth >= 0 ? "+" : ""}${growth} B; growth below ${CONTROL_UI_CSS_GZIP_GROWTH_BYTES} B allowed)`, ); } } for (const warning of controlUiPerformanceWarnings(metrics, budgets)) { lines.push(` warning: ${warning}`); } if (metrics.mermaidRenderer.length > 0) { lines.push( ` isolated Mermaid JS: ${formatAssetSummary(summarizeAssets(metrics.mermaidRenderer))} (limits: 1 deferred asset, ${formatControlUiPerformanceBytes(MERMAID_RENDERER_GZIP_BYTES)} gzip; forbidden at startup)`, ); } if (metrics.localeCatalogs.length > 0) { const localeGroups = collectControlUiLocaleAssetGroups(metrics.localeCatalogs); const largestLocalePair = localeGroups.toSorted( (left, right) => summarizeAssets([...right.base, ...right.configHints]).gzipBytes - summarizeAssets([...left.base, ...left.configHints]).gzipBytes || left.locale.localeCompare(right.locale), )[0]!; const largestLocalePairSummary = summarizeAssets([ ...largestLocalePair.base, ...largestLocalePair.configHints, ]); lines.push( ` locale catalog JS: ${formatAssetSummary(summarizeAssets(metrics.localeCatalogs))} (largest pair: ${largestLocalePair.locale}, ${formatControlUiPerformanceBytes(largestLocalePairSummary.gzipBytes)} gzip; limits: 1 base + 1 config-hint asset per locale, ${formatControlUiPerformanceBytes(CONTROL_UI_LOCALE_GZIP_BYTES)} combined; forbidden at startup)`, ); } if ( startupBudgetBaseline && metrics.startup.js.gzipBytes + STARTUP_JS_BASELINE_RATCHET_BYTES < startupBudgetBaseline.startupJsGzipBytes ) { lines.push( ` hint: startup JS gzip is more than ${STARTUP_JS_BASELINE_RATCHET_BYTES} B below the ${startupBudgetBaseline.startupJsGzipBytes} B baseline; lower it with ${BASELINE_UPDATE_COMMAND}`, ); } if (violations.length > 0) { lines.push( " violations:", ...violations.map((violation) => ` - ${formatViolation(violation)}`), ); } return lines.join("\n"); } function isIsoDate(value: string): boolean { if (!/^\d{4}-\d{2}-\d{2}$/u.test(value)) { return false; } const date = new Date(`${value}T00:00:00Z`); return !Number.isNaN(date.valueOf()) && date.toISOString().slice(0, 10) === value; } function readControlUiStartupBudgetBaseline(baselinePath: string): ControlUiStartupBudgetBaseline { try { const parsed: unknown = JSON.parse(fs.readFileSync(baselinePath, "utf8")); const record: Record = isMetricsRecord(parsed) ? parsed : {}; const { startupJsGzipBytes, reason, updatedAt } = record; if ( typeof startupJsGzipBytes !== "number" || !Number.isSafeInteger(startupJsGzipBytes) || startupJsGzipBytes < 0 || typeof reason !== "string" || reason.trim().length === 0 || typeof updatedAt !== "string" || !isIsoDate(updatedAt) ) { throw new Error( "expected non-negative integer startupJsGzipBytes, non-empty reason, and YYYY-MM-DD updatedAt", ); } return { startupJsGzipBytes, reason, updatedAt }; } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error( `Cannot read Control UI startup budget baseline ${baselinePath}: ${detail}. Regenerate it with ${BASELINE_UPDATE_COMMAND}.`, { cause: error }, ); } } function writeControlUiStartupBudgetBaseline( baselinePath: string, startupJsGzipBytes: number, reason: string, ) { if (startupJsGzipBytes > CONTROL_UI_PERFORMANCE_BUDGETS.startupJsGzipBytes) { throw new Error("startup JS gzip baseline exceeds the committed-baseline cap"); } const baseline = { startupJsGzipBytes, reason, updatedAt: new Date().toISOString().slice(0, 10), }; fs.writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`); return baseline; } type ControlUiStartupBudgetBaseline = ReturnType; function validateExplicitStartupJsBytes( startupJsBytes: number, currentBaseline: ControlUiStartupBudgetBaseline, ): void { const delta = Math.abs(startupJsBytes - currentBaseline.startupJsGzipBytes); if (delta > STARTUP_JS_BASELINE_RATCHET_BYTES) { throw new Error( `startup JS gzip baseline update: ${startupJsBytes} B differs from current baseline ${currentBaseline.startupJsGzipBytes} B by ${delta} B, exceeding the ${STARTUP_JS_BASELINE_RATCHET_BYTES} B ratchet`, ); } } export function runControlUiPerformanceCheck( distDir: string, budgets: Readonly = CONTROL_UI_PERFORMANCE_BUDGETS, baselinePath = DEFAULT_STARTUP_BUDGET_BASELINE_PATH, baseDistDir?: string, ) { const startupBudgetBaseline = readControlUiStartupBudgetBaseline(baselinePath); const metrics = collectControlUiPerformanceMetrics(distDir); const baseMetrics = baseDistDir ? collectControlUiPerformanceMetrics(baseDistDir) : null; const violations = evaluateControlUiPerformanceBudgets( metrics, budgets, startupBudgetBaseline, undefined, baseMetrics, ); const report = formatControlUiPerformanceReport( metrics, budgets, startupBudgetBaseline, undefined, baseMetrics, ); return { metrics, baseMetrics, budgets, startupBudgetBaseline, startupJsTolerance: CONTROL_UI_STARTUP_JS_GZIP_TOLERANCE_BYTES, startupJsBuildVariance: CONTROL_UI_STARTUP_JS_GZIP_BUILD_VARIANCE_BYTES, violations, warnings: controlUiPerformanceWarnings(metrics, budgets), report, }; } function main(argv: string[] = process.argv.slice(2)): void { let json = false; let reportOnly = false; let distDir: string | undefined; let baseDistDir: string | undefined; let updateBaseline = false; let reason: string | undefined; let startupJsBytes: number | undefined; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--json") { json = true; } else if (arg === "--report-only") { reportOnly = true; } else if (arg === "--base-dist" || arg === "--dist") { const value = argv[++index]; if (!value || value.startsWith("--")) { throw new Error(`${arg} requires a directory`); } if (arg === "--dist") { distDir = path.resolve(value); } else { baseDistDir = path.resolve(value); } } else if (arg === "--update-baseline") { updateBaseline = true; } else if (arg === "--reason") { reason = argv[index + 1]; if (!reason || reason.trim().length === 0 || reason.startsWith("--")) { throw new Error("--reason requires a non-empty value"); } index += 1; } else if (arg === "--startup-js-bytes") { const value = argv[index + 1]; if (!value || !/^[1-9]\d*$/u.test(value)) { throw new Error("--startup-js-bytes requires a positive integer"); } startupJsBytes = Number(value); if (!Number.isSafeInteger(startupJsBytes)) { throw new Error("--startup-js-bytes requires a positive integer"); } index += 1; } else { throw new Error(`Unknown option: ${arg}`); } } if (reason !== undefined && !updateBaseline) { throw new Error("--reason requires --update-baseline"); } if (startupJsBytes !== undefined && !updateBaseline) { throw new Error("--startup-js-bytes requires --update-baseline"); } if (json && updateBaseline) { throw new Error("--json cannot be combined with --update-baseline"); } if (updateBaseline && (reportOnly || baseDistDir || distDir)) { throw new Error( "--report-only, --base-dist and --dist cannot be combined with --update-baseline", ); } distDir ??= path.resolve(SCRIPT_DIR, "../dist/control-ui"); if (updateBaseline) { if (startupJsBytes !== undefined) { const currentBaseline = readControlUiStartupBudgetBaseline( DEFAULT_STARTUP_BUDGET_BASELINE_PATH, ); validateExplicitStartupJsBytes(startupJsBytes, currentBaseline); } const nextStartupJsBytes = startupJsBytes ?? collectControlUiPerformanceMetrics(distDir).startup.js.gzipBytes; const baseline = writeControlUiStartupBudgetBaseline( DEFAULT_STARTUP_BUDGET_BASELINE_PATH, nextStartupJsBytes, reason ?? "manual baseline update", ); process.stdout.write( `Updated config/control-ui-startup-budget-baseline.json to ${baseline.startupJsGzipBytes} B (${baseline.reason}).\n`, ); return; } const result = runControlUiPerformanceCheck(distDir, undefined, undefined, baseDistDir); if (json) { process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); } else { process.stdout.write(`${result.report}\n`); } if (!reportOnly) { const artifactContractMetrics = new Set([ "isolated Mermaid JS assets", "startup Mermaid JS assets", "locale catalog base JS assets per locale", "locale config-hint JS assets per locale", "startup locale catalog JS assets", ]); const limitsFailed = reportLimitViolations( result.violations .filter((violation) => !artifactContractMetrics.has(violation.metric)) .map((violation) => ({ file: violation.metric === "startup JS gzip baseline" ? "config/control-ui-startup-budget-baseline.json" : "scripts/check-control-ui-performance.mts", title: "Control UI asset budget", message: formatViolation(violation), })), ); if ( limitsFailed || result.violations.some((violation) => artifactContractMetrics.has(violation.metric)) ) { process.exitCode = 1; } } } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { try { main(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; } }