mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
379 lines • 17.1 kB
JavaScript
export class DependencyAnalyzer {
projectRoot;
constructor(projectRoot) {
this.projectRoot = projectRoot;
}
async analyze() {
const fs = await import('fs-extra');
const path = await import('path');
try {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (!(await fs.pathExists(packageJsonPath))) {
return {
totalDependencies: 0,
outdated: 0,
vulnerabilities: 0,
unused: []
};
}
const packageJson = await fs.readJson(packageJsonPath);
const dependencies = packageJson.dependencies || {};
const devDependencies = packageJson.devDependencies || {};
const allDeps = { ...dependencies, ...devDependencies };
const totalDependencies = Object.keys(allDeps).length;
// Check for potential issues by analyzing dependency patterns
let outdated = 0;
let vulnerabilities = 0;
const unused = [];
// Simple heuristics for outdated dependencies (check for very old version patterns)
Object.entries(allDeps).forEach(([name, version]) => {
const versionStr = String(version);
// Flag potentially old versions (major version < 2 for common packages)
if (versionStr.match(/^[\^~]?[01]\./) &&
['react', 'vue', 'angular', 'typescript', 'webpack'].some(lib => name.includes(lib))) {
outdated++;
}
// Flag potential security concerns (very old packages or known problematic patterns)
if (versionStr.match(/^[\^~]?0\./) ||
name.includes('debug') && versionStr.match(/^[\^~]?[0-2]\./)) {
vulnerabilities++;
}
});
// Look for potentially unused dependencies by checking if they're imported
const srcDir = path.join(this.projectRoot, 'src');
if (await fs.pathExists(srcDir)) {
const usedDeps = new Set();
// Scan for imports in TypeScript/JavaScript files
const files = await this.findSourceFiles(srcDir);
for (const file of files) {
try {
const content = await fs.readFile(file, 'utf-8');
// Extract import statements
const importMatches = content.match(/(?:import|require)\s*[\(\[\{]?[^'"]*['"]([^'"]+)['"]/g) || [];
importMatches.forEach((match) => {
const moduleMatch = match.match(/['"]([^'"]+)['"]/);
if (moduleMatch) {
const moduleName = moduleMatch[1];
// Extract package name (handle scoped packages)
const packageName = moduleName.startsWith('@')
? moduleName.split('/').slice(0, 2).join('/')
: moduleName.split('/')[0];
usedDeps.add(packageName);
}
});
}
catch {
// Skip files that can't be read
}
}
// Find dependencies that aren't imported
Object.keys(allDeps).forEach(dep => {
if (!usedDeps.has(dep) &&
!['@types/', 'eslint', 'prettier', 'jest', 'vitest', 'webpack', 'vite'].some(tool => dep.includes(tool))) {
unused.push(dep);
}
});
}
// Advanced analysis - skip in test environments or when no node_modules
const nodeModulesPath = path.join(this.projectRoot, 'node_modules');
const skipAdvancedAnalysis = !(await fs.pathExists(nodeModulesPath)) ||
process.env.NODE_ENV === 'test' ||
process.env.MIRA_QUICK_MODE === 'true';
const dependencyGraph = skipAdvancedAnalysis ? undefined : await this.analyzeDependencyGraph();
const qualityMetrics = skipAdvancedAnalysis ? undefined : await this.analyzePackageQuality(allDeps);
// Calculate security score
const securityScore = this.calculateSecurityScore(totalDependencies, vulnerabilities, outdated, dependencyGraph, qualityMetrics);
return {
totalDependencies,
outdated: Math.min(outdated, totalDependencies),
vulnerabilities: Math.min(vulnerabilities, totalDependencies),
unused: unused.slice(0, 10), // Limit to top 10 potentially unused
dependencyGraph,
qualityMetrics,
securityScore
};
}
catch (error) {
return {
totalDependencies: 0,
outdated: 0,
vulnerabilities: 0,
unused: []
};
}
}
async findSourceFiles(dir) {
const fs = await import('fs-extra');
const path = await import('path');
const files = [];
try {
const entries = await fs.readdir(dir);
for (const entry of entries) {
const fullPath = path.join(dir, entry);
const stat = await fs.stat(fullPath);
if (stat.isDirectory() && !['node_modules', '.git', 'dist', 'build'].includes(entry)) {
files.push(...await this.findSourceFiles(fullPath));
}
else if (stat.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry)) {
files.push(fullPath);
}
}
}
catch {
// Return empty if directory can't be read
}
return files;
}
async analyzeDependencyGraph() {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
const fs = await import('fs-extra');
const path = await import('path');
try {
// Check if npm ls command works
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (!(await fs.pathExists(packageJsonPath))) {
return undefined;
}
// Check if node_modules exists
const nodeModulesPath = path.join(this.projectRoot, 'node_modules');
if (!(await fs.pathExists(nodeModulesPath))) {
// No node_modules, can't analyze dependency graph
return undefined;
}
let dependencyTree;
try {
// Run npm ls to get dependency tree
const { stdout } = await execAsync('npm ls --json --depth=3', {
cwd: this.projectRoot,
timeout: 5000,
maxBuffer: 1024 * 1024 // 1MB buffer
});
dependencyTree = JSON.parse(stdout);
}
catch (error) {
// npm ls might fail if there are missing dependencies
console.log('[DependencyAnalyzer] npm ls failed, skipping dependency graph analysis');
return undefined;
}
// Analyze the dependency tree
const circularDependencies = [];
const duplicates = [];
const heaviestPaths = [];
// Find duplicates
const packageVersions = new Map();
const traverseTree = (node, path = [], visited = new Set()) => {
if (!node.dependencies)
return 0;
let weight = 0;
for (const [name, info] of Object.entries(node.dependencies)) {
const dependency = info;
const fullName = `${name}@${dependency.version}`;
// Check for circular dependencies
if (visited.has(name)) {
circularDependencies.push([...path, name].join(' -> '));
}
else {
// Track versions for duplicate detection
if (!packageVersions.has(name)) {
packageVersions.set(name, new Set());
}
packageVersions.get(name).add(dependency.version);
// Recurse into subdependencies
const newVisited = new Set(visited);
newVisited.add(name);
const subWeight = traverseTree(dependency, [...path, name], newVisited);
weight += subWeight + 1;
}
}
return weight;
};
const totalWeight = traverseTree(dependencyTree);
// Find duplicates
for (const [name, versions] of packageVersions.entries()) {
if (versions.size > 1) {
duplicates.push({
name,
versions: Array.from(versions)
});
}
}
// Calculate depth
const calculateDepth = (node, currentDepth = 0) => {
if (!node.dependencies)
return currentDepth;
let maxDepth = currentDepth;
for (const dependency of Object.values(node.dependencies)) {
maxDepth = Math.max(maxDepth, calculateDepth(dependency, currentDepth + 1));
}
return maxDepth;
};
const depth = calculateDepth(dependencyTree);
return {
depth,
circularDependencies: [...new Set(circularDependencies)].slice(0, 10),
heaviestPaths: heaviestPaths.slice(0, 5),
duplicates: duplicates.slice(0, 20)
};
}
catch (error) {
// npm ls failed, return minimal analysis
return {
depth: 0,
circularDependencies: [],
heaviestPaths: [],
duplicates: []
};
}
}
async analyzePackageQuality(dependencies) {
const https = await import('https');
const { promisify } = await import('util');
try {
const highRiskPackages = [];
let totalQualityScore = 0;
let analyzedPackages = 0;
// Analyze a sample of packages (to avoid API rate limits)
const packageNames = Object.keys(dependencies).slice(0, 10);
for (const packageName of packageNames) {
try {
// Fetch package info from npm registry
const packageInfo = await this.fetchPackageInfo(packageName);
if (packageInfo) {
const issues = [];
let qualityScore = 100;
// Check last update
const lastPublished = new Date(packageInfo.time?.modified || packageInfo.time?.created);
const daysSinceUpdate = (Date.now() - lastPublished.getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceUpdate > 365) {
issues.push('Not updated in over a year');
qualityScore -= 30;
}
else if (daysSinceUpdate > 180) {
issues.push('Not updated in over 6 months');
qualityScore -= 15;
}
// Check maintainers
const maintainers = packageInfo.maintainers?.length || 0;
if (maintainers === 0) {
issues.push('No maintainers listed');
qualityScore -= 20;
}
else if (maintainers === 1) {
issues.push('Single maintainer (bus factor risk)');
qualityScore -= 10;
}
// Check downloads (this would require npm download stats API)
const weeklyDownloads = 0; // Placeholder
// Check for common quality indicators
if (!packageInfo.repository) {
issues.push('No repository URL');
qualityScore -= 15;
}
if (!packageInfo.license) {
issues.push('No license specified');
qualityScore -= 10;
}
if (!packageInfo.description || packageInfo.description.length < 20) {
issues.push('Poor or missing description');
qualityScore -= 5;
}
if (issues.length > 0) {
highRiskPackages.push({
name: packageName,
issues,
weeklyDownloads,
lastUpdate: lastPublished.toISOString().split('T')[0],
maintainers
});
}
totalQualityScore += Math.max(0, qualityScore);
analyzedPackages++;
}
}
catch (error) {
// Skip packages that can't be analyzed
}
}
const averageQualityScore = analyzedPackages > 0 ? totalQualityScore / analyzedPackages : 0;
return {
averageQualityScore,
highRiskPackages: highRiskPackages.slice(0, 10)
};
}
catch (error) {
return undefined;
}
}
async fetchPackageInfo(packageName) {
return new Promise(async (resolve, reject) => {
const https = await import('https');
const options = {
hostname: 'registry.npmjs.org',
path: `/${encodeURIComponent(packageName)}`,
method: 'GET',
headers: {
'User-Agent': 'MIRA-DependencyAnalyzer/1.0'
},
timeout: 5000
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const packageInfo = JSON.parse(data);
resolve(packageInfo);
}
catch (error) {
resolve(null);
}
});
});
req.on('error', () => {
resolve(null);
});
req.on('timeout', () => {
req.destroy();
resolve(null);
});
req.end();
});
}
calculateSecurityScore(totalDependencies, vulnerabilities, outdated, dependencyGraph, qualityMetrics) {
let score = 100;
// No dependencies = perfect score
if (totalDependencies === 0) {
return 100;
}
// Vulnerability impact (weighted heavily)
const vulnRatio = vulnerabilities / totalDependencies;
if (vulnRatio > 0) {
score -= Math.min(50, vulnRatio * 100 * 2); // Up to 50 points for vulnerabilities
}
// Outdated packages impact
const outdatedRatio = outdated / totalDependencies;
if (outdatedRatio > 0) {
score -= Math.min(20, outdatedRatio * 100 * 0.4); // Up to 20 points for outdated
}
// Circular dependencies impact
if (dependencyGraph && dependencyGraph.circularDependencies.length > 0) {
score -= Math.min(10, dependencyGraph.circularDependencies.length * 2);
}
// Duplicate packages impact
if (dependencyGraph && dependencyGraph.duplicates.length > 0) {
score -= Math.min(10, dependencyGraph.duplicates.length);
}
// Quality metrics impact
if (qualityMetrics) {
const qualityPenalty = Math.max(0, 100 - qualityMetrics.averageQualityScore) * 0.1;
score -= Math.min(10, qualityPenalty);
}
// Ensure score is within bounds
return Math.max(0, Math.min(100, Math.round(score)));
}
}
//# sourceMappingURL=DependencyAnalyzer.js.map