UNPKG

ngperf-audit

Version:

A comprehensive Angular performance analyzer that identifies performance bottlenecks, memory leaks, and optimization opportunities in Angular applications

915 lines • 40.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.PerformanceAnalyzerCLI = exports.PerformanceAnalyzer = void 0; const ts = __importStar(require("typescript")); const fs_1 = require("fs"); const path_1 = require("path"); // Main Performance Analyzer Class class PerformanceAnalyzer { constructor(typeChecker) { this.typeChecker = typeChecker; } analyzeComponent(componentPath) { this.componentInfo = this.parseComponentFile(componentPath); this.sourceFile = ts.createSourceFile(this.componentInfo.filePath, this.componentInfo.sourceCode, ts.ScriptTarget.Latest, true); const changeDetectionIssues = this.analyzeChangeDetection(); const templateIssues = this.analyzeTemplate(); const subscriptionIssues = this.analyzeSubscriptions(); const bundleOptimizations = this.analyzeBundleOptimizations(); const performanceScore = this.calculatePerformanceScore(changeDetectionIssues, templateIssues, subscriptionIssues); const recommendations = this.generateRecommendations(changeDetectionIssues, templateIssues, subscriptionIssues, bundleOptimizations); return { componentName: this.componentInfo.name, filePath: this.componentInfo.filePath, changeDetectionIssues, templateIssues, subscriptionIssues, bundleOptimizations, performanceScore, recommendations, }; } parseComponentFile(filePath) { const sourceCode = (0, fs_1.readFileSync)(filePath, 'utf8'); const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true); let componentName = ''; let metadata = { selector: '', inputs: [], outputs: [], providers: [], }; const visit = (node) => { if (ts.isClassDeclaration(node) && node.name) { componentName = node.name.text; // Extract @Component decorator let decorators; if (ts.canHaveDecorators(node)) { decorators = ts.getDecorators(node); } const decorator = decorators?.find((d) => ts.isDecorator(d) && ts.isCallExpression(d.expression) && ts.isIdentifier(d.expression.expression) && d.expression.expression.text === 'Component'); if (decorator && ts.isCallExpression(decorator.expression)) { const arg = decorator.expression.arguments[0]; if (ts.isObjectLiteralExpression(arg)) { metadata = this.parseComponentMetadata(arg); } } } ts.forEachChild(node, visit); }; visit(sourceFile); let templateCode = ''; if (metadata.templateUrl) { const templatePath = (0, path_1.join)(filePath, '..', metadata.templateUrl); try { templateCode = (0, fs_1.readFileSync)(templatePath, 'utf8'); } catch (error) { console.warn(`Could not read template file: ${templatePath}`); } } return { name: componentName, filePath, sourceCode, templateCode, metadata, }; } parseComponentMetadata(metadataObj) { const metadata = { selector: '', inputs: [], outputs: [], providers: [], }; metadataObj.properties.forEach((prop) => { if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { const name = prop.name.text; if (name === 'selector' && ts.isStringLiteral(prop.initializer)) { metadata.selector = prop.initializer.text; } else if (name === 'templateUrl' && ts.isStringLiteral(prop.initializer)) { metadata.templateUrl = prop.initializer.text; } else if (name === 'changeDetection' && ts.isPropertyAccessExpression(prop.initializer)) { metadata.changeDetection = prop.initializer.name.text; } } }); return metadata; } analyzeChangeDetection() { const issues = []; // Check if OnPush is missing - but only for components that would benefit from it if (!this.componentInfo.metadata.changeDetection || this.componentInfo.metadata.changeDetection !== 'OnPush') { if (this.shouldRecommendOnPush()) { issues.push({ type: 'missing-onpush', severity: 'high', location: this.getComponentDecoratorLocation(), description: 'Component uses default change detection strategy', estimatedImpact: '60% reduction in change detection cycles', fix: 'Add ChangeDetectionStrategy.OnPush to component decorator', }); } } // Check for function calls in template if (this.componentInfo.templateCode) { const functionCalls = this.findFunctionCallsInTemplate(this.componentInfo.templateCode); functionCalls.forEach((call) => { issues.push({ type: 'function-in-template', severity: 'high', location: call.location, description: `Function call '${call.functionName}' in template causes unnecessary re-execution`, estimatedImpact: 'Significant performance degradation on each change detection', fix: 'Move function call to component property or use pipe', }); }); } // Check for object comparisons in ngIf if (this.componentInfo.templateCode) { const objectComparisons = this.findObjectComparisonsInTemplate(this.componentInfo.templateCode); objectComparisons.forEach((comparison) => { issues.push({ type: 'object-comparison', severity: 'medium', location: comparison.location, description: `Object comparison in template: ${comparison.expression}`, estimatedImpact: 'Unnecessary re-renders when object references change', fix: 'Use trackBy function or compare primitive values', }); }); } return issues; } analyzeTemplate() { const issues = []; if (!this.componentInfo.templateCode) return issues; // Check for missing trackBy in ngFor const ngForLoops = this.findNgForLoops(this.componentInfo.templateCode); ngForLoops.forEach((loop) => { if (!loop.hasTrackBy) { const syntaxType = loop.isModernSyntax ? '@for' : '*ngFor'; const defaultFix = loop.isModernSyntax ? 'Add track expression to prevent unnecessary DOM manipulations' : 'Add trackBy function to prevent unnecessary DOM manipulations'; const smartFix = loop.recommendedTracking || defaultFix; issues.push({ type: 'missing-trackby', severity: 'high', location: loop.location, description: `${syntaxType} loop missing tracking function`, elementCount: loop.estimatedSize, fix: smartFix, }); } else if (loop.recommendedTracking) { // Has tracking but could be improved issues.push({ type: 'suboptimal-trackby', severity: 'medium', location: loop.location, description: `${loop.isModernSyntax ? '@for' : '*ngFor'} loop tracking could be optimized`, elementCount: loop.estimatedSize, fix: loop.recommendedTracking, }); } }); // Check for async pipe opportunities const subscriptionUsages = this.findSubscriptionUsagesInTemplate(this.componentInfo.templateCode); subscriptionUsages.forEach((usage) => { issues.push({ type: 'async-pipe-opportunity', severity: 'medium', location: usage.location, description: `Manual subscription can be replaced with async pipe`, fix: 'Replace manual subscription with async pipe for automatic unsubscription', }); }); // Check for large ngFor lists ngForLoops.forEach((loop) => { if (loop.estimatedSize && loop.estimatedSize > 100) { issues.push({ type: 'large-ngfor', severity: 'high', location: loop.location, description: `Large ngFor loop with ~${loop.estimatedSize} items`, elementCount: loop.estimatedSize, fix: 'Consider virtual scrolling or pagination for large lists', }); } }); return issues; } analyzeSubscriptions() { const issues = []; // Find manual subscriptions const subscriptions = this.findManualSubscriptions(); subscriptions.forEach((sub) => { issues.push({ type: 'manual-subscription', severity: 'medium', location: sub.location, description: `Manual subscription without proper cleanup: ${sub.variableName}`, fix: 'Use async pipe, takeUntil pattern, or implement OnDestroy', }); }); // Check for multiple subscriptions that could be combined if (subscriptions.length > 3) { issues.push({ type: 'multiple-subscriptions', severity: 'low', location: this.getComponentDecoratorLocation(), description: `Component has ${subscriptions.length} manual subscriptions`, fix: 'Consider combining subscriptions using combineLatest or merge operators', }); } return issues; } analyzeBundleOptimizations() { const optimizations = []; // Check for lazy loading opportunities const imports = this.findImports(); const largeImports = imports.filter((imp) => this.isLargeLibrary(imp.moduleName)); largeImports.forEach((imp) => { optimizations.push({ type: 'lazy-loading', description: `Large library '${imp.moduleName}' could be lazy loaded`, estimatedSizeReduction: '20-40KB', implementation: 'Consider lazy loading this module or using dynamic imports', }); }); return optimizations; } calculatePerformanceScore(changeDetectionIssues, templateIssues, subscriptionIssues) { let score = 100; // Deduct points for each issue based on severity const deductPoints = (issues) => { issues.forEach((issue) => { switch (issue.severity) { case 'high': score -= 15; break; case 'medium': score -= 10; break; case 'low': score -= 5; break; } }); }; deductPoints(changeDetectionIssues); deductPoints(templateIssues); deductPoints(subscriptionIssues); return Math.max(0, score); } generateRecommendations(changeDetectionIssues, templateIssues, subscriptionIssues, bundleOptimizations) { const recommendations = []; // Priority recommendations based on impact const hasOnPushIssue = changeDetectionIssues.some((issue) => issue.type === 'missing-onpush'); if (hasOnPushIssue) { recommendations.push({ priority: 'high', category: 'performance', title: 'Implement OnPush Change Detection', description: 'Switch to OnPush strategy to dramatically reduce change detection cycles', implementation: 'Add ChangeDetectionStrategy.OnPush to @Component decorator', estimatedImpact: '60% reduction in change detection overhead', }); } const hasTrackByIssues = templateIssues.some((issue) => issue.type === 'missing-trackby'); if (hasTrackByIssues) { recommendations.push({ priority: 'high', category: 'performance', title: 'Add TrackBy Functions', description: 'Implement trackBy functions for all ngFor loops to prevent unnecessary DOM updates', implementation: 'Create trackBy functions that return unique identifiers for list items', estimatedImpact: 'Eliminate unnecessary DOM re-renders for list updates', }); } const hasSubscriptionIssues = subscriptionIssues.length > 0; if (hasSubscriptionIssues) { recommendations.push({ priority: 'medium', category: 'memory', title: 'Optimize Subscription Management', description: 'Replace manual subscriptions with async pipes or proper cleanup', implementation: 'Use async pipe in templates or implement takeUntil pattern', estimatedImpact: 'Prevent memory leaks and improve component cleanup', }); } return recommendations; } /** * Determines if a component is complex enough to warrant OnPush strategy * Only recommends OnPush for components that have meaningful logic or complexity */ shouldRecommendOnPush() { const sourceCode = this.componentInfo.sourceCode; const templateCode = this.componentInfo.templateCode || ''; // Check if component has meaningful complexity indicators const complexityIndicators = [ // Has injected services (likely doing some logic) /constructor\s*\([^)]*\s+\w+Service/i, /constructor\s*\([^)]*\s+Http/i, /constructor\s*\([^)]*\s+Api/i, // Has lifecycle hooks (ngOnInit, ngOnChanges, etc.) /ngOnInit\s*\(/, /ngOnChanges\s*\(/, /ngAfterViewInit\s*\(/, /ngOnDestroy\s*\(/, // Has observables/subscriptions /\.subscribe\s*\(/, /Observable\s*</, /Subject\s*</, /BehaviorSubject\s*</, // Has form handling /FormBuilder/, /FormGroup/, /FormControl/, /Validators\./, // Has complex methods (more than just getters/setters) /\w+\s*\([^)]*\)\s*:\s*\w+\s*\{[\s\S]{50,}/, // Has computed properties /get\s+\w+\s*\(\s*\)\s*\{[\s\S]{20,}/, // Has event handlers /on\w+\s*\(/, /handle\w+\s*\(/, // Has inputs that could change frequently /@Input\(\)\s+\w+(?:\s*:\s*(?:any|object|\w+\[\]))/, ]; // Check template complexity indicators const templateComplexityIndicators = [ // Has ngFor loops (data iteration) /\*ngFor/, // Has conditional rendering /\*ngIf/, /\[ngSwitch\]/, // Has event bindings /\(\w+\)\s*=/, // Has property bindings /\[\w+\]\s*=/, // Has multiple interpolations /\{\{.*?\}\}/g, ]; // Count complexity indicators in source code const sourceComplexityCount = complexityIndicators.reduce((count, pattern) => { return count + (pattern.test(sourceCode) ? 1 : 0); }, 0); // Count template complexity const templateMatches = templateCode.match(/\{\{.*?\}\}/g) || []; const hasTemplateComplexity = templateComplexityIndicators.some(pattern => pattern.test(templateCode)); // Only recommend OnPush if component has meaningful complexity // Criteria: // - Has at least 2 source complexity indicators, OR // - Has template complexity AND at least 1 source complexity indicator, OR // - Has many interpolations (>3) indicating data binding complexity return (sourceComplexityCount >= 2 || (hasTemplateComplexity && sourceComplexityCount >= 1) || templateMatches.length > 3); } // Helper methods for analysis getComponentDecoratorLocation() { return { file: this.componentInfo.filePath, line: 1, column: 1, snippet: '@Component({', }; } findFunctionCallsInTemplate(template) { const functionCalls = []; const regex = /\{\{\s*(\w+)\s*\(\s*\)\s*\}\}/g; let match; while ((match = regex.exec(template)) !== null) { functionCalls.push({ functionName: match[1], location: { file: this.componentInfo.templatePath || 'inline template', line: this.getLineNumber(template, match.index), column: this.getColumnNumber(template, match.index), snippet: match[0], }, }); } return functionCalls; } findObjectComparisonsInTemplate(template) { const comparisons = []; const regex = /\*ngIf\s*=\s*"([^"]*\.\w+\s*===?\s*[^"]*)/g; let match; while ((match = regex.exec(template)) !== null) { comparisons.push({ expression: match[1], location: { file: this.componentInfo.templatePath || 'inline template', line: this.getLineNumber(template, match.index), column: this.getColumnNumber(template, match.index), snippet: match[0], }, }); } return comparisons; } findNgForLoops(template) { const loops = []; // Legacy *ngFor syntax const ngForRegex = /\*ngFor\s*=\s*"([^"]*)"/g; let match; while ((match = ngForRegex.exec(template)) !== null) { const hasTrackBy = match[1].includes('trackBy'); loops.push({ location: { file: this.componentInfo.templatePath || 'inline template', line: this.getLineNumber(template, match.index), column: this.getColumnNumber(template, match.index), snippet: match[0], }, hasTrackBy, estimatedSize: this.estimateLoopSize(match[1]), isModernSyntax: false }); } // Modern @for syntax const modernForRegex = /@for\s*\(\s*([^;]+);\s*track\s+([^)]+)\)|@for\s*\(\s*([^)]+)\)/g; while ((match = modernForRegex.exec(template)) !== null) { const hasTrackBy = match[2] !== undefined; // Has track expression const loopExpression = match[1] || match[3]; // Get the loop expression const trackExpression = match[2]; // Get tracking expression if present // Analyze the loop expression for smart tracking recommendations const recommendedTracking = this.analyzeForTrackingRecommendation(loopExpression, trackExpression); loops.push({ location: { file: this.componentInfo.templatePath || 'inline template', line: this.getLineNumber(template, match.index), column: this.getColumnNumber(template, match.index), snippet: match[0], }, hasTrackBy, estimatedSize: this.estimateLoopSize(loopExpression), isModernSyntax: true, recommendedTracking }); } return loops; } analyzeForTrackingRecommendation(loopExpression, currentTracking) { if (currentTracking) { // Already has tracking, check if it could be improved if (currentTracking.includes('$index') && this.hasIdProperty(loopExpression)) { return 'Consider tracking by object id instead of $index for better performance'; } return undefined; } // No tracking, provide recommendation if (this.hasIdProperty(loopExpression)) { const itemVar = this.extractItemVariable(loopExpression); return `Add tracking: track ${itemVar}.id`; } else { const itemVar = this.extractItemVariable(loopExpression); return `Add tracking: track ${itemVar}`; } } hasIdProperty(loopExpression) { // Simple heuristic: if the array variable contains 'item', 'user', 'product', etc. // and typically has id properties, suggest id tracking const commonIdPatterns = /\b(items?|users?|products?|entities?|records?|data|list|todos?|tasks?|messages?|posts?)\b/i; return commonIdPatterns.test(loopExpression); } extractItemVariable(loopExpression) { // Extract item variable from expressions like "let item of items" or "item of collection; let i = $index" const match = loopExpression.match(/let\s+(\w+)\s+of|(\w+)\s+of/); return match ? (match[1] || match[2]) : 'item'; } findSubscriptionUsagesInTemplate(template) { const usages = []; const regex = /\{\{\s*(\w+)\s*\}\}/g; let match; while ((match = regex.exec(template)) !== null) { if (this.isSubscriptionVariable(match[1])) { usages.push({ location: { file: this.componentInfo.templatePath || 'inline template', line: this.getLineNumber(template, match.index), column: this.getColumnNumber(template, match.index), snippet: match[0], }, }); } } return usages; } findManualSubscriptions() { const subscriptions = []; const visit = (node) => { if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'subscribe') { const sourceFile = node.getSourceFile(); const lineChar = sourceFile.getLineAndCharacterOfPosition(node.getStart()); subscriptions.push({ location: { file: this.componentInfo.filePath, line: lineChar.line + 1, column: lineChar.character + 1, snippet: node.getText(), }, variableName: node.expression.expression.getText(), }); } ts.forEachChild(node, visit); }; visit(this.sourceFile); return subscriptions; } findImports() { const imports = []; const visit = (node) => { if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { const sourceFile = node.getSourceFile(); const lineChar = sourceFile.getLineAndCharacterOfPosition(node.getStart()); imports.push({ moduleName: node.moduleSpecifier.text, location: { file: this.componentInfo.filePath, line: lineChar.line + 1, column: lineChar.character + 1, snippet: node.getText(), }, }); } ts.forEachChild(node, visit); }; visit(this.sourceFile); return imports; } estimateLoopSize(ngForExpression) { // This is a simplified estimation - in real implementation, you'd analyze the component // to understand the data source size if (ngForExpression.includes('users') || ngForExpression.includes('items')) { return 50; // Estimated average } return undefined; } isSubscriptionVariable(variableName) { // Check if variable name suggests it's from a subscription return (variableName.includes('$') || variableName.includes('subscription') || variableName.includes('observable') || variableName.includes('stream')); } isLargeLibrary(moduleName) { const largeLibraries = ['lodash', 'moment', 'rxjs', '@angular/material']; return largeLibraries.some((lib) => moduleName.includes(lib)); } getLineNumber(text, index) { return text.substring(0, index).split('\n').length; } getColumnNumber(text, index) { const lines = text.substring(0, index).split('\n'); return lines[lines.length - 1].length + 1; } } exports.PerformanceAnalyzer = PerformanceAnalyzer; // Usage example and CLI integration helper class PerformanceAnalyzerCLI { static analyzeProject(projectPath) { const analyzer = new PerformanceAnalyzer(); const results = []; // Use current working directory if no path provided const targetPath = projectPath || process.cwd(); // In real implementation, you'd recursively find all component files // This is a simplified example const componentFiles = this.findComponentFiles(targetPath); componentFiles.forEach((filePath) => { try { const analysis = analyzer.analyzeComponent(filePath); results.push(analysis); } catch (error) { console.error(`Error analyzing ${filePath}:`, error); } }); return results; } static analyzeProjectWithSummary(projectPath) { const analyzer = new PerformanceAnalyzer(); const results = []; // Use current working directory if no path provided const targetPath = projectPath || process.cwd(); const componentFiles = this.findComponentFiles(targetPath); console.log(`Found ${componentFiles.length} component files to analyze...`); let successCount = 0; let errorCount = 0; componentFiles.forEach((filePath, index) => { try { console.log(`Analyzing ${index + 1}/${componentFiles.length}: ${filePath}`); const analysis = analyzer.analyzeComponent(filePath); results.push(analysis); successCount++; } catch (error) { console.error(`Error analyzing ${filePath}:`, error); errorCount++; } }); const summary = this.generateProjectSummary(results, successCount, errorCount); return { analyses: results, summary, }; } static generateProjectSummary(analyses, successCount, errorCount) { const totalIssues = analyses.reduce((sum, analysis) => { return (sum + analysis.changeDetectionIssues.length + analysis.templateIssues.length + analysis.subscriptionIssues.length); }, 0); const averageScore = analyses.length > 0 ? analyses.reduce((sum, analysis) => sum + analysis.performanceScore, 0) / analyses.length : 0; const issuesByType = { 'missing-onpush': 0, 'function-in-template': 0, 'missing-trackby': 0, 'manual-subscription': 0, 'async-pipe-opportunity': 0, 'large-ngfor': 0, }; analyses.forEach((analysis) => { analysis.changeDetectionIssues.forEach((issue) => { issuesByType[issue.type]++; }); analysis.templateIssues.forEach((issue) => { issuesByType[issue.type]++; }); analysis.subscriptionIssues.forEach((issue) => { issuesByType[issue.type]++; }); }); return { totalComponents: successCount, totalIssues, averagePerformanceScore: Math.round(averageScore * 100) / 100, analysisErrors: errorCount, issueBreakdown: issuesByType, topIssues: Object.entries(issuesByType) .sort(([, a], [, b]) => b - a) .slice(0, 3) .map(([type, count]) => ({ type, count })), }; } static findComponentFiles(projectPath) { const componentFiles = []; const traverseDirectory = (dirPath) => { try { const items = (0, fs_1.readdirSync)(dirPath); for (const item of items) { const fullPath = (0, path_1.join)(dirPath, item); try { const stats = (0, fs_1.statSync)(fullPath); if (stats.isDirectory()) { // Skip node_modules, dist, and other build directories if (!this.shouldSkipDirectory(item)) { traverseDirectory(fullPath); } } else if (stats.isFile()) { // Check if it's a component file if (this.isComponentFile(fullPath)) { componentFiles.push(fullPath); } } } catch (error) { // Skip files/directories we can't access console.warn(`Could not access ${fullPath}:`, error instanceof Error ? error.message : error); } } } catch (error) { console.warn(`Could not read directory ${dirPath}:`, error instanceof Error ? error.message : error); } }; traverseDirectory(projectPath); return componentFiles; } static shouldSkipDirectory(dirName) { const skipDirs = [ 'node_modules', 'dist', 'build', '.git', '.angular', 'coverage', 'e2e', '.vscode', '.idea', 'tmp', 'temp', ]; return skipDirs.includes(dirName) || dirName.startsWith('.'); } static isComponentFile(filePath) { // Check if the file has .component.ts extension if (!filePath.endsWith('.component.ts')) { return false; } // Additional validation: check if the file actually contains a @Component decorator try { const content = (0, fs_1.readFileSync)(filePath, 'utf8'); // Simple check for @Component decorator const hasComponentDecorator = content.includes('@Component'); const hasClassDeclaration = /class\s+\w+.*Component/i.test(content); return hasComponentDecorator && hasClassDeclaration; } catch (error) { console.warn(`Could not validate component file ${filePath}:`, error instanceof Error ? error.message : error); return false; } } static generateReport(analyses) { let report = '# Angular Performance Analysis Report\n\n'; if (analyses.length === 0) { report += 'āš ļø No components found to analyze.\n\n'; return report; } // Project overview const totalIssues = analyses.reduce((sum, analysis) => { return (sum + analysis.changeDetectionIssues.length + analysis.templateIssues.length + analysis.subscriptionIssues.length); }, 0); const averageScore = analyses.reduce((sum, analysis) => sum + analysis.performanceScore, 0) / analyses.length; report += `## šŸ“Š Project Overview\n`; report += `- **Components Analyzed**: ${analyses.length}\n`; report += `- **Average Performance Score**: ${Math.round(averageScore * 100) / 100}/100\n`; report += `- **Total Issues Found**: ${totalIssues}\n\n`; // Performance distribution const scoreRanges = { excellent: analyses.filter((a) => a.performanceScore >= 90).length, good: analyses.filter((a) => a.performanceScore >= 70 && a.performanceScore < 90).length, fair: analyses.filter((a) => a.performanceScore >= 50 && a.performanceScore < 70).length, poor: analyses.filter((a) => a.performanceScore < 50).length, }; report += `## šŸŽÆ Performance Distribution\n`; report += `- **Excellent (90-100)**: ${scoreRanges.excellent} components\n`; report += `- **Good (70-89)**: ${scoreRanges.good} components\n`; report += `- **Fair (50-69)**: ${scoreRanges.fair} components\n`; report += `- **Poor (0-49)**: ${scoreRanges.poor} components\n\n`; // Top priority components (lowest scores) const sortedByScore = [...analyses].sort((a, b) => a.performanceScore - b.performanceScore); const topPriority = sortedByScore.slice(0, 5); if (topPriority.length > 0) { report += `## 🚨 Top Priority Components (Lowest Scores)\n`; topPriority.forEach((analysis, index) => { report += `${index + 1}. **${analysis.componentName}** - Score: ${analysis.performanceScore}/100\n`; }); report += '\n'; } // Detailed component analysis report += `## šŸ“‹ Detailed Component Analysis\n\n`; analyses.forEach((analysis) => { report += `### ${analysis.componentName}\n`; report += `**File**: \`${analysis.filePath}\` \n`; report += `**Performance Score**: ${analysis.performanceScore}/100\n\n`; const allIssues = [ ...analysis.changeDetectionIssues, ...analysis.templateIssues, ...analysis.subscriptionIssues, ]; if (allIssues.length > 0) { report += `#### Issues Found (${allIssues.length})\n`; // Group by severity const highSeverity = allIssues.filter((i) => i.severity === 'high'); const mediumSeverity = allIssues.filter((i) => i.severity === 'medium'); const lowSeverity = allIssues.filter((i) => i.severity === 'low'); [ { severity: 'high', issues: highSeverity, emoji: 'šŸ”“' }, { severity: 'medium', issues: mediumSeverity, emoji: '🟔' }, { severity: 'low', issues: lowSeverity, emoji: '🟢' }, ].forEach((group) => { if (group.issues.length > 0) { group.issues.forEach((issue) => { report += `${group.emoji} **${issue.type}** (${issue.severity})\n`; report += ` ${issue.description}\n`; report += ` *Fix*: ${issue.fix}\n\n`; }); } }); } if (analysis.recommendations.length > 0) { report += '#### šŸ’” Recommendations\n'; analysis.recommendations.forEach((rec) => { const priorityEmoji = rec.priority === 'high' ? 'šŸ”„' : rec.priority === 'medium' ? '⚔' : 'šŸ’”'; report += `${priorityEmoji} **${rec.title}** (${rec.priority} priority)\n`; report += ` ${rec.description}\n`; report += ` *Impact*: ${rec.estimatedImpact}\n\n`; }); } report += '---\n\n'; }); return report; } static generateReportWithSummary(analyses, summary) { let report = this.generateReport(analyses); // Add summary section at the end report += `## šŸ“ˆ Analysis Summary\n\n`; report += `- **Components Processed**: ${summary.totalComponents}\n`; report += `- **Analysis Errors**: ${summary.analysisErrors}\n`; report += `- **Average Score**: ${summary.averagePerformanceScore}/100\n\n`; if (summary.topIssues.length > 0) { report += `### Most Common Issues\n`; summary.topIssues.forEach((issue, index) => { report += `${index + 1}. **${issue.type}**: ${issue.count} occurrences\n`; }); } return report; } static async saveReportToFile(report, outputPath = 'performance-report.md') { const fs = await Promise.resolve().then(() => __importStar(require('fs/promises'))); try { await fs.writeFile(outputPath, report, 'utf8'); console.log(`āœ… Performance report saved to: ${outputPath}`); } catch (error) { console.error(`āŒ Failed to save report:`, error); throw error; } } static runAnalysis(projectPath, outputPath) { // Use current working directory if no path provided const targetPath = projectPath || process.cwd(); console.log(`šŸ” Starting performance analysis for: ${targetPath}`); const startTime = Date.now(); const { analyses, summary } = this.analyzeProjectWithSummary(targetPath); const endTime = Date.now(); console.log(`\nāœ… Analysis completed in ${endTime - startTime}ms`); console.log(`šŸ“Š Results: ${summary.totalComponents} components, ${summary.totalIssues} issues found`); const report = this.generateReportWithSummary(analyses, summary); if (outputPath) { this.saveReportToFile(report, outputPath).catch(console.error); } else { console.log('\n' + report); } return analyses; } } exports.PerformanceAnalyzerCLI = PerformanceAnalyzerCLI; //# sourceMappingURL=performance-analyzer.js.map