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
JavaScript
;
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