diff --git a/.gitignore b/.gitignore index 1d47a97..636062e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ package-lock.json # Archivos temporales de generación .temp-generated/ temp-generated/ +dist/ # Reportes generation-report.json diff --git a/dist/generate.js b/dist/generate.js deleted file mode 100644 index d4d0d13..0000000 --- a/dist/generate.js +++ /dev/null @@ -1,523 +0,0 @@ -#!/usr/bin/env node -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const child_process_1 = require("child_process"); -const fs_extra_1 = __importDefault(require("fs-extra")); -const path_1 = __importDefault(require("path")); -const js_yaml_1 = __importDefault(require("js-yaml")); -const mustache_1 = __importDefault(require("mustache")); -const commander_1 = require("commander"); -// Desactivar escape HTML para que los literales < y > generen tipos genéricos válidos de TS. -mustache_1.default.escape = function (text) { - 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' -}; -function log(message, color = 'reset') { - console.log(`${colors[color]}${message}${colors.reset}`); -} -function logSuccess(message) { - log(`✅ ${message}`, 'green'); -} -function logInfo(message) { - log(`ℹ️ ${message}`, 'blue'); -} -function logWarning(message) { - log(`⚠️ ${message}`, 'yellow'); -} -function logError(message) { - log(`❌ ${message}`, 'red'); -} -function logStep(message) { - log(`\n🚀 ${message}`, 'cyan'); -} -// Configuración del CLI -commander_1.program - .name('generate-clean-arch') - .description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger') - .version('1.0.0') - .option('-i, --input ', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml') - .option('-o, --output ', 'Directorio de salida', './src/app') - .option('-t, --templates ', 'Directorio de templates personalizados', './templates') - .option('--skip-install', 'No instalar dependencias') - .option('--dry-run', 'Simular sin generar archivos') - .parse(process.argv); -const options = commander_1.program.opts(); -// Validar que existe openapi-generator-cli -function checkOpenApiGenerator() { - try { - (0, child_process_1.execSync)('openapi-generator-cli version', { stdio: 'ignore' }); - return true; - } - catch (_error) { - return false; - } -} -// Instalar openapi-generator-cli -function installOpenApiGenerator() { - logStep('Instalando @openapitools/openapi-generator-cli...'); - try { - (0, child_process_1.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) { - const dirs = [ - path_1.default.join(baseDir, 'data/dtos'), - path_1.default.join(baseDir, 'data/repositories'), - path_1.default.join(baseDir, 'data/mappers'), - path_1.default.join(baseDir, 'domain/repositories'), - path_1.default.join(baseDir, 'domain/use-cases'), - path_1.default.join(baseDir, 'di/repositories'), - path_1.default.join(baseDir, 'di/use-cases'), - path_1.default.join(baseDir, 'entities/models') - ]; - dirs.forEach((dir) => { - fs_extra_1.default.ensureDirSync(dir); - }); - logSuccess('Estructura de directorios creada'); -} -// Analizar el swagger para extraer tags y dominios -function analyzeSwagger(swaggerFile) { - logStep('Analizando archivo OpenAPI...'); - try { - const fileContent = fs_extra_1.default.readFileSync(swaggerFile, 'utf8'); - const swagger = js_yaml_1.default.load(fileContent); - const tags = Array.isArray(swagger.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) => { - const t = tag; - logInfo(` - ${t.name}: ${t.description || 'Sin descripción'}`); - }); - return { tags, paths, swagger }; - } - catch (error) { - const err = error; - logError(`Error al leer el archivo Swagger: ${err.message}`); - process.exit(1); - } -} -// Generar código con OpenAPI Generator -function generateCode(swaggerFile, templatesDir) { - logStep('Generando código desde OpenAPI...'); - const tempDir = path_1.default.join(process.cwd(), '.temp-generated'); - // Limpiar directorio temporal - if (fs_extra_1.default.existsSync(tempDir)) { - fs_extra_1.default.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`; - (0, child_process_1.execSync)(command, { stdio: 'inherit' }); - logSuccess('Código generado correctamente'); - return tempDir; - } - catch (_error) { - logError('Error al generar código'); - if (fs_extra_1.default.existsSync(tempDir)) { - fs_extra_1.default.removeSync(tempDir); - } - process.exit(1); - } -} -// Organizar archivos según Clean Architecture (DTOs) -function organizeFiles(tempDir, outputDir) { - logStep('Organizando archivos DTO generados...'); - const sourceDir = path_1.default.join(tempDir, 'model'); - const destDir = path_1.default.join(outputDir, 'data/dtos'); - let filesMoved = 0; - if (fs_extra_1.default.existsSync(sourceDir)) { - fs_extra_1.default.ensureDirSync(destDir); - const files = fs_extra_1.default.readdirSync(sourceDir).filter((file) => file.endsWith('.dto.ts')); - files.forEach((file) => { - const sourcePath = path_1.default.join(sourceDir, file); - const destPath = path_1.default.join(destDir, file); - fs_extra_1.default.copySync(sourcePath, destPath); - filesMoved++; - logInfo(` ${file} → ${path_1.default.relative(process.cwd(), destPath)}`); - }); - } - logSuccess(`${filesMoved} DTOs movidos correctamente`); -} -// Utilidad para mapear tipos OpenAPI elementales a TypeScript -function mapSwaggerTypeToTs(type) { - if (!type) - return 'unknown'; - const typeMap = { - integer: 'number', - string: 'string', - boolean: 'boolean', - number: 'number', - array: 'Array', - object: 'unknown' - }; - return typeMap[type] || 'unknown'; -} -// Generar Clean Architecture con Mustache -function generateCleanArchitecture(analysis, outputDir, templatesDir) { - logStep('Generando artefactos de Clean Architecture usando Mustache...'); - const 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 schemaObj = schemas[schemaName]; - const rawProperties = schemaObj.properties || {}; - const requiredProps = schemaObj.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() || 'unknown'; - } - 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: schemaObj.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_1.default.join(templatesDir, 'model-entity.mustache'); - if (fs_extra_1.default.existsSync(modelTemplatePath)) { - const template = fs_extra_1.default.readFileSync(modelTemplatePath, 'utf8'); - const output = mustache_1.default.render(template, modelViewData); - const destPath = path_1.default.join(outputDir, 'entities/models', `${baseName.toLowerCase()}.model.ts`); - fs_extra_1.default.writeFileSync(destPath, output); - generatedCount.models++; - logInfo(` ${baseName.toLowerCase()}.model.ts → ${path_1.default.relative(process.cwd(), destPath)}`); - } - // Mapper - const mapperTemplatePath = path_1.default.join(templatesDir, 'mapper.mustache'); - if (fs_extra_1.default.existsSync(mapperTemplatePath)) { - const template = fs_extra_1.default.readFileSync(mapperTemplatePath, 'utf8'); - const output = mustache_1.default.render(template, mapperViewData); - const destPath = path_1.default.join(outputDir, 'data/mappers', `${baseName.toLowerCase()}.mapper.ts`); - fs_extra_1.default.writeFileSync(destPath, output); - generatedCount.mappers++; - } - }); - // 2. Generar Casos de Uso y Repositorios a partir de Paths/Tags - const tagsMap = {}; - // 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) => ({ - 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 = 'unknown'; - const content = op.requestBody.content?.['application/json']?.schema; - if (content) { - if (content.$ref) - bodyType = content.$ref.split('/').pop() || 'unknown'; - 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() || 'unknown'; - returnBaseType = returnType; - } - else if (responseSchema.type === 'array' && responseSchema.items?.$ref) { - returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown'; - 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, i) => ({ ...p, '-last': i === allParams.length - 1 })), - hasQueryParams: (op.parameters || []).some((p) => p.in === 'query'), - queryParams: (op.parameters || []) - .filter((p) => p.in === 'query') - .map((p, i, arr) => ({ - 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 = []; - Object.keys(schemas).forEach((s) => { - // Import heurístico burdo - if (tagsMap[tag].some((op) => 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_1.default.join(templatesDir, 'api.use-cases.contract.mustache'); - if (fs_extra_1.default.existsSync(ucContractPath)) { - const template = fs_extra_1.default.readFileSync(ucContractPath, 'utf8'); - const output = mustache_1.default.render(template, apiViewData); - const destPath = path_1.default.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.contract.ts`); - fs_extra_1.default.writeFileSync(destPath, output); - generatedCount.useCases++; - } - // Use Case Impl - const ucImplPath = path_1.default.join(templatesDir, 'api.use-cases.impl.mustache'); - if (fs_extra_1.default.existsSync(ucImplPath)) { - const template = fs_extra_1.default.readFileSync(ucImplPath, 'utf8'); - const output = mustache_1.default.render(template, apiViewData); - const destPath = path_1.default.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.impl.ts`); - fs_extra_1.default.writeFileSync(destPath, output); - generatedCount.useCases++; - } - // Repository Contract - const repoContractPath = path_1.default.join(templatesDir, 'api.repository.contract.mustache'); - if (fs_extra_1.default.existsSync(repoContractPath)) { - const template = fs_extra_1.default.readFileSync(repoContractPath, 'utf8'); - const output = mustache_1.default.render(template, apiViewData); - const destPath = path_1.default.join(outputDir, 'domain/repositories', `${tag.toLowerCase()}.repository.contract.ts`); - fs_extra_1.default.writeFileSync(destPath, output); - generatedCount.repositories++; - } - // Repository Impl - const repoImplPath = path_1.default.join(templatesDir, 'api.repository.impl.mustache'); - if (fs_extra_1.default.existsSync(repoImplPath)) { - const template = fs_extra_1.default.readFileSync(repoImplPath, 'utf8'); - const output = mustache_1.default.render(template, apiViewData); - const destPath = path_1.default.join(outputDir, 'data/repositories', `${tag.toLowerCase()}.repository.impl.ts`); - fs_extra_1.default.writeFileSync(destPath, output); - generatedCount.repositories++; - } - // Use Case Provider - const ucProviderPath = path_1.default.join(templatesDir, 'use-cases.provider.mustache'); - if (fs_extra_1.default.existsSync(ucProviderPath)) { - const template = fs_extra_1.default.readFileSync(ucProviderPath, 'utf8'); - const output = mustache_1.default.render(template, apiViewData); - const destPath = path_1.default.join(outputDir, 'di/use-cases', `${tag.toLowerCase()}.use-cases.provider.ts`); - fs_extra_1.default.writeFileSync(destPath, output); - generatedCount.providers++; - } - // Repository Provider - const repoProviderPath = path_1.default.join(templatesDir, 'repository.provider.mustache'); - if (fs_extra_1.default.existsSync(repoProviderPath)) { - const template = fs_extra_1.default.readFileSync(repoProviderPath, 'utf8'); - const output = mustache_1.default.render(template, apiViewData); - const destPath = path_1.default.join(outputDir, 'di/repositories', `${tag.toLowerCase()}.repository.provider.ts`); - fs_extra_1.default.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) { - if (fs_extra_1.default.existsSync(tempDir)) { - fs_extra_1.default.removeSync(tempDir); - logInfo('Archivos temporales eliminados'); - } -} -// Generar reporte -function generateReport(outputDir, analysis) { - logStep('Generando reporte de generación...'); - const report = { - timestamp: new Date().toISOString(), - tags: analysis.tags.length, - endpoints: Object.keys(analysis.paths).length, - outputDirectory: outputDir, - structure: { - dtos: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/dtos')).length, - repositories: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/repositories')).length, - mappers: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/mappers')).length, - useCases: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'domain/use-cases')).length, - providers: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'di/repositories')).length + - fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'di/use-cases')).length - } - }; - const reportPath = path_1.default.join(process.cwd(), 'generation-report.json'); - fs_extra_1.default.writeJsonSync(reportPath, report, { spaces: 2 }); - logSuccess(`Reporte guardado en: ${reportPath}`); - return report; -} -// Función principal -async function main() { - 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_extra_1.default.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) => { - const err = error; - logError(`Error fatal: ${err.message}`); - console.error(error); - process.exit(1); -});