perf-audit-cli
Version:
CLI tool for continuous performance monitoring and analysis
195 lines • 7.63 kB
JavaScript
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