UNPKG

perf-lens

Version:

AI-powered frontend performance optimizer

213 lines (212 loc) • 11.9 kB
import { Command, Option } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import path from 'path'; import { runLighthouse } from '../../utils/lighthouse.js'; import { analyzeCodebase, analyzeCodebaseWithAgent } from '../../utils/codeAnalysis.js'; import { saveReport } from '../../utils/output.js'; import { loadConfig } from '../../utils/config.js'; import { getCostSummary } from '../../ai/cost.js'; export function createScanCommand() { const scanCommand = new Command('scan') .description('Scan your frontend UI for performance issues') .option('-c, --config <path>', 'Path to config file') .option('-p, --port <number>', 'Development server port') .option('-t, --target <directory>', 'Target directory to scan (default: current directory)') .option('-f, --max-files <number>', 'Maximum total number of files to analyze') .option('-b, --batch-size <number>', 'Number of files to analyze per batch') .option('-s, --max-size <number>', 'Maximum file size in KB to analyze') .option('-d, --batch-delay <number>', 'Delay between batches in milliseconds') .option('-o, --output <path>', 'Output file path for the report') .option('--format <type>', 'Output format (md or html)') .option('--mobile', 'Enable mobile emulation') .option('--cpu-throttle <number>', 'CPU throttle percentage') .option('--network-throttle <type>', 'Network throttle type (slow3G, fast3G, 4G, none)') .option('--agent', 'Agentic analysis: the model investigates the codebase via tools') .addOption(new Option('--fail-on <severity>', 'Exit with code 1 if findings at or above this severity exist (for CI)').choices(['critical', 'warning', 'suggestion'])) .option('--no-cache', 'Skip the analysis result cache') .option('--verbose', 'Enable verbose output') .option('--timeout <number>', 'Timeout in milliseconds for Lighthouse analysis') .option('--retries <number>', 'Number of retries for failed operations') .action(async (options) => { const startTime = Date.now(); try { console.clear(); console.log(chalk.blue.bold('šŸ” PerfLens Performance Scanner')); console.log(chalk.gray('─'.repeat(50))); // Load configuration const config = await loadConfig(options.config ? path.resolve(process.cwd(), options.config) : undefined); // Override config with CLI options if (options.maxFiles) config.analysis.maxFiles = parseInt(options.maxFiles); if (options.batchSize) config.analysis.batchSize = parseInt(options.batchSize); if (options.maxSize) config.analysis.maxFileSize = parseInt(options.maxSize) * 1024; if (options.batchDelay) config.analysis.batchDelay = parseInt(options.batchDelay); if (options.format) config.output.format = options.format; if (options.port) config.lighthouse.port = parseInt(options.port); if (options.target) config.analysis.targetDir = options.target; if (options.verbose) config.verbose = options.verbose; if (options.output) { const outputPath = options.output; config.output = { ...config.output, directory: path.dirname(outputPath), filename: path.basename(outputPath, path.extname(outputPath)), }; } if (options.mobile) config.lighthouse.mobileEmulation = true; if (options.cpuThrottle) config.lighthouse.throttling.cpu = parseInt(options.cpuThrottle); if (options.networkThrottle) config.lighthouse.throttling.network = options.networkThrottle; if (options.timeout) config.lighthouse.timeout = parseInt(options.timeout); if (options.retries) config.lighthouse.retries = parseInt(options.retries); // Print analysis configuration if (config.verbose) { console.log(chalk.blue.bold('\nAnalysis Configuration:')); if (config.analysis?.targetDir) { console.log(`Target directory: ${chalk.yellow(config.analysis.targetDir)}`); } console.log(`Maximum files to analyze: ${chalk.yellow(config.analysis?.maxFiles)}`); console.log(`Files per batch: ${chalk.yellow(config.analysis?.batchSize)}`); console.log(`Maximum file size: ${chalk.yellow(config.analysis?.maxFileSize ? Math.round(config.analysis.maxFileSize / 1024) + 'KB' : '100KB')}`); console.log(`Batch delay: ${chalk.yellow(config.analysis?.batchDelay)}ms`); if (config.analysis?.ignore?.length) { console.log(`Ignore patterns: ${chalk.yellow(config.analysis.ignore.length)} patterns`); } if (config.lighthouse?.port) { console.log(`Development server port: ${chalk.yellow(config.lighthouse.port)}`); } if (config.output?.directory) { console.log(`Output directory: ${chalk.yellow(config.output.directory)}`); } console.log(`Output format: ${chalk.yellow(config.output?.format || 'md')}`); if (config.ai) { console.log(`AI Model: ${chalk.yellow(config.ai.model || 'default')}`); if (config.ai.maxTokens) { console.log(`Max Tokens: ${chalk.yellow(config.ai.maxTokens)}`); } } console.log(chalk.gray('─'.repeat(50))); } // Step 1: Run Lighthouse Analysis let lhResults; try { lhResults = await runLighthouse({ ...config.lighthouse, bundleThresholds: config.bundleThresholds, performanceThresholds: config.thresholds, verbose: config.verbose, ai: config.ai, }); // Step 2: AI code analysis const spinner = ora('Analyzing your code...').start(); if (config.verbose || options.agent) spinner.stop(); try { const analyze = options.agent ? analyzeCodebaseWithAgent : analyzeCodebase; const codeAnalysisResults = await analyze({ ...config.analysis, lighthouseContext: { metrics: lhResults.metrics, analysis: lhResults.analysis, }, ai: config.ai, verbose: config.verbose, noCache: options.cache === false, }); if (config.verbose) { // Display results in console console.log('\n' + chalk.blue.bold('🧠 Code Analysis')); console.log(chalk.gray('─'.repeat(50))); console.log(`\nFound ${chalk.red(codeAnalysisResults.critical.length.toString())} critical, ${chalk.yellow(codeAnalysisResults.warnings.length.toString())} warnings, and ${chalk.blue(codeAnalysisResults.suggestions.length.toString())} suggestions.\n`); if (codeAnalysisResults.critical.length > 0) { console.log(chalk.red.bold('\n🚨 Critical Code Issues:')); codeAnalysisResults.critical.forEach(issue => console.log(issue)); } if (codeAnalysisResults.warnings.length > 0) { console.log(chalk.yellow.bold('\nāš ļø Code Warnings:')); codeAnalysisResults.warnings.forEach(issue => console.log(issue)); } if (codeAnalysisResults.suggestions.length > 0) { console.log(chalk.blue.bold('\nšŸ’” Code Suggestions:')); codeAnalysisResults.suggestions.forEach(issue => console.log(issue)); } } spinner.succeed('Code analysis complete!'); // Save report const reportData = { lighthouse: { metrics: lhResults.metrics, report: lhResults.report, analysis: lhResults.analysis, }, codeAnalysis: { critical: codeAnalysisResults.critical, warnings: codeAnalysisResults.warnings, suggestions: codeAnalysisResults.suggestions, findings: codeAnalysisResults.findings, }, metadata: { timestamp: new Date().toISOString(), duration: Date.now() - startTime, config, }, }; const savedPath = saveReport(reportData, config.output?.format || 'md', config.output?.directory ? path.join(config.output.directory, `${config.output.filename || 'performance-report'}${config.output.includeTimestamp ? `-${new Date().toISOString().replace(/[:.]/g, '-')}` : ''}.${config.output.format}`) : undefined); console.log(chalk.green(`\nReport saved to: ${savedPath}`)); const costSummary = getCostSummary(); if (costSummary) { console.log(chalk.gray(costSummary)); } console.log('\n' + chalk.blue.bold('✨ Analysis Complete')); console.log(chalk.gray('─'.repeat(50))); // CI gate: fail when findings reach the configured severity if (options.failOn) { const rank = { suggestion: 0, warning: 1, critical: 2 }; const threshold = rank[options.failOn]; let failing = codeAnalysisResults.critical.length; if (threshold <= 1) failing += codeAnalysisResults.warnings.length; if (threshold <= 0) failing += codeAnalysisResults.suggestions.length; if (failing > 0) { console.log(chalk.red(`āœ– ${failing} finding(s) at or above '${options.failOn}' severity (--fail-on)`)); process.exit(1); } } } catch (error) { console.error(chalk.red('Error during code analysis:'), error instanceof Error ? error.message : error); process.exit(1); } } catch (lhError) { if (lhError instanceof Error && lhError.message.includes('No development server detected')) { console.log(lhError.message); process.exit(1); } console.error(lhError); process.exit(1); } } catch (error) { console.error('Error:', error); process.exit(1); } }); return scanCommand; }