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
text/typescript
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);
}
};