UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

379 lines 17.1 kB
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