UNPKG

ruch

Version:

Revolutionary React TypeScript CLI with hexagonal architecture & AI-powered development assistance. Create maintainable, scalable applications with domain-driven design and integrated AI tooling.

214 lines (172 loc) • 6.98 kB
import { Command } from 'commander'; import path from 'path'; import fs from 'fs'; import express from 'express'; import { spawn } from 'child_process'; import { generateGraphData } from '../utils/dependency-analyzer'; import { createFileSystem } from '../utils/file-operations'; import { logger } from '../utils/logger'; export interface VisualizeOptions { port?: number; output?: string; format?: 'json' | 'console' | 'app'; projectRoot?: string; open?: boolean; } export const createVisualizeCommand = (): Command => { const command = new Command('visualize'); command .description('Visualize domain dependencies in an interactive React app') .option('-p, --port <port>', 'Port for the web server', '3000') .option('-o, --output <path>', 'Output path for graph data JSON') .option('-f, --format <format>', 'Output format (json|console|app)', 'app') .option('-r, --project-root <path>', 'Project root directory', process.cwd()) .option('--no-open', 'Do not automatically open browser') .action(async (options: VisualizeOptions) => { try { await executeVisualize(options); } catch (error) { logger.error(`Failed to run visualize: ${error instanceof Error ? error.message : 'Unknown error'}`); process.exit(1); } }); return command; }; const displayConsoleVisualization = (graphData: any): void => { console.log('\nšŸ—ļø RUCH Domain Visualization\n'); console.log('═'.repeat(50)); // Stats console.log(`šŸ“Š Statistics:`); console.log(` • Domains: ${graphData.stats.totalDomains}`); console.log(` • Dependencies: ${graphData.stats.totalDependencies}`); console.log(` • Circular Dependencies: ${graphData.stats.circularDependencyCount}`); console.log(''); // Domains console.log('šŸ“ Domains:'); graphData.nodes.forEach((node: any) => { const typeEmoji = node.type === 'core' ? 'šŸ”§' : node.type === 'shared' ? 'šŸ”—' : '⚔'; console.log(` ${typeEmoji} ${node.name} (${node.fileCount} files)`); if (node.dependencies.length > 0) { console.log(` Dependencies: ${node.dependencies.join(', ')}`); } }); // Circular dependencies if (graphData.circularDependencies.length > 0) { console.log('\nāš ļø Circular Dependencies:'); graphData.circularDependencies.forEach((cycle: string[], index: number) => { console.log(` ${index + 1}. ${cycle.join(' → ')} → ${cycle[0]}`); }); } console.log('\n' + '═'.repeat(50)); console.log('šŸ’” Tip: Use --format json to export data or --format app to start the interactive app'); }; const openBrowser = (url: string): void => { const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'; spawn(start, [url], { detached: true, stdio: 'ignore' }); }; const createExpressServer = (graphData: any, port: number): Promise<void> => { return new Promise((resolve, reject) => { const app = express(); // Get the path to the built React app const cliPath = path.dirname(require.resolve('../../package.json')); const appDistPath = path.join(cliPath, 'src', 'visualizer', 'app', 'dist'); // API endpoint for graph data app.get('/api/graph', (req, res) => { res.json(graphData); }); // Serve static React app app.use(express.static(appDistPath)); // Fallback to index.html for SPA routing app.get('*', (req, res) => { res.sendFile(path.join(appDistPath, 'index.html')); }); const server = app.listen(port, () => { resolve(); }); server.on('error', (error) => { reject(error); }); // Handle graceful shutdown process.on('SIGINT', () => { logger.info('Shutting down visualization server...'); server.close(() => { process.exit(0); }); }); }); }; const buildReactApp = async (): Promise<void> => { return new Promise((resolve, reject) => { const cliPath = path.dirname(require.resolve('../../package.json')); const appPath = path.join(cliPath, 'src', 'visualizer', 'app'); logger.info('šŸ“¦ Building React app...'); const buildProcess = spawn('npm', ['run', 'build'], { cwd: appPath, stdio: 'pipe', shell: true }); buildProcess.stdout?.on('data', (data) => { // Optionally log build output }); buildProcess.stderr?.on('data', (data) => { console.error(data.toString()); }); buildProcess.on('close', (code) => { if (code === 0) { logger.success('āœ… React app built successfully'); resolve(); } else { reject(new Error(`Build process exited with code ${code}`)); } }); }); }; export const executeVisualize = async (options: VisualizeOptions): Promise<void> => { const projectRoot = options.projectRoot || process.cwd(); const port = parseInt(options.port?.toString() || '3000'); const shouldOpen = options.open !== false; logger.info('šŸ” Analyzing domain dependencies...'); // Generate graph data from user's project const fileSystem = createFileSystem(); const graphData = await generateGraphData(projectRoot, fileSystem); if (graphData.nodes.length === 0) { logger.warning('No domains found in src/domains/. Make sure you have domains created.'); return; } logger.success(`Found ${graphData.stats.totalDomains} domains with ${graphData.stats.totalDependencies} dependencies`); if (graphData.stats.circularDependencyCount > 0) { logger.warning(`āš ļø Found ${graphData.stats.circularDependencyCount} circular dependencies`); } // Handle different output formats if (options.format === 'json' || options.output) { const outputPath = options.output || path.join(projectRoot, 'graph-data.json'); await fs.promises.writeFile(outputPath, JSON.stringify(graphData, null, 2)); logger.success(`Graph data saved to: ${outputPath}`); if (options.format === 'json') { return; // Only output JSON, don't start server } } if (options.format === 'console') { displayConsoleVisualization(graphData); return; } // Default behavior: start React app with Express server try { // Build the React app first await buildReactApp(); logger.info('šŸš€ Starting visualization server...'); // Start Express server await createExpressServer(graphData, port); const url = `http://localhost:${port}`; logger.success(`šŸŽÆ Visualization ready at: ${url}`); if (shouldOpen) { setTimeout(() => openBrowser(url), 1000); // Small delay to ensure server is ready } // Keep the process alive logger.info('Press Ctrl+C to stop the server'); } catch (error) { logger.error(`Failed to start server: ${error instanceof Error ? error.message : 'Unknown error'}`); process.exit(1); } };