UNPKG

vibe-janitor

Version:

A CLI tool that cleans AI-generated JavaScript/TypeScript projects efficiently and intelligently

202 lines (199 loc) 7.16 kB
import fs from 'fs-extra'; import path from 'path'; import madge from 'madge'; import { Logger } from '../utils/logger.js'; /** * Detects circular dependencies in the project using madge */ export class CircularDependencyScanner { targetDir; options; constructor(targetDir, options = {}) { this.targetDir = targetDir; this.options = { verbose: options.verbose ?? false, excludeRegExp: options.excludeRegExp ?? ['node_modules', 'dist', 'build', 'coverage'], fileExtensions: options.fileExtensions ?? ['js', 'jsx', 'ts', 'tsx'], }; } /** * Find circular dependencies in the project */ async scan() { if (this.options.verbose) { Logger.info('Scanning for circular dependencies...'); } const result = { circularDependencies: [], dependencyCount: 0, fileCount: 0, warnings: [], }; try { // Check if the directory exists if (!fs.existsSync(this.targetDir)) { throw new Error(`Directory not found: ${this.targetDir}`); } // Configure madge options const madgeConfig = { excludeRegExp: this.options.excludeRegExp, fileExtensions: this.options.fileExtensions, detectiveOptions: { ts: { skipTypeImports: true, }, }, }; // Run madge analysis const madgeResult = await madge(this.targetDir, madgeConfig); // Get the dependency graph const dependencyGraph = madgeResult.obj(); result.fileCount = Object.keys(dependencyGraph).length; // Count total dependencies let dependencyCount = 0; Object.values(dependencyGraph).forEach((deps) => { dependencyCount += deps.length; }); result.dependencyCount = dependencyCount; // Detect circular dependencies const circularDeps = madgeResult.circular(); result.circularDependencies = circularDeps; if (this.options.verbose) { Logger.info(`Found ${circularDeps.length} circular dependencies among ${result.fileCount} files`); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); Logger.error(`Circular dependency scan failed: ${errorMessage}`); result.warnings.push(errorMessage); return result; } } /** * Generate a report of the circular dependencies */ generateReport(result) { if (result.circularDependencies.length === 0) { return 'No circular dependencies found! 🎉'; } let report = '# Circular Dependency Report\n\n'; report += `Found ${result.circularDependencies.length} circular dependencies among ${result.fileCount} files.\n\n`; report += '## Circular Dependencies\n\n'; result.circularDependencies.forEach((circle, index) => { report += `### Circular Dependency #${index + 1}\n\n`; report += 'File chain:\n\n'; circle.forEach((file, i) => { if (i === circle.length - 1) { report += `${i + 1}. \`${file}\` → cycles back to \`${circle[0]}\`\n`; } else { report += `${i + 1}. \`${file}\` → imports \`${circle[i + 1]}\`\n`; } }); report += '\n**Recommended Fix:** Break this dependency cycle by:\n'; report += '- Extracting shared logic to a separate module\n'; report += '- Using dependency injection\n'; report += '- Rethinking the component architecture\n\n'; }); if (result.warnings.length > 0) { report += '## Warnings\n\n'; result.warnings.forEach((warning) => { report += `- ${warning}\n`; }); report += '\n'; } report += '## Impact\n\n'; report += 'Circular dependencies can cause:\n\n'; report += '- Unexpected behavior in module initialization\n'; report += '- Problems with tree-shaking and bundle size\n'; report += '- Difficulty in understanding and maintaining code\n'; report += '- Testing challenges\n'; return report; } /** * Generate a visual graph of the dependencies as HTML */ async generateGraph() { try { // Configure madge options const madgeConfig = { excludeRegExp: this.options.excludeRegExp, fileExtensions: this.options.fileExtensions, }; // Run madge analysis const madgeResult = await madge(this.targetDir, madgeConfig); // Generate an SVG graph const svgGraph = await madgeResult.svg(); // Create an HTML file that displays the graph const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Dependency Graph</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; } h1 { text-align: center; color: #333; } .graph-container { overflow: auto; border: 1px solid #ddd; padding: 10px; margin-top: 20px; } .legend { margin-top: 20px; padding: 10px; background-color: #f5f5f5; border-radius: 4px; } .legend h3 { margin-top: 0; } .circular { color: #d32f2f; font-weight: bold; } </style> </head> <body> <h1>Project Dependency Graph</h1> <div class="legend"> <h3>How to Read This Graph</h3> <p>This graph shows the dependencies between files in your project:</p> <ul> <li>Each <strong>node</strong> represents a file</li> <li>Each <strong>arrow</strong> represents a dependency (import)</li> <li>Files in <span class="circular">red</span> are part of circular dependencies</li> </ul> <p>Tip: You can zoom and pan the graph for better visibility.</p> </div> <div class="graph-container"> ${svgGraph} </div> </body> </html> `; // Write the HTML file const outputPath = path.join(this.targetDir, 'dependency-graph.html'); await fs.writeFile(outputPath, html); if (this.options.verbose) { Logger.success(`Dependency graph generated: ${outputPath}`); } return outputPath; } catch (error) { Logger.error(`Failed to generate dependency graph: ${error instanceof Error ? error.message : String(error)}`); return null; } } }