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.

361 lines (304 loc) 9.56 kB
import fs from 'fs-extra'; import path from 'path'; /** * Interface representing a parsed entity field */ export interface EntityField { name: string; type: string; optional: boolean; description?: string; } /** * Interface representing a parsed entity */ export interface ParsedEntity { name: string; fields: EntityField[]; filePath: string; } /** * Interface representing a parsed method from ports/adapters */ export interface ParsedMethod { name: string; returnType: string; parameters: Array<{ name: string; type: string; optional: boolean; }>; isAsync: boolean; httpMethod?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; endpoint?: string; } /** * Interface representing parsed adapter information */ export interface ParsedAdapter { name: string; methods: ParsedMethod[]; baseUrl?: string; filePath: string; } /** * Interface representing parsed ports information */ export interface ParsedPorts { name: string; methods: ParsedMethod[]; filePath: string; } /** * Complete domain analysis result */ export interface DomainAnalysis { domainName: string; entities: ParsedEntity[]; ports: ParsedPorts[]; adapters: ParsedAdapter[]; primaryEntity?: ParsedEntity; } /** * Parse TypeScript interface or type definition */ const parseInterface = (content: string, interfaceName: string): EntityField[] => { const fields: EntityField[] = []; try { // Regex to find interface definition const interfacePattern = `(?:interface|type)\\s+${interfaceName}\\s*(?:extends[^{]*)?\\s*{([^}]+)}`; const interfaceRegex = new RegExp(interfacePattern, 'gs'); const match = interfaceRegex.exec(content); if (!match || !match[1]) return fields; const interfaceBody = match[1]; // Parse each field const fieldRegex = /^\s*(\w+)(\?)?\s*:\s*([^;,\n]+)(?:\s*;|\s*,|\s*$)/gm; let fieldMatch; while ((fieldMatch = fieldRegex.exec(interfaceBody)) !== null) { const [, name, optional, type] = fieldMatch; if (name && type) { fields.push({ name: name.trim(), type: type.trim(), optional: !!optional, }); } } } catch (error) { // Ignore parsing errors } return fields; }; /** * Parse method signatures from class or interface */ const parseMethods = (content: string, className?: string): ParsedMethod[] => { const methods: ParsedMethod[] = []; try { // If className is provided, extract only methods from that class/interface let targetContent = content; if (className) { const classPattern = `(?:class|interface)\\s+${className}[^{]*{([^}]+(?:{[^}]*}[^}]*)*)}`; const classRegex = new RegExp(classPattern, 'gs'); const classMatch = classRegex.exec(content); if (classMatch && classMatch[1]) { targetContent = classMatch[1]; } } // Parse method signatures - improved regex to handle multiline methods const methodRegex = /^\s*(?:async\s+)?(\w+)\s*\(\s*([^)]*)\s*\)\s*:\s*([^{;,\n]+)/gm; let methodMatch; while ((methodMatch = methodRegex.exec(targetContent)) !== null) { const [fullMatch, methodName, params, returnType] = methodMatch; if (!methodName || typeof params !== 'string' || !returnType) continue; const isAsync = fullMatch.includes('async') || returnType.includes('Promise'); // Parse parameters const parameters = params .split(',') .map(param => { const trimmed = param.trim(); if (!trimmed) return null; const paramMatch = trimmed.match(/^\s*(\w+)(\?)?\s*:\s*(.+)$/); if (!paramMatch || !paramMatch[1] || !paramMatch[3]) return null; return { name: paramMatch[1], type: paramMatch[3].trim(), optional: !!paramMatch[2], }; }) .filter(Boolean) as Array<{name: string; type: string; optional: boolean}>; // Try to infer HTTP method and endpoint from method name const httpMethod = inferHttpMethod(methodName); const endpoint = inferEndpoint(methodName); methods.push({ name: methodName, returnType: returnType.trim(), parameters, isAsync, httpMethod, endpoint, }); } } catch (error) { // Ignore parsing errors } return methods; }; /** * Infer HTTP method from method name */ const inferHttpMethod = (methodName: string): 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | undefined => { const name = methodName.toLowerCase(); if (name.startsWith('get') || name.startsWith('find') || name.startsWith('list') || name.startsWith('fetch')) { return 'GET'; } if (name.startsWith('create') || name.startsWith('add') || name.startsWith('post')) { return 'POST'; } if (name.startsWith('update') || name.startsWith('put') || name.startsWith('edit')) { return 'PUT'; } if (name.startsWith('delete') || name.startsWith('remove')) { return 'DELETE'; } if (name.startsWith('patch') || name.startsWith('modify')) { return 'PATCH'; } return undefined; }; /** * Infer API endpoint from method name */ const inferEndpoint = (methodName: string): string | undefined => { const name = methodName.toLowerCase(); // Common patterns if (name.includes('byid') || name.includes('by_id')) { return '/:id'; } if (name.includes('list') || name.includes('all') || name === 'get') { return ''; } if (name.includes('create') || name.includes('add')) { return ''; } if (name.includes('update') || name.includes('edit')) { return '/:id'; } if (name.includes('delete') || name.includes('remove')) { return '/:id'; } return undefined; }; /** * Extract base URL from adapter content */ const extractBaseUrl = (content: string, domainName: string): string => { // Look for baseUrl assignments const baseUrlRegex = /baseUrl\s*[:=]\s*['"`]([^'"`]+)['"`]/; const match = baseUrlRegex.exec(content); if (match && match[1]) { return match[1]; } // Default to /api/{domain} return `/api/${domainName}`; }; /** * Analyze entities files in a domain */ export const analyzeEntities = async (domainPath: string): Promise<ParsedEntity[]> => { const entities: ParsedEntity[] = []; const entitiesPath = path.join(domainPath, 'entities'); if (!await fs.pathExists(entitiesPath)) { return entities; } const files = await fs.readdir(entitiesPath); const tsFiles = files.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts')); for (const file of tsFiles) { const filePath = path.join(entitiesPath, file); const content = await fs.readFile(filePath, 'utf8'); const entityName = path.basename(file, '.ts'); const fields = parseInterface(content, entityName); if (fields.length > 0) { entities.push({ name: entityName, fields, filePath, }); } } return entities; }; /** * Analyze ports files in a domain */ export const analyzePorts = async (domainPath: string): Promise<ParsedPorts[]> => { const ports: ParsedPorts[] = []; const portsPath = path.join(domainPath, 'ports'); if (!await fs.pathExists(portsPath)) { return ports; } const files = await fs.readdir(portsPath); const tsFiles = files.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts')); for (const file of tsFiles) { const filePath = path.join(portsPath, file); const content = await fs.readFile(filePath, 'utf8'); const portsName = path.basename(file, '.ts'); const methods = parseMethods(content, portsName); if (methods.length > 0) { ports.push({ name: portsName, methods, filePath, }); } } return ports; }; /** * Analyze adapters files in a domain */ export const analyzeAdapters = async (domainPath: string, domainName: string): Promise<ParsedAdapter[]> => { const adapters: ParsedAdapter[] = []; const adaptersPath = path.join(domainPath, 'adapters'); if (!await fs.pathExists(adaptersPath)) { return adapters; } const files = await fs.readdir(adaptersPath); const tsFiles = files.filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts')); for (const file of tsFiles) { const filePath = path.join(adaptersPath, file); const content = await fs.readFile(filePath, 'utf8'); const adapterName = path.basename(file, '.ts'); const methods = parseMethods(content, adapterName); const baseUrl = extractBaseUrl(content, domainName); if (methods.length > 0) { adapters.push({ name: adapterName, methods, baseUrl, filePath, }); } } return adapters; }; /** * Perform complete analysis of a domain */ export const analyzeDomain = async (domainPath: string, domainName: string): Promise<DomainAnalysis> => { const entities = await analyzeEntities(domainPath); const ports = await analyzePorts(domainPath); const adapters = await analyzeAdapters(domainPath, domainName); // Try to identify the primary entity (usually matches domain name) const primaryEntity = entities.find(e => e.name.toLowerCase() === domainName.toLowerCase() || e.name.toLowerCase() === domainName.toLowerCase().slice(0, -1) // Remove potential 's' ) || entities[0]; return { domainName, entities, ports, adapters, primaryEntity, }; };