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