perf-audit-cli
Version:
CLI tool for continuous performance monitoring and analysis
207 lines ⢠10.5 kB
JavaScript
import chalk from 'chalk';
import { BundleAnalyzer } from "../core/bundle-analyzer.js";
import { formatDelta, formatSize } from "./size.js";
export class ConsoleReporter {
config;
constructor(config) {
this.config = config;
}
reportBundleAnalysis(result, showDetails = false) {
console.log(chalk.blue('\nšÆ Performance Audit Report'));
console.log(chalk.blue('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
const typeIndicator = result.analysisType === 'both'
? 'š¦š„ļø Client & Server Analysis'
: result.analysisType === 'client'
? 'š¦ Client-side Analysis'
: 'š„ļø Server-side Analysis';
console.log(chalk.blue(`\n${typeIndicator}`));
if (result.clientBundles.length > 0) {
console.log(chalk.yellow('\nš¦ Client Bundles:'));
result.clientBundles.forEach(bundle => {
const status = this.getStatusIcon(bundle.status);
const sizeText = formatSize(bundle.size);
const gzipText = bundle.gzipSize ? ` (gzip: ${formatSize(bundle.gzipSize)})` : '';
const deltaText = bundle.delta ? ` ${this.formatDelta(bundle.delta)}` : '';
console.log(`āā ${bundle.name}: ${sizeText}${gzipText} ${status}${deltaText}`);
if (showDetails && bundle.status !== 'ok') {
this.showBundleDetails(bundle);
}
});
const clientTotalSize = result.clientBundles.reduce((sum, b) => sum + b.size, 0);
const clientTotalGzipSize = result.clientBundles.reduce((sum, b) => sum + (b.gzipSize || 0), 0);
const clientSizeText = formatSize(clientTotalSize);
const clientGzipText = clientTotalGzipSize > 0 ? ` (gzip: ${formatSize(clientTotalGzipSize)})` : '';
console.log(`āā Client Total: ${clientSizeText}${clientGzipText}`);
}
if (result.serverBundles.length > 0) {
console.log(chalk.green('\nš„ļø Server Bundles:'));
result.serverBundles.forEach(bundle => {
const status = this.getStatusIcon(bundle.status);
const sizeText = formatSize(bundle.size);
const gzipText = bundle.gzipSize ? ` (gzip: ${formatSize(bundle.gzipSize)})` : '';
const deltaText = bundle.delta ? ` ${this.formatDelta(bundle.delta)}` : '';
console.log(`āā ${bundle.name}: ${sizeText}${gzipText} ${status}${deltaText}`);
if (showDetails && bundle.status !== 'ok') {
this.showBundleDetails(bundle);
}
});
const serverTotalSize = result.serverBundles.reduce((sum, b) => sum + b.size, 0);
const serverTotalGzipSize = result.serverBundles.reduce((sum, b) => sum + (b.gzipSize || 0), 0);
const serverSizeText = formatSize(serverTotalSize);
const serverGzipText = serverTotalGzipSize > 0 ? ` (gzip: ${formatSize(serverTotalGzipSize)})` : '';
console.log(`āā Server Total: ${serverSizeText}${serverGzipText}`);
}
if (result.analysisType === 'both' && (result.clientBundles.length > 0 || result.serverBundles.length > 0)) {
console.log(chalk.blue('\nš Overall Total:'));
const totalSizes = BundleAnalyzer.calculateTotalSize([...result.serverBundles, ...result.clientBundles]);
const totalSizeText = formatSize(totalSizes.size);
const totalGzipText = totalSizes.gzipSize ? ` (gzip: ${formatSize(totalSizes.gzipSize)})` : '';
console.log(`āā Combined Total: ${totalSizeText}${totalGzipText}`);
}
if (result.lighthouse) {
this.reportPerformanceMetrics(result.lighthouse);
}
if (result.recommendations.length > 0) {
console.log(chalk.blue('\nš” Recommendations:'));
result.recommendations.forEach(rec => {
console.log(`- ${rec}`);
});
}
console.log(this.getOverallStatus(result.budgetStatus));
}
reportBudgetCheck(result) {
console.log(chalk.blue('\nš° Budget Check Report'));
console.log(chalk.blue('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
let hasViolations = false;
[...result.serverBundles, ...result.clientBundles].forEach(bundle => {
if (bundle.status !== 'ok') {
hasViolations = true;
const status = this.getStatusIcon(bundle.status);
const sizeText = formatSize(bundle.size);
console.log(`${status} ${bundle.name}: ${sizeText}`);
const budgetKey = this.getBudgetKey(bundle.name);
const bundleType = bundle.type ?? 'client';
const budgetConfig = bundleType === 'server' ? this.config.budgets.server : this.config.budgets.client;
const budget = budgetConfig.bundles[budgetKey];
if (budget) {
console.log(` Budget: ${budget.warning} (warning) / ${budget.max} (max)`);
}
}
});
if (!hasViolations) {
console.log(chalk.green('ā
All bundles are within budget limits'));
}
console.log(this.getOverallStatus(result.budgetStatus));
}
reportLighthouseResults(result) {
if (!result.lighthouse)
return;
const metrics = result.lighthouse;
console.log(chalk.blue('\nš Lighthouse Scores'));
console.log(`āā Performance: ${this.formatScore(metrics.performance)}/100 ${this.getScoreIcon(metrics.performance)}`);
console.log(`āā Accessibility: ${this.formatScore(metrics.accessibility || 0)}/100 ${this.getScoreIcon(metrics.accessibility || 0)}`);
console.log(`āā Best Practices: ${this.formatScore(metrics.bestPractices || 0)}/100 ${this.getScoreIcon(metrics.bestPractices || 0)}`);
console.log(`āā SEO: ${this.formatScore(metrics.seo || 0)}/100 ${this.getScoreIcon(metrics.seo || 0)}`);
console.log(chalk.blue('\nš Core Web Vitals'));
if (metrics.metrics) {
const { fcp, lcp, cls, tti } = metrics.metrics;
console.log(`āā FCP: ${fcp}ms ${this.getMetricStatus(fcp, this.config.budgets.metrics.fcp)}`);
console.log(`āā LCP: ${lcp}ms ${this.getMetricStatus(lcp, this.config.budgets.metrics.lcp)}`);
console.log(`āā CLS: ${cls} ${this.getMetricStatus(cls, this.config.budgets.metrics.cls)}`);
console.log(`āā TTI: ${tti}ms ${this.getMetricStatus(tti, this.config.budgets.metrics.tti)}`);
}
if (result.recommendations.length > 0) {
console.log(chalk.blue('\nš” Performance Recommendations:'));
result.recommendations.forEach(rec => {
console.log(`- ${rec}`);
});
}
console.log(this.getOverallStatus(result.budgetStatus));
}
formatScore(score) {
return score.toString().padStart(2);
}
getScoreIcon(score) {
if (score >= 90)
return chalk.green('ā
');
if (score >= 75)
return chalk.yellow('ā ļø');
return chalk.red('ā');
}
reportPerformanceMetrics(metrics) {
console.log(chalk.blue('\nš Performance Metrics'));
if (metrics.performance !== undefined) {
const status = metrics.performance >= 90 ? 'ā
' : metrics.performance >= 75 ? 'ā ļø' : 'ā';
console.log(`āā Performance Score: ${metrics.performance}/100 ${status}`);
}
if (metrics.metrics) {
const { fcp, lcp, cls, tti } = metrics.metrics;
console.log(`āā FCP: ${fcp}ms ${this.getMetricStatus(fcp, this.config.budgets.metrics.fcp)}`);
console.log(`āā LCP: ${lcp}ms ${this.getMetricStatus(lcp, this.config.budgets.metrics.lcp)}`);
console.log(`āā CLS: ${cls} ${this.getMetricStatus(cls, this.config.budgets.metrics.cls)}`);
console.log(`āā TTI: ${tti}ms ${this.getMetricStatus(tti, this.config.budgets.metrics.tti)}`);
}
}
getStatusIcon(status) {
switch (status) {
case 'ok':
return chalk.green('ā
');
case 'warning':
return chalk.yellow('ā ļø');
case 'error':
return chalk.red('ā');
default:
return '';
}
}
formatDelta(delta) {
const formatted = formatDelta(delta);
if (delta > 0) {
return chalk.red(formatted);
}
else if (delta < 0) {
return chalk.green(formatted);
}
return formatted;
}
showBundleDetails(bundle) {
const indent = ' ';
console.log(chalk.dim(`${indent}Size: ${formatSize(bundle.size)}`));
if (bundle.gzipSize) {
console.log(chalk.dim(`${indent}Gzipped: ${formatSize(bundle.gzipSize)}`));
}
}
getMetricStatus(value, budget) {
if (value >= budget.max) {
return chalk.red(`ā (budget: ${budget.max})`);
}
else if (value >= budget.warning) {
return chalk.yellow(`ā ļø (budget: ${budget.warning})`);
}
return chalk.green('ā
');
}
getOverallStatus(status) {
const timestamp = new Date().toLocaleString();
switch (status) {
case 'ok':
return chalk.green(`\nā
All checks passed! (${timestamp})`);
case 'warning':
return chalk.yellow(`\nā ļø Some warnings detected (${timestamp})`);
case 'error':
return chalk.red(`\nā Budget violations detected! (${timestamp})`);
default:
return '';
}
}
getBudgetKey(bundleName) {
const name = bundleName.toLowerCase();
if (name.includes('main') || name.includes('index'))
return 'main';
if (name.includes('vendor') || name.includes('chunk'))
return 'vendor';
if (name.includes('runtime'))
return 'runtime';
return 'main';
}
}
//# sourceMappingURL=reporter.js.map