UNPKG

perf-audit-cli

Version:

CLI tool for continuous performance monitoring and analysis

195 lines 7.63 kB
import ora from 'ora'; import { ERROR_EXIT_CODE, SIZE_UNITS, WARNING_EXIT_CODE } from "../constants/index.js"; import { BundleAnalyzer } from "../core/bundle-analyzer.js"; import { applyBudgetsToAllBundles, getBudgetStatus } from "../utils/bundle.js"; import { CIIntegration } from "../utils/ci-integration.js"; import { getCurrentTimestamp } from "../utils/command-helpers.js"; import { loadConfig } from "../utils/config.js"; import { Logger } from "../utils/logger.js"; import { ConsoleReporter } from "../utils/reporter.js"; export const budgetCommand = async (options) => { const useSpinner = shouldUseSpinner(options.format); const spinner = createSpinner(useSpinner); try { const config = await loadConfig(); updateSpinnerText(spinner, 'Checking performance budgets...'); const bundles = await analyzeBundlesForBudget(config); if (bundles.length === 0) { handleNoBundles(spinner, useSpinner, options.format); return; } const bundlesWithBudgets = applyBudgetsToAllBundles(bundles, config); const totalStatus = calculateTotalBudgetStatus(bundles, config); const result = createBudgetAuditResult(bundlesWithBudgets, totalStatus, config); const ciContext = CIIntegration.detectCIEnvironment(); succeedSpinner(spinner, 'Budget check completed'); await generateBudgetReport(result, options, config); await CIIntegration.outputCIAnnotations(result, ciContext); exitWithBudgetStatus(result.budgetStatus); } catch (error) { handleBudgetError(spinner, useSpinner, options.format, error); } }; const shouldUseSpinner = (format) => format !== 'json'; const createSpinner = (useSpinner) => { return useSpinner ? ora('Loading configuration...').start() : null; }; const updateSpinnerText = (spinner, text) => { if (spinner) { spinner.text = text; } }; const succeedSpinner = (spinner, text) => { if (spinner) { spinner.succeed(text); } }; const analyzeBundlesForBudget = async (config) => { const allBundles = []; const analysisTarget = config.analysis.target; if (analysisTarget === 'client' || analysisTarget === 'both') { const clientBundles = await analyzeClientBundlesForBudget(config); allBundles.push(...clientBundles); } if (analysisTarget === 'server' || analysisTarget === 'both') { const serverBundles = await analyzeServerBundlesForBudget(config); allBundles.push(...serverBundles); } return allBundles; }; const analyzeClientBundlesForBudget = async (config) => { const configTyped = config; const clientAnalyzer = new BundleAnalyzer({ outputPath: configTyped.project.client.outputPath, gzip: configTyped.analysis.gzip, ignorePaths: configTyped.analysis.ignorePaths, }); const clientBundles = await clientAnalyzer.analyzeBundles(); return clientBundles.map(bundle => ({ ...bundle, type: 'client' })); }; const analyzeServerBundlesForBudget = async (config) => { const configTyped = config; const serverAnalyzer = new BundleAnalyzer({ outputPath: configTyped.project.server.outputPath, gzip: configTyped.analysis.gzip, ignorePaths: configTyped.analysis.ignorePaths, }); const serverBundles = await serverAnalyzer.analyzeBundles(); return serverBundles.map(bundle => ({ ...bundle, type: 'server' })); }; const handleNoBundles = (spinner, useSpinner, format) => { if (useSpinner) { spinner.fail('No bundles found for budget check'); } if (format !== 'json') { Logger.warn('Make sure your project has been built and the output path is correct.'); } }; const calculateTotalBudgetStatus = (bundles, config) => { const clientBundles = bundles.filter(b => b.type === 'client'); const serverBundles = bundles.filter(b => b.type === 'server'); const configTyped = config; const clientTotalBudget = configTyped.budgets.client.bundles.total; const serverTotalBudget = configTyped.budgets.server.bundles.total; const clientTotalStatus = calculateBudgetStatusForType(clientBundles, clientTotalBudget); const serverTotalStatus = calculateBudgetStatusForType(serverBundles, serverTotalBudget); return combineBudgetStatus(clientTotalStatus, serverTotalStatus); }; const calculateBudgetStatusForType = (bundles, totalBudget) => { if (!totalBudget) { return 'ok'; } const maxSize = parseSize(totalBudget.max); const warningSize = parseSize(totalBudget.warning); const totalSize = bundles.reduce((sum, b) => sum + b.size, 0); return getStatus(totalSize, warningSize, maxSize); }; const combineBudgetStatus = (clientStatus, serverStatus) => { if (clientStatus === 'error' || serverStatus === 'error') { return 'error'; } if (clientStatus === 'warning' || serverStatus === 'warning') { return 'warning'; } return 'ok'; }; const createBudgetAuditResult = (bundlesWithBudgets, totalStatus, config) => { const analysisTarget = config.analysis.target; return { timestamp: getCurrentTimestamp(), serverBundles: bundlesWithBudgets.filter(b => b.type === 'server'), clientBundles: bundlesWithBudgets.filter(b => b.type === 'client'), recommendations: [], budgetStatus: getBudgetStatus(bundlesWithBudgets, totalStatus), analysisType: analysisTarget, }; }; const generateBudgetReport = async (result, options, config) => { switch (options.format) { case 'json': generateJsonBudgetReport(result); break; case 'console': default: generateConsoleBudgetReport(result, config); break; } }; const generateJsonBudgetReport = (result) => { const jsonOutput = { passed: result.budgetStatus === 'ok', status: result.budgetStatus, violations: [ ...result.serverBundles.filter(b => b.status !== 'ok'), ...result.clientBundles.filter(b => b.status !== 'ok'), ], timestamp: result.timestamp, }; Logger.json(jsonOutput); }; const generateConsoleBudgetReport = (result, config) => { const reporter = new ConsoleReporter(config); reporter.reportBudgetCheck(result); }; const exitWithBudgetStatus = (budgetStatus) => { if (budgetStatus === 'error') { process.exit(ERROR_EXIT_CODE); } else if (budgetStatus === 'warning') { process.exit(WARNING_EXIT_CODE); } }; const handleBudgetError = (spinner, useSpinner, format, error) => { if (useSpinner) { spinner.fail('Budget check failed'); } if (format !== 'json') { Logger.error(error instanceof Error ? error.message : 'Unknown error'); } process.exit(ERROR_EXIT_CODE); }; const isSupportedSizeUnit = (value) => { return typeof value === 'string' && value in SIZE_UNITS; }; const parseSize = (sizeString) => { const match = sizeString.match(/^(\d+(?:\.\d+)?)\s*([KMGT]?B)$/i); if (!match) { throw new Error(`Invalid size format: ${sizeString}`); } const [, value, unit] = match; const unitKey = unit.toUpperCase(); if (!isSupportedSizeUnit(unitKey)) { throw new Error(`Unsupported size unit: ${unit}`); } const multiplier = SIZE_UNITS[unitKey]; return Math.round(parseFloat(value) * multiplier); }; const getStatus = (current, warning, max) => { if (current >= max) return 'error'; if (current >= warning) return 'warning'; return 'ok'; }; //# sourceMappingURL=budget.js.map