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.

362 lines (304 loc) 9.8 kB
import path from 'path'; import { createFileSystem, type FileSystem } from './file-operations'; export interface ImportStatement { statement: string; line: number; from?: string; imports?: string[]; type: 'import' | 'export'; } export interface DomainNode { id: string; name: string; type: string; fileCount: number; dependencies: string[]; dependents: string[]; files: string[]; } export interface DependencyEdge { from: string; to: string; type: 'direct-import' | 'indirect-import'; count: number; metadata: { files: string[]; }; } export interface GraphData { nodes: DomainNode[]; edges: DependencyEdge[]; circularDependencies: string[][]; stats: { totalDomains: number; totalDependencies: number; circularDependencyCount: number; }; } export const extractImportStatements = (content: string): ImportStatement[] => { const lines = content.split('\n'); const imports: ImportStatement[] = []; const importRegex = /^import\s+(?:type\s+)?(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"];?/; const exportRegex = /^export\s+(?:\{[^}]*\}\s+from\s+['"]([^'"]+)['"]|.*from\s+['"]([^'"]+)['"])/; lines.forEach((line, index) => { const trimmedLine = line.trim(); // Import statements const importMatch = trimmedLine.match(importRegex); if (importMatch) { imports.push({ statement: trimmedLine, line: index + 1, from: importMatch[1], type: 'import' }); } // Export statements with from const exportMatch = trimmedLine.match(exportRegex); if (exportMatch) { imports.push({ statement: trimmedLine, line: index + 1, from: exportMatch[1] || exportMatch[2], type: 'export' }); } }); return imports; }; export const extractDomainFromImportPath = (importPath: string, currentDomainPath: string): string | null => { // Skip non-relative imports (node_modules, etc.) if (!importPath.startsWith('.') && !importPath.startsWith('/')) { return null; } // Resolve the absolute path const resolvedPath = path.resolve(path.dirname(currentDomainPath), importPath); // Check if it's a cross-domain import const domainMatch = resolvedPath.match(/\/src\/domains\/([^\/]+)/); if (domainMatch) { const targetDomain = domainMatch[1]; // Extract current domain from current file path const currentDomainMatch = currentDomainPath.match(/\/src\/domains\/([^\/]+)/); const currentDomain = currentDomainMatch && currentDomainMatch[1] ? currentDomainMatch[1] : null; // Only return if it's a different domain if (targetDomain !== currentDomain) { return targetDomain; } } return null; }; export const scanDomainFiles = async ( fileSystem: FileSystem, domainPath: string ): Promise<string[]> => { const files: string[] = []; const scanDirectory = async (dirPath: string): Promise<void> => { try { const entries = await fileSystem.readdir(dirPath); for (const entry of entries) { const fullPath = path.join(dirPath, entry); const stats = await fileSystem.stat(fullPath); if (stats.isDirectory()) { await scanDirectory(fullPath); } else if (entry.endsWith('.ts') || entry.endsWith('.tsx')) { files.push(fullPath); } } } catch (error) { // Directory might not exist or be accessible } }; await scanDirectory(domainPath); return files; }; export const analyzeDomainDependencies = async ( fileSystem: FileSystem, domainPath: string, domainName: string ): Promise<{ dependencies: string[], files: string[] }> => { const files = await scanDomainFiles(fileSystem, domainPath); const dependencies = new Set<string>(); for (const filePath of files) { try { const content = await fileSystem.readFile(filePath, 'utf8') as string; const imports = extractImportStatements(content); for (const importStmt of imports) { if (importStmt.from) { const targetDomain = extractDomainFromImportPath(importStmt.from, filePath); if (targetDomain) { dependencies.add(targetDomain); } } } } catch (error) { // File might not be readable } } return { dependencies: Array.from(dependencies), files }; }; export const detectCircularDependencies = (nodes: DomainNode[]): string[][] => { const cycles: string[][] = []; const visited = new Set<string>(); const recursionStack = new Set<string>(); const dfs = (nodeId: string, path: string[]): void => { if (recursionStack.has(nodeId)) { // Found a cycle const cycleStart = path.indexOf(nodeId); if (cycleStart !== -1) { cycles.push(path.slice(cycleStart)); } return; } if (visited.has(nodeId)) { return; } visited.add(nodeId); recursionStack.add(nodeId); const node = nodes.find(n => n.id === nodeId); if (node) { for (const dependency of node.dependencies) { dfs(dependency, [...path, nodeId]); } } recursionStack.delete(nodeId); }; for (const node of nodes) { if (!visited.has(node.id)) { dfs(node.id, []); } } return cycles; }; const determineDomainType = (node: DomainNode, allNodes: DomainNode[]): string => { const domainName = node.name.toLowerCase(); // 1. Check explicit naming patterns if (domainName.includes('shared') || domainName.includes('common') || domainName.includes('lib')) { return 'shared'; } if (domainName.includes('core') || domainName.includes('kernel')) { return 'core'; } // 2. Analyze dependency patterns const dependentCount = node.dependents.length; const dependencyCount = node.dependencies.length; // Shared domains: used by many, depend on few if (dependentCount >= 3 && dependencyCount <= 1) { return 'shared'; } // Core domains: used by many, no dependencies on other domains (except shared/lib) const nonLibDependencies = node.dependencies.filter(dep => !dep.includes('lib') && !dep.includes('shared') && !dep.includes('common') ); if (dependentCount >= 2 && nonLibDependencies.length === 0) { return 'core'; } // 3. Check for typical core domain names const coreDomainNames = ['user', 'auth', 'account', 'identity', 'security']; if (coreDomainNames.some(coreName => domainName.includes(coreName))) { // Only if it's used by others and doesn't depend on business domains if (dependentCount > 0 && nonLibDependencies.length === 0) { return 'core'; } } // Default to feature return 'feature'; }; export const generateGraphData = async ( projectRoot: string, fileSystem?: FileSystem ): Promise<GraphData> => { const fs = fileSystem || createFileSystem(); const domainsPath = path.join(projectRoot, 'src', 'domains'); // Check if domains directory exists const domainsExist = await fs.exists(domainsPath); if (!domainsExist) { return { nodes: [], edges: [], circularDependencies: [], stats: { totalDomains: 0, totalDependencies: 0, circularDependencyCount: 0 } }; } // Get all domain directories const domainEntries = await fs.readdir(domainsPath); const domainDirs: string[] = []; for (const entry of domainEntries) { const entryPath = path.join(domainsPath, entry); const stats = await fs.stat(entryPath); if (stats.isDirectory()) { domainDirs.push(entry); } } // Analyze each domain const nodes: DomainNode[] = []; const dependencyMap = new Map<string, Set<string>>(); for (const domainName of domainDirs) { const domainPath = path.join(domainsPath, domainName); const analysis = await analyzeDomainDependencies(fs, domainPath, domainName); const node: DomainNode = { id: domainName, name: domainName, type: 'feature', // Will be determined later based on dependency analysis fileCount: analysis.files.length, dependencies: analysis.dependencies, dependents: [], // Will be filled later files: analysis.files }; nodes.push(node); dependencyMap.set(domainName, new Set(analysis.dependencies)); } // Calculate dependents for (const node of nodes) { for (const dependency of node.dependencies) { const dependentNode = nodes.find(n => n.id === dependency); if (dependentNode) { dependentNode.dependents.push(node.id); } } } // Determine domain types based on dependency patterns and naming for (const node of nodes) { node.type = determineDomainType(node, nodes); } // Generate edges const edges: DependencyEdge[] = []; const edgeMap = new Map<string, DependencyEdge>(); for (const node of nodes) { for (const dependency of node.dependencies) { const edgeKey = `${node.id}->${dependency}`; if (!edgeMap.has(edgeKey)) { edgeMap.set(edgeKey, { from: node.id, to: dependency, type: 'direct-import', count: 1, metadata: { files: [] } }); } else { const edge = edgeMap.get(edgeKey)!; edge.count++; } } } edges.push(...edgeMap.values()); // Detect circular dependencies const circularDependencies = detectCircularDependencies(nodes); return { nodes, edges, circularDependencies, stats: { totalDomains: nodes.length, totalDependencies: edges.length, circularDependencyCount: circularDependencies.length } }; };