stack-performance
Version:
A comprehensive application stack analyzer that evaluates MEAN, MERN, and other Node.js-based applications across 15 performance criteria
182 lines (159 loc) • 5.62 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
/**
* Base class for all criterion analyzers
* Provides common functionality and enforces interface
*/
class BaseAnalyzer {
constructor(stackInfo, projectPath, criterionName) {
this.stackInfo = stackInfo;
this.projectPath = projectPath;
this.criterionName = criterionName;
}
/**
* Main analysis method - must be implemented by subclasses
* @returns {Object} Analysis result with score, remark, and details
*/
async analyze() {
throw new Error('analyze() method must be implemented by subclass');
}
/**
* Convert numerical score to performance remark
* @param {number} score - Score out of 100
* @returns {string} Performance remark
*/
getPerformanceRemark(score) {
if (score >= 95) return 'Excellent';
if (score >= 85) return 'Best';
if (score >= 75) return 'Good';
if (score >= 60) return 'Average';
return 'Poor';
}
/**
* Check if a dependency exists in the project
* @param {string} dependency - Dependency name
* @returns {boolean} Whether dependency exists
*/
hasDependency(dependency) {
const packageJson = this.stackInfo.packageJson;
if (!packageJson) return false;
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
...packageJson.optionalDependencies
};
return !!allDeps[dependency];
}
/**
* Get dependency version if it exists
* @param {string} dependency - Dependency name
* @returns {string|null} Version string or null
*/
getDependencyVersion(dependency) {
const packageJson = this.stackInfo.packageJson;
if (!packageJson) return null;
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
...packageJson.optionalDependencies
};
return allDeps[dependency] || null;
}
/**
* Check if file exists in project
* @param {string} relativePath - Path relative to project root
* @returns {Promise<boolean>} Whether file exists
*/
async fileExists(relativePath) {
return await fs.pathExists(path.join(this.projectPath, relativePath));
}
/**
* Find files matching pattern
* @param {string} pattern - Glob pattern
* @returns {Array<string>} Matching file paths
*/
findFiles(pattern) {
return glob.sync(pattern, { cwd: this.projectPath });
}
/**
* Read file content safely
* @param {string} relativePath - Path relative to project root
* @returns {Promise<string|null>} File content or null
*/
async readFile(relativePath) {
try {
return await fs.readFile(path.join(this.projectPath, relativePath), 'utf8');
} catch (error) {
return null;
}
}
/**
* Calculate weighted score based on multiple factors
* @param {Array<Object>} factors - Array of {score, weight} objects
* @returns {number} Weighted average score
*/
calculateWeightedScore(factors) {
const totalWeight = factors.reduce((sum, factor) => sum + factor.weight, 0);
if (totalWeight === 0) return 0;
const weightedSum = factors.reduce((sum, factor) => sum + (factor.score * factor.weight), 0);
return Math.round(weightedSum / totalWeight);
}
/**
* Analyze code complexity based on file patterns
* @param {Array<string>} files - Array of file paths
* @returns {Object} Complexity metrics
*/
async analyzeCodeComplexity(files) {
const complexity = {
totalFiles: files.length,
totalLines: 0,
avgLinesPerFile: 0,
complexityScore: 0
};
for (const file of files) {
const content = await this.readFile(file);
if (content) {
const lines = content.split('\n').length;
complexity.totalLines += lines;
// Basic complexity indicators
const complexityIndicators = [
(content.match(/if\s*\(/g) || []).length,
(content.match(/for\s*\(/g) || []).length,
(content.match(/while\s*\(/g) || []).length,
(content.match(/function\s+\w+/g) || []).length,
(content.match(/=>\s*{/g) || []).length,
(content.match(/try\s*{/g) || []).length,
(content.match(/catch\s*\(/g) || []).length
];
const fileComplexity = complexityIndicators.reduce((sum, count) => sum + count, 0);
complexity.complexityScore += fileComplexity / lines * 100; // Normalize by file size
}
}
if (complexity.totalFiles > 0) {
complexity.avgLinesPerFile = Math.round(complexity.totalLines / complexity.totalFiles);
complexity.complexityScore = Math.round(complexity.complexityScore / complexity.totalFiles);
}
return complexity;
}
/**
* Create standardized result object
* @param {number} score - Score out of 100
* @param {Object} details - Detailed analysis results
* @param {Array<string>} recommendations - Improvement recommendations
* @returns {Object} Standardized result
*/
createResult(score, details = {}, recommendations = []) {
return {
name: this.criterionName,
score: Math.max(0, Math.min(100, Math.round(score))), // Ensure score is 0-100
remark: this.getPerformanceRemark(score),
details,
recommendations,
timestamp: new Date().toISOString()
};
}
}
module.exports = BaseAnalyzer;