- Introduced a new TypeScript file (generate.ts) for generating Angular code with Clean Architecture from OpenAPI/Swagger specifications. - Implemented a CLI using Commander.js for user input and options. - Added functions for analyzing Swagger files, generating code, organizing files, and creating a report. - Integrated Mustache templates for generating models, repositories, use cases, and mappers. - Created a build process with TypeScript and updated package.json to include build scripts and dependencies. - Added TypeScript configuration (tsconfig.json) for compiling the TypeScript code. - Updated the main entry point in package.json to point to the compiled JavaScript file in the dist directory.
596 lines
20 KiB
JavaScript
Executable File
596 lines
20 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
import { execSync } from 'child_process';
|
||
import fs from 'fs-extra';
|
||
import path from 'path';
|
||
import yaml from 'js-yaml';
|
||
import mustache from 'mustache';
|
||
import { program } from 'commander';
|
||
|
||
// Desactivar escape HTML para que los literales < y > generen tipos genéricos válidos de TS.
|
||
(mustache as any).escape = function(text: string): string { return text; };
|
||
|
||
// Colores para console (sin dependencias externas)
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
bright: '\x1b[1m',
|
||
green: '\x1b[32m',
|
||
blue: '\x1b[34m',
|
||
yellow: '\x1b[33m',
|
||
red: '\x1b[31m',
|
||
cyan: '\x1b[36m'
|
||
};
|
||
|
||
type Color = keyof typeof colors;
|
||
|
||
function log(message: string, color: Color = 'reset'): void {
|
||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||
}
|
||
|
||
function logSuccess(message: string): void {
|
||
log(`✅ ${message}`, 'green');
|
||
}
|
||
|
||
function logInfo(message: string): void {
|
||
log(`ℹ️ ${message}`, 'blue');
|
||
}
|
||
|
||
function logWarning(message: string): void {
|
||
log(`⚠️ ${message}`, 'yellow');
|
||
}
|
||
|
||
function logError(message: string): void {
|
||
log(`❌ ${message}`, 'red');
|
||
}
|
||
|
||
function logStep(message: string): void {
|
||
log(`\n🚀 ${message}`, 'cyan');
|
||
}
|
||
|
||
// Configuración del CLI
|
||
program
|
||
.name('generate-clean-arch')
|
||
.description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger')
|
||
.version('1.0.0')
|
||
.option('-i, --input <file>', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml')
|
||
.option('-o, --output <dir>', 'Directorio de salida', './src/app')
|
||
.option('-t, --templates <dir>', 'Directorio de templates personalizados', './templates')
|
||
.option('--skip-install', 'No instalar dependencias')
|
||
.option('--dry-run', 'Simular sin generar archivos')
|
||
.parse(process.argv);
|
||
|
||
interface CliOptions {
|
||
input: string;
|
||
output: string;
|
||
templates: string;
|
||
skipInstall?: boolean;
|
||
dryRun?: boolean;
|
||
}
|
||
|
||
const options = program.opts() as CliOptions;
|
||
|
||
// Validar que existe openapi-generator-cli
|
||
function checkOpenApiGenerator(): boolean {
|
||
try {
|
||
execSync('openapi-generator-cli version', { stdio: 'ignore' });
|
||
return true;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Instalar openapi-generator-cli
|
||
function installOpenApiGenerator(): void {
|
||
logStep('Instalando @openapitools/openapi-generator-cli...');
|
||
try {
|
||
execSync('npm install -g @openapitools/openapi-generator-cli', { stdio: 'inherit' });
|
||
logSuccess('OpenAPI Generator CLI instalado correctamente');
|
||
} catch (error) {
|
||
logError('Error al instalar OpenAPI Generator CLI');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// Crear estructura de directorios
|
||
function createDirectoryStructure(baseDir: string): void {
|
||
const dirs = [
|
||
path.join(baseDir, 'data/dtos'),
|
||
path.join(baseDir, 'data/repositories'),
|
||
path.join(baseDir, 'data/mappers'),
|
||
path.join(baseDir, 'domain/repositories'),
|
||
path.join(baseDir, 'domain/use-cases'),
|
||
path.join(baseDir, 'di/repositories'),
|
||
path.join(baseDir, 'di/use-cases'),
|
||
path.join(baseDir, 'entities/models')
|
||
];
|
||
|
||
dirs.forEach(dir => {
|
||
fs.ensureDirSync(dir);
|
||
});
|
||
|
||
logSuccess('Estructura de directorios creada');
|
||
}
|
||
|
||
interface SwaggerAnalysis {
|
||
tags: any[];
|
||
paths: Record<string, any>;
|
||
swagger: any;
|
||
}
|
||
|
||
// Analizar el swagger para extraer tags y dominios
|
||
function analyzeSwagger(swaggerFile: string): SwaggerAnalysis {
|
||
logStep('Analizando archivo OpenAPI...');
|
||
|
||
try {
|
||
const fileContent = fs.readFileSync(swaggerFile, 'utf8');
|
||
const swagger = yaml.load(fileContent) as any;
|
||
|
||
const tags = swagger.tags || [];
|
||
const paths = swagger.paths || {};
|
||
|
||
logInfo(`Encontrados ${tags.length} tags en el API`);
|
||
logInfo(`Encontrados ${Object.keys(paths).length} endpoints`);
|
||
|
||
tags.forEach((tag: any) => {
|
||
logInfo(` - ${tag.name}: ${tag.description || 'Sin descripción'}`);
|
||
});
|
||
|
||
return { tags, paths, swagger };
|
||
} catch (error: any) {
|
||
logError(`Error al leer el archivo Swagger: ${error.message}`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// Generar código con OpenAPI Generator
|
||
function generateCode(swaggerFile: string, templatesDir: string): string {
|
||
logStep('Generando código desde OpenAPI...');
|
||
|
||
const tempDir = path.join(process.cwd(), '.temp-generated');
|
||
|
||
// Limpiar directorio temporal
|
||
if (fs.existsSync(tempDir)) {
|
||
fs.removeSync(tempDir);
|
||
}
|
||
|
||
try {
|
||
const command = `openapi-generator-cli generate \
|
||
-i "${swaggerFile}" \
|
||
-g typescript-angular \
|
||
--global-property models \
|
||
-t "${templatesDir}" \
|
||
-o "${tempDir}" \
|
||
--additional-properties=ngVersion=17.0.0,modelFileSuffix=.dto`;
|
||
|
||
execSync(command, { stdio: 'inherit' });
|
||
logSuccess('Código generado correctamente');
|
||
|
||
return tempDir;
|
||
} catch (error) {
|
||
logError('Error al generar código');
|
||
if (fs.existsSync(tempDir)) {
|
||
fs.removeSync(tempDir);
|
||
}
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// Organizar archivos según Clean Architecture (DTOs)
|
||
function organizeFiles(tempDir: string, outputDir: string): void {
|
||
logStep('Organizando archivos DTO generados...');
|
||
|
||
const sourceDir = path.join(tempDir, 'model');
|
||
const destDir = path.join(outputDir, 'data/dtos');
|
||
let filesMoved = 0;
|
||
|
||
if (fs.existsSync(sourceDir)) {
|
||
fs.ensureDirSync(destDir);
|
||
|
||
const files = fs.readdirSync(sourceDir).filter(file => file.endsWith('.dto.ts'));
|
||
|
||
files.forEach(file => {
|
||
const sourcePath = path.join(sourceDir, file);
|
||
const destPath = path.join(destDir, file);
|
||
|
||
fs.copySync(sourcePath, destPath);
|
||
filesMoved++;
|
||
logInfo(` ${file} → ${path.relative(process.cwd(), destPath)}`);
|
||
});
|
||
}
|
||
|
||
logSuccess(`${filesMoved} DTOs movidos correctamente`);
|
||
}
|
||
|
||
// Utilidad para mapear tipos OpenAPI elementales a TypeScript
|
||
function mapSwaggerTypeToTs(type: string): string {
|
||
const typeMap: Record<string, string> = {
|
||
'integer': 'number',
|
||
'string': 'string',
|
||
'boolean': 'boolean',
|
||
'number': 'number',
|
||
'array': 'Array<any>',
|
||
'object': 'any'
|
||
};
|
||
return typeMap[type] || 'any';
|
||
}
|
||
|
||
interface GeneratedCount {
|
||
models: number;
|
||
repositories: number;
|
||
mappers: number;
|
||
useCases: number;
|
||
providers: number;
|
||
}
|
||
|
||
// Generar Clean Architecture con Mustache
|
||
function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, templatesDir: string): GeneratedCount {
|
||
logStep('Generando artefactos de Clean Architecture usando Mustache...');
|
||
let generatedCount: GeneratedCount = { models: 0, repositories: 0, mappers: 0, useCases: 0, providers: 0 };
|
||
|
||
const schemas = analysis.swagger.components?.schemas || {};
|
||
|
||
// 1. Generar Modelos, Entidades y Mappers a partir de Schemas
|
||
Object.keys(schemas).forEach(schemaName => {
|
||
// Sanitizar nombres base para que coincidan con cómo OpenAPI los emite (sin Dto duplicado)
|
||
const baseName = schemaName.replace(/Dto$/, '');
|
||
|
||
// variables para model
|
||
const rawProperties = schemas[schemaName].properties || {};
|
||
const requiredProps: string[] = schemas[schemaName].required || [];
|
||
|
||
const varsMap = Object.keys(rawProperties).map(k => {
|
||
let tsType = mapSwaggerTypeToTs(rawProperties[k].type);
|
||
if (rawProperties[k].$ref) {
|
||
// Simple extración del tipo de la ref
|
||
tsType = rawProperties[k].$ref.split('/').pop() || 'any';
|
||
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
|
||
tsType = `Array<${rawProperties[k].items.$ref.split('/').pop()}>`;
|
||
}
|
||
return {
|
||
name: k,
|
||
dataType: tsType,
|
||
description: rawProperties[k].description || '',
|
||
required: requiredProps.includes(k)
|
||
};
|
||
});
|
||
|
||
const modelViewData = {
|
||
models: [{
|
||
model: {
|
||
classname: baseName,
|
||
classFilename: baseName.toLowerCase(),
|
||
classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1),
|
||
description: schemas[schemaName].description || '',
|
||
vars: varsMap
|
||
}
|
||
}],
|
||
// Para plantillas que esperan allModels o importaciones (mapper)
|
||
allModels: [{ model: { vars: varsMap } }]
|
||
};
|
||
|
||
// Y para mapper.mustache, que además pide apiInfo
|
||
const mapperViewData = {
|
||
...modelViewData,
|
||
apiInfo: {
|
||
apis: [{
|
||
operations: {
|
||
classname: baseName,
|
||
classFilename: baseName.toLowerCase(),
|
||
classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1),
|
||
}
|
||
}]
|
||
}
|
||
};
|
||
|
||
// Model (Entities)
|
||
const modelTemplatePath = path.join(templatesDir, 'model-entity.mustache');
|
||
if (fs.existsSync(modelTemplatePath)) {
|
||
const template = fs.readFileSync(modelTemplatePath, 'utf8');
|
||
const output = mustache.render(template, modelViewData);
|
||
const destPath = path.join(outputDir, 'entities/models', `${baseName.toLowerCase()}.model.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.models++;
|
||
logInfo(` ${baseName.toLowerCase()}.model.ts → ${path.relative(process.cwd(), destPath)}`);
|
||
}
|
||
|
||
// Mapper
|
||
const mapperTemplatePath = path.join(templatesDir, 'mapper.mustache');
|
||
if (fs.existsSync(mapperTemplatePath)) {
|
||
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
|
||
const output = mustache.render(template, mapperViewData);
|
||
const destPath = path.join(outputDir, 'data/mappers', `${baseName.toLowerCase()}.mapper.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.mappers++;
|
||
}
|
||
});
|
||
|
||
// 2. Generar Casos de Uso y Repositorios a partir de Paths/Tags
|
||
const tagsMap: Record<string, any[]> = {};
|
||
|
||
// Agrupar operaciones por Tag
|
||
Object.keys(analysis.paths).forEach(pathKey => {
|
||
const pathObj = analysis.paths[pathKey];
|
||
Object.keys(pathObj).forEach(method => {
|
||
const op = pathObj[method];
|
||
if (op.tags && op.tags.length > 0) {
|
||
const tag = op.tags[0]; // Usamos el primer tag
|
||
if (!tagsMap[tag]) tagsMap[tag] = [];
|
||
|
||
// Parsear parámetros
|
||
const allParams = (op.parameters || []).map((p: any) => ({
|
||
paramName: p.name,
|
||
dataType: mapSwaggerTypeToTs(p.schema?.type),
|
||
description: p.description || '',
|
||
required: p.required
|
||
}));
|
||
|
||
// Añadir body como parámetro si existe
|
||
if (op.requestBody) {
|
||
let bodyType = 'any';
|
||
const content = op.requestBody.content?.['application/json']?.schema;
|
||
if (content) {
|
||
if (content.$ref) bodyType = content.$ref.split('/').pop() || 'any';
|
||
else if (content.type) bodyType = mapSwaggerTypeToTs(content.type);
|
||
}
|
||
allParams.push({
|
||
paramName: 'body',
|
||
dataType: bodyType,
|
||
description: op.requestBody.description || '',
|
||
required: true
|
||
});
|
||
}
|
||
|
||
// Parsear respuestas
|
||
let returnType = 'void';
|
||
let returnBaseType = 'void';
|
||
let isListContainer = false;
|
||
const responseSchema = op.responses?.['200']?.content?.['application/json']?.schema;
|
||
if (responseSchema) {
|
||
if (responseSchema.$ref) {
|
||
returnType = responseSchema.$ref.split('/').pop() || 'any';
|
||
returnBaseType = returnType;
|
||
} else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
||
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'any';
|
||
returnType = `Array<${returnBaseType}>`;
|
||
isListContainer = true;
|
||
}
|
||
}
|
||
|
||
tagsMap[tag].push({
|
||
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
||
summary: op.summary || '',
|
||
notes: op.description || '',
|
||
httpMethod: method.toLowerCase(),
|
||
path: pathKey,
|
||
allParams: allParams.map((p: any, i: number) => ({ ...p, '-last': i === allParams.length - 1 })),
|
||
hasQueryParams: (op.parameters || []).some((p: any) => p.in === 'query'),
|
||
queryParams: (op.parameters || []).filter((p: any) => p.in === 'query').map((p: any, i: number, arr: any[]) => ({ paramName: p.name, '-last': i === arr.length - 1 })),
|
||
hasBodyParam: !!op.requestBody,
|
||
bodyParam: 'body',
|
||
returnType: returnType !== 'void' ? returnType : false,
|
||
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
|
||
isListContainer: isListContainer,
|
||
vendorExtensions: {}
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
// Generar por cada Tag
|
||
Object.keys(tagsMap).forEach(tag => {
|
||
// Buscar si ese tag cruza con alguna entidad para importarla
|
||
const imports: any[] = [];
|
||
Object.keys(schemas).forEach(s => {
|
||
// Import heurístico burdo
|
||
if (tagsMap[tag].some((op: any) => op.returnType === s || op.returnType === `Array<${s}>`)) {
|
||
imports.push({ classname: s, classFilename: s.toLowerCase(), classVarName: s.charAt(0).toLowerCase() + s.slice(1) });
|
||
}
|
||
});
|
||
|
||
const apiViewData = {
|
||
apiInfo: {
|
||
apis: [{
|
||
operations: {
|
||
classname: tag,
|
||
classFilename: tag.toLowerCase(),
|
||
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
||
operation: tagsMap[tag],
|
||
imports: imports
|
||
}
|
||
}]
|
||
}
|
||
};
|
||
|
||
// Use Case Contract
|
||
const ucContractPath = path.join(templatesDir, 'api.use-cases.contract.mustache');
|
||
if (fs.existsSync(ucContractPath)) {
|
||
const template = fs.readFileSync(ucContractPath, 'utf8');
|
||
const output = mustache.render(template, apiViewData);
|
||
const destPath = path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.contract.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.useCases++;
|
||
}
|
||
|
||
// Use Case Impl
|
||
const ucImplPath = path.join(templatesDir, 'api.use-cases.impl.mustache');
|
||
if (fs.existsSync(ucImplPath)) {
|
||
const template = fs.readFileSync(ucImplPath, 'utf8');
|
||
const output = mustache.render(template, apiViewData);
|
||
const destPath = path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.impl.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.useCases++;
|
||
}
|
||
|
||
// Repository Contract
|
||
const repoContractPath = path.join(templatesDir, 'api.repository.contract.mustache');
|
||
if (fs.existsSync(repoContractPath)) {
|
||
const template = fs.readFileSync(repoContractPath, 'utf8');
|
||
const output = mustache.render(template, apiViewData);
|
||
const destPath = path.join(outputDir, 'domain/repositories', `${tag.toLowerCase()}.repository.contract.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.repositories++;
|
||
}
|
||
|
||
// Repository Impl
|
||
const repoImplPath = path.join(templatesDir, 'api.repository.impl.mustache');
|
||
if (fs.existsSync(repoImplPath)) {
|
||
const template = fs.readFileSync(repoImplPath, 'utf8');
|
||
const output = mustache.render(template, apiViewData);
|
||
const destPath = path.join(outputDir, 'data/repositories', `${tag.toLowerCase()}.repository.impl.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.repositories++;
|
||
}
|
||
|
||
// Use Case Provider
|
||
const ucProviderPath = path.join(templatesDir, 'use-cases.provider.mustache');
|
||
if (fs.existsSync(ucProviderPath)) {
|
||
const template = fs.readFileSync(ucProviderPath, 'utf8');
|
||
const output = mustache.render(template, apiViewData);
|
||
const destPath = path.join(outputDir, 'di/use-cases', `${tag.toLowerCase()}.use-cases.provider.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.providers++;
|
||
}
|
||
|
||
// Repository Provider
|
||
const repoProviderPath = path.join(templatesDir, 'repository.provider.mustache');
|
||
if (fs.existsSync(repoProviderPath)) {
|
||
const template = fs.readFileSync(repoProviderPath, 'utf8');
|
||
const output = mustache.render(template, apiViewData);
|
||
const destPath = path.join(outputDir, 'di/repositories', `${tag.toLowerCase()}.repository.provider.ts`);
|
||
fs.writeFileSync(destPath, output);
|
||
generatedCount.providers++;
|
||
}
|
||
});
|
||
|
||
logSuccess(`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers generados con Mustache`);
|
||
return generatedCount;
|
||
}
|
||
|
||
// Limpiar directorio temporal
|
||
function cleanup(tempDir: string): void {
|
||
if (fs.existsSync(tempDir)) {
|
||
fs.removeSync(tempDir);
|
||
logInfo('Archivos temporales eliminados');
|
||
}
|
||
}
|
||
|
||
interface GenerationReport {
|
||
timestamp: string;
|
||
tags: number;
|
||
endpoints: number;
|
||
outputDirectory: string;
|
||
structure: {
|
||
dtos: number;
|
||
repositories: number;
|
||
mappers: number;
|
||
useCases: number;
|
||
providers: number;
|
||
};
|
||
}
|
||
|
||
// Generar reporte
|
||
function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport {
|
||
logStep('Generando reporte de generación...');
|
||
|
||
const report: GenerationReport = {
|
||
timestamp: new Date().toISOString(),
|
||
tags: analysis.tags.length,
|
||
endpoints: Object.keys(analysis.paths).length,
|
||
outputDirectory: outputDir,
|
||
structure: {
|
||
dtos: fs.readdirSync(path.join(outputDir, 'data/dtos')).length,
|
||
repositories: fs.readdirSync(path.join(outputDir, 'data/repositories')).length,
|
||
mappers: fs.readdirSync(path.join(outputDir, 'data/mappers')).length,
|
||
useCases: fs.readdirSync(path.join(outputDir, 'domain/use-cases')).length,
|
||
providers: fs.readdirSync(path.join(outputDir, 'di/repositories')).length + fs.readdirSync(path.join(outputDir, 'di/use-cases')).length
|
||
}
|
||
};
|
||
|
||
const reportPath = path.join(process.cwd(), 'generation-report.json');
|
||
fs.writeJsonSync(reportPath, report, { spaces: 2 });
|
||
|
||
logSuccess(`Reporte guardado en: ${reportPath}`);
|
||
|
||
return report;
|
||
}
|
||
|
||
// Función principal
|
||
async function main(): Promise<void> {
|
||
console.log('\n' + '='.repeat(60));
|
||
log(' OpenAPI Clean Architecture Generator', 'bright');
|
||
log(' Angular + Clean Architecture Code Generator', 'cyan');
|
||
console.log('='.repeat(60) + '\n');
|
||
|
||
// Validar archivo de entrada
|
||
if (!fs.existsSync(options.input)) {
|
||
logError(`Archivo no encontrado: ${options.input}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
logInfo(`Archivo de entrada: ${options.input}`);
|
||
logInfo(`Directorio de salida: ${options.output}`);
|
||
logInfo(`Templates: ${options.templates}`);
|
||
|
||
if (options.dryRun) {
|
||
logWarning('Modo DRY RUN - No se generarán archivos');
|
||
}
|
||
|
||
// Verificar/Instalar OpenAPI Generator
|
||
if (!checkOpenApiGenerator()) {
|
||
logWarning('OpenAPI Generator CLI no encontrado');
|
||
if (!options.skipInstall) {
|
||
installOpenApiGenerator();
|
||
} else {
|
||
logError('Instala openapi-generator-cli con: npm install -g @openapitools/openapi-generator-cli');
|
||
process.exit(1);
|
||
}
|
||
} else {
|
||
logSuccess('OpenAPI Generator CLI encontrado');
|
||
}
|
||
|
||
// Analizar Swagger
|
||
const analysis = analyzeSwagger(options.input);
|
||
|
||
if (options.dryRun) {
|
||
logInfo('Finalizando en modo DRY RUN');
|
||
return;
|
||
}
|
||
|
||
// Crear estructura de directorios
|
||
createDirectoryStructure(options.output);
|
||
|
||
// Generar código
|
||
const tempDir = generateCode(options.input, options.templates);
|
||
|
||
// Organizar archivos
|
||
organizeFiles(tempDir, options.output);
|
||
|
||
// Crear componentes Clean Architecture con nuestro script de Mustache
|
||
generateCleanArchitecture(analysis, options.output, options.templates);
|
||
|
||
// Limpiar
|
||
cleanup(tempDir);
|
||
|
||
// Generar reporte
|
||
const report = generateReport(options.output, analysis);
|
||
|
||
// Resumen final
|
||
console.log('\n' + '='.repeat(60));
|
||
log(' ✨ Generación completada con éxito', 'green');
|
||
console.log('='.repeat(60));
|
||
console.log(`\n📊 Resumen:`);
|
||
console.log(` - DTOs generados: ${report.structure.dtos}`);
|
||
console.log(` - Repositories: ${report.structure.repositories}`);
|
||
console.log(` - Mappers: ${report.structure.mappers}`);
|
||
console.log(` - Use Cases: ${report.structure.useCases}`);
|
||
console.log(` - Providers: ${report.structure.providers}`);
|
||
console.log(`\n📁 Archivos generados en: ${colors.cyan}${options.output}${colors.reset}\n`);
|
||
}
|
||
|
||
// Ejecutar
|
||
main().catch((error: any) => {
|
||
logError(`Error fatal: ${error.message}`);
|
||
console.error(error);
|
||
process.exit(1);
|
||
});
|