From cd00eb39ca02807f03ed90804835869260d73b73 Mon Sep 17 00:00:00 2001 From: didavila Date: Mon, 23 Mar 2026 17:01:22 +0100 Subject: [PATCH 1/3] feat: add TypeScript-based OpenAPI Clean Architecture generator - 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. --- README.md | 23 +- dist/generate.js | 492 +++++++++++++++++++++++++++++++++++++ generate.js => generate.ts | 126 ++++++---- package.json | 17 +- tsconfig.json | 17 ++ 5 files changed, 620 insertions(+), 55 deletions(-) create mode 100644 dist/generate.js rename generate.js => generate.ts (84%) create mode 100644 tsconfig.json diff --git a/README.md b/README.md index 0d6b1b5..ed651e5 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,20 @@ npm run setup ### Comando básico ```bash -node generate.js -i swagger.yaml +# Versión compilada +npm run generate -- -i swagger.yaml + +# Versión desarrollo (ts-node) +npm run generate:dev -- -i swagger.yaml + +# Versión link global (si hiciste npm link) +generate-clean-arch -i swagger.yaml ``` ### Opciones disponibles ```bash -node generate.js [opciones] +npm run generate -- [opciones] Opciones: -V, --version Mostrar versión @@ -45,16 +52,16 @@ Opciones: ```bash # Generar desde swagger.yaml en src/app -node generate.js -i swagger.yaml -o ./src/app +npm run generate -- -i swagger.yaml -o ./src/app # Usar templates personalizados -node generate.js -i api.yaml -t ./mis-templates +npm run generate -- -i api.yaml -t ./mis-templates # Modo de prueba (no genera archivos) -node generate.js -i swagger.yaml --dry-run +npm run generate -- -i swagger.yaml --dry-run # Especificar todos los parámetros -node generate.js -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates +npm run generate -- -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates ``` ## 📁 Estructura Generada @@ -183,7 +190,7 @@ cp ../docs/api.yaml ./swagger.yaml ### 3. Generar código ```bash -node generate.js +npm run generate -- -i swagger.yaml ``` ### 4. Registrar providers en Angular @@ -246,7 +253,7 @@ npm run setup Asegúrate de especificar la ruta correcta: ```bash -node generate.js -i ./ruta/a/tu/swagger.yaml +npm run generate -- -i ./ruta/a/tu/swagger.yaml ``` ### Los imports no se resuelven (@/ no funciona) diff --git a/dist/generate.js b/dist/generate.js new file mode 100644 index 0000000..317a59a --- /dev/null +++ b/dist/generate.js @@ -0,0 +1,492 @@ +#!/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 = swagger.tags || []; + const paths = swagger.paths || {}; + logInfo(`Encontrados ${tags.length} tags en el API`); + logInfo(`Encontrados ${Object.keys(paths).length} endpoints`); + tags.forEach((tag) => { + logInfo(` - ${tag.name}: ${tag.description || 'Sin descripción'}`); + }); + return { tags, paths, swagger }; + } + catch (error) { + logError(`Error al leer el archivo Swagger: ${error.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) { + const typeMap = { + 'integer': 'number', + 'string': 'string', + 'boolean': 'boolean', + 'number': 'number', + 'array': 'Array', + 'object': 'any' + }; + return typeMap[type] || 'any'; +} +// Generar Clean Architecture con Mustache +function generateCleanArchitecture(analysis, outputDir, templatesDir) { + logStep('Generando artefactos de Clean Architecture usando Mustache...'); + let 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 = 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_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 = '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, 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) => { + logError(`Error fatal: ${error.message}`); + console.error(error); + process.exit(1); +}); diff --git a/generate.js b/generate.ts similarity index 84% rename from generate.js rename to generate.ts index 84e888c..aec3ab9 100755 --- a/generate.js +++ b/generate.ts @@ -1,14 +1,14 @@ #!/usr/bin/env node -const { execSync } = require('child_process'); -const fs = require('fs-extra'); -const path = require('path'); -const yaml = require('js-yaml'); -const mustache = require('mustache'); -const { program } = require('commander'); +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.escape = function(text) { return text; }; +(mustache as any).escape = function(text: string): string { return text; }; // Colores para console (sin dependencias externas) const colors = { @@ -21,27 +21,29 @@ const colors = { cyan: '\x1b[36m' }; -function log(message, color = 'reset') { +type Color = keyof typeof colors; + +function log(message: string, color: Color = 'reset'): void { console.log(`${colors[color]}${message}${colors.reset}`); } -function logSuccess(message) { +function logSuccess(message: string): void { log(`✅ ${message}`, 'green'); } -function logInfo(message) { +function logInfo(message: string): void { log(`ℹ️ ${message}`, 'blue'); } -function logWarning(message) { +function logWarning(message: string): void { log(`⚠️ ${message}`, 'yellow'); } -function logError(message) { +function logError(message: string): void { log(`❌ ${message}`, 'red'); } -function logStep(message) { +function logStep(message: string): void { log(`\n🚀 ${message}`, 'cyan'); } @@ -57,10 +59,18 @@ program .option('--dry-run', 'Simular sin generar archivos') .parse(process.argv); -const options = program.opts(); +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() { +function checkOpenApiGenerator(): boolean { try { execSync('openapi-generator-cli version', { stdio: 'ignore' }); return true; @@ -70,7 +80,7 @@ function checkOpenApiGenerator() { } // Instalar openapi-generator-cli -function installOpenApiGenerator() { +function installOpenApiGenerator(): void { logStep('Instalando @openapitools/openapi-generator-cli...'); try { execSync('npm install -g @openapitools/openapi-generator-cli', { stdio: 'inherit' }); @@ -82,7 +92,7 @@ function installOpenApiGenerator() { } // Crear estructura de directorios -function createDirectoryStructure(baseDir) { +function createDirectoryStructure(baseDir: string): void { const dirs = [ path.join(baseDir, 'data/dtos'), path.join(baseDir, 'data/repositories'), @@ -101,13 +111,19 @@ function createDirectoryStructure(baseDir) { logSuccess('Estructura de directorios creada'); } +interface SwaggerAnalysis { + tags: any[]; + paths: Record; + swagger: any; +} + // Analizar el swagger para extraer tags y dominios -function analyzeSwagger(swaggerFile) { +function analyzeSwagger(swaggerFile: string): SwaggerAnalysis { logStep('Analizando archivo OpenAPI...'); try { const fileContent = fs.readFileSync(swaggerFile, 'utf8'); - const swagger = yaml.load(fileContent); + const swagger = yaml.load(fileContent) as any; const tags = swagger.tags || []; const paths = swagger.paths || {}; @@ -115,19 +131,19 @@ function analyzeSwagger(swaggerFile) { logInfo(`Encontrados ${tags.length} tags en el API`); logInfo(`Encontrados ${Object.keys(paths).length} endpoints`); - tags.forEach(tag => { + tags.forEach((tag: any) => { logInfo(` - ${tag.name}: ${tag.description || 'Sin descripción'}`); }); return { tags, paths, swagger }; - } catch (error) { + } catch (error: any) { logError(`Error al leer el archivo Swagger: ${error.message}`); process.exit(1); } } // Generar código con OpenAPI Generator -function generateCode(swaggerFile, templatesDir) { +function generateCode(swaggerFile: string, templatesDir: string): string { logStep('Generando código desde OpenAPI...'); const tempDir = path.join(process.cwd(), '.temp-generated'); @@ -160,7 +176,7 @@ function generateCode(swaggerFile, templatesDir) { } // Organizar archivos según Clean Architecture (DTOs) -function organizeFiles(tempDir, outputDir) { +function organizeFiles(tempDir: string, outputDir: string): void { logStep('Organizando archivos DTO generados...'); const sourceDir = path.join(tempDir, 'model'); @@ -186,8 +202,8 @@ function organizeFiles(tempDir, outputDir) { } // Utilidad para mapear tipos OpenAPI elementales a TypeScript -function mapSwaggerTypeToTs(type) { - const typeMap = { +function mapSwaggerTypeToTs(type: string): string { + const typeMap: Record = { 'integer': 'number', 'string': 'string', 'boolean': 'boolean', @@ -198,10 +214,18 @@ function mapSwaggerTypeToTs(type) { return typeMap[type] || 'any'; } +interface GeneratedCount { + models: number; + repositories: number; + mappers: number; + useCases: number; + providers: number; +} + // Generar Clean Architecture con Mustache -function generateCleanArchitecture(analysis, outputDir, templatesDir) { +function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, templatesDir: string): GeneratedCount { logStep('Generando artefactos de Clean Architecture usando Mustache...'); - let generatedCount = { models: 0, repositories: 0, mappers: 0, useCases: 0, providers: 0 }; + let generatedCount: GeneratedCount = { models: 0, repositories: 0, mappers: 0, useCases: 0, providers: 0 }; const schemas = analysis.swagger.components?.schemas || {}; @@ -212,13 +236,13 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { // variables para model const rawProperties = schemas[schemaName].properties || {}; - const requiredProps = schemas[schemaName].required || []; + 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(); + 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()}>`; } @@ -281,7 +305,7 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { }); // 2. Generar Casos de Uso y Repositorios a partir de Paths/Tags - const tagsMap = {}; + const tagsMap: Record = {}; // Agrupar operaciones por Tag Object.keys(analysis.paths).forEach(pathKey => { @@ -293,7 +317,7 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { if (!tagsMap[tag]) tagsMap[tag] = []; // Parsear parámetros - const allParams = (op.parameters || []).map(p => ({ + const allParams = (op.parameters || []).map((p: any) => ({ paramName: p.name, dataType: mapSwaggerTypeToTs(p.schema?.type), description: p.description || '', @@ -305,7 +329,7 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { let bodyType = 'any'; const content = op.requestBody.content?.['application/json']?.schema; if (content) { - if (content.$ref) bodyType = content.$ref.split('/').pop(); + if (content.$ref) bodyType = content.$ref.split('/').pop() || 'any'; else if (content.type) bodyType = mapSwaggerTypeToTs(content.type); } allParams.push({ @@ -323,10 +347,10 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { const responseSchema = op.responses?.['200']?.content?.['application/json']?.schema; if (responseSchema) { if (responseSchema.$ref) { - returnType = responseSchema.$ref.split('/').pop(); + returnType = responseSchema.$ref.split('/').pop() || 'any'; returnBaseType = returnType; } else if (responseSchema.type === 'array' && responseSchema.items?.$ref) { - returnBaseType = responseSchema.items.$ref.split('/').pop(); + returnBaseType = responseSchema.items.$ref.split('/').pop() || 'any'; returnType = `Array<${returnBaseType}>`; isListContainer = true; } @@ -338,9 +362,9 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { 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 })), + 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, @@ -355,10 +379,10 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { // Generar por cada Tag Object.keys(tagsMap).forEach(tag => { // Buscar si ese tag cruza con alguna entidad para importarla - const imports = []; + const imports: any[] = []; Object.keys(schemas).forEach(s => { // Import heurístico burdo - if (tagsMap[tag].some(op => op.returnType === s || op.returnType === `Array<${s}>`)) { + 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) }); } }); @@ -443,18 +467,32 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { } // Limpiar directorio temporal -function cleanup(tempDir) { +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, analysis) { +function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport { logStep('Generando reporte de generación...'); - const report = { + const report: GenerationReport = { timestamp: new Date().toISOString(), tags: analysis.tags.length, endpoints: Object.keys(analysis.paths).length, @@ -477,7 +515,7 @@ function generateReport(outputDir, analysis) { } // Función principal -async function main() { +async function main(): Promise { console.log('\n' + '='.repeat(60)); log(' OpenAPI Clean Architecture Generator', 'bright'); log(' Angular + Clean Architecture Code Generator', 'cyan'); @@ -550,7 +588,7 @@ async function main() { } // Ejecutar -main().catch(error => { +main().catch((error: any) => { logError(`Error fatal: ${error.message}`); console.error(error); process.exit(1); diff --git a/package.json b/package.json index 1de7a73..d9c1b25 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,15 @@ "name": "openapi-clean-arch-generator", "version": "1.0.0", "description": "Generador de código Angular con Clean Architecture desde OpenAPI/Swagger", - "main": "generate.js", + "main": "dist/generate.js", "bin": { - "generate-clean-arch": "./generate.js" + "generate-clean-arch": "./dist/generate.js" }, "scripts": { - "generate": "node generate.js", + "build": "tsc", + "prepublishOnly": "npm run build", + "generate": "node dist/generate.js", + "generate:dev": "ts-node generate.ts", "setup": "npm install -g @openapitools/openapi-generator-cli" }, "keywords": [ @@ -28,5 +31,13 @@ }, "engines": { "node": ">=14.0.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/js-yaml": "^4.0.9", + "@types/mustache": "^4.2.6", + "@types/node": "^25.5.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1d988ae --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": ["generate.ts"], + "exclude": ["node_modules", "dist"] +} From cebadbfbcc5b04740ba6975d564b470e1b42b6a2 Mon Sep 17 00:00:00 2001 From: didavila Date: Mon, 23 Mar 2026 17:23:06 +0100 Subject: [PATCH 2/3] feat: add Prettier and ESLint configuration for code formatting and linting - Create .prettierrc for Prettier configuration - Add eslint.config.js for ESLint setup with TypeScript support - Update package.json to include linting and formatting scripts - Refactor generate.ts and generate.js for improved readability and error handling - Enhance QUICKSTART.md and README.md with formatting and clarity improvements --- .prettierrc | 7 + QUICKSTART.md | 7 +- README.md | 8 +- dist/generate.js | 121 +++++++++----- eslint.config.js | 30 ++++ generate.ts | 427 +++++++++++++++++++++++++++++++---------------- package.json | 14 +- 7 files changed, 422 insertions(+), 192 deletions(-) create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d8a8846 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "tabWidth": 2, + "semi": true +} diff --git a/QUICKSTART.md b/QUICKSTART.md index c14326c..2dfce55 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -112,9 +112,9 @@ import { USER_USE_CASES } from '@/domain/use-cases/user/user.use-cases.contract' }) export class UsersComponent { #userUseCases = inject(USER_USE_CASES); - + ngOnInit() { - this.#userUseCases.getUsers().subscribe(users => { + this.#userUseCases.getUsers().subscribe((users) => { console.log(users); }); } @@ -126,6 +126,7 @@ export class UsersComponent { ### ❌ Error: openapi-generator-cli: command not found **Solución:** + ```bash npm install -g @openapitools/openapi-generator-cli ``` @@ -133,6 +134,7 @@ npm install -g @openapitools/openapi-generator-cli ### ❌ Error: Cannot find module 'commander' **Solución:** + ```bash npm install ``` @@ -140,6 +142,7 @@ npm install ### ❌ Los archivos no se generan **Solución:** Verifica que el directorio de salida existe o usa `--dry-run` para ver qué pasaría: + ```bash node generate.js --dry-run ``` diff --git a/README.md b/README.md index ed651e5..b10d3ea 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ src/app/ Los templates están en la carpeta `templates/`. Cada archivo `.mustache` define cómo se genera un tipo de archivo. Templates disponibles: + - `model.mustache` - DTOs - `model-entity.mustache` - Entidades del modelo - `mapper.mustache` - Mappers @@ -208,7 +209,7 @@ import { NodeUseCasesProvider } from '@/di/use-cases/node.use-cases.provider'; NodeRepositoryProvider, OrderTypeRepositoryProvider, SupplyModeRepositoryProvider, - + // Use Cases NodeUseCasesProvider, OrderTypeUseCasesProvider, @@ -230,9 +231,9 @@ import { NODE_USE_CASES, NodeUseCases } from '@/domain/use-cases/node/node.use-c }) export class NodesComponent { #nodeUseCases = inject(NODE_USE_CASES); - + loadNodes() { - this.#nodeUseCases.getNodes('TI').subscribe(nodes => { + this.#nodeUseCases.getNodes('TI').subscribe((nodes) => { console.log(nodes); }); } @@ -252,6 +253,7 @@ npm run setup ### Error: Archivo swagger.yaml no encontrado Asegúrate de especificar la ruta correcta: + ```bash npm run generate -- -i ./ruta/a/tu/swagger.yaml ``` diff --git a/dist/generate.js b/dist/generate.js index 317a59a..d4d0d13 100644 --- a/dist/generate.js +++ b/dist/generate.js @@ -11,7 +11,9 @@ 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; }; +mustache_1.default.escape = function (text) { + return text; +}; // Colores para console (sin dependencias externas) const colors = { reset: '\x1b[0m', @@ -58,7 +60,7 @@ function checkOpenApiGenerator() { (0, child_process_1.execSync)('openapi-generator-cli version', { stdio: 'ignore' }); return true; } - catch (error) { + catch (_error) { return false; } } @@ -69,7 +71,7 @@ function installOpenApiGenerator() { (0, child_process_1.execSync)('npm install -g @openapitools/openapi-generator-cli', { stdio: 'inherit' }); logSuccess('OpenAPI Generator CLI instalado correctamente'); } - catch (error) { + catch (_error) { logError('Error al instalar OpenAPI Generator CLI'); process.exit(1); } @@ -86,7 +88,7 @@ function createDirectoryStructure(baseDir) { path_1.default.join(baseDir, 'di/use-cases'), path_1.default.join(baseDir, 'entities/models') ]; - dirs.forEach(dir => { + dirs.forEach((dir) => { fs_extra_1.default.ensureDirSync(dir); }); logSuccess('Estructura de directorios creada'); @@ -97,17 +99,19 @@ function analyzeSwagger(swaggerFile) { try { const fileContent = fs_extra_1.default.readFileSync(swaggerFile, 'utf8'); const swagger = js_yaml_1.default.load(fileContent); - const tags = swagger.tags || []; + 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) => { - logInfo(` - ${tag.name}: ${tag.description || 'Sin descripción'}`); + const t = tag; + logInfo(` - ${t.name}: ${t.description || 'Sin descripción'}`); }); return { tags, paths, swagger }; } catch (error) { - logError(`Error al leer el archivo Swagger: ${error.message}`); + const err = error; + logError(`Error al leer el archivo Swagger: ${err.message}`); process.exit(1); } } @@ -131,7 +135,7 @@ function generateCode(swaggerFile, templatesDir) { logSuccess('Código generado correctamente'); return tempDir; } - catch (error) { + catch (_error) { logError('Error al generar código'); if (fs_extra_1.default.existsSync(tempDir)) { fs_extra_1.default.removeSync(tempDir); @@ -147,8 +151,8 @@ function organizeFiles(tempDir, outputDir) { 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 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); @@ -160,33 +164,43 @@ function organizeFiles(tempDir, outputDir) { } // 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': 'any' + integer: 'number', + string: 'string', + boolean: 'boolean', + number: 'number', + array: 'Array', + object: 'unknown' }; - return typeMap[type] || 'any'; + return typeMap[type] || 'unknown'; } // Generar Clean Architecture con Mustache function generateCleanArchitecture(analysis, outputDir, templatesDir) { logStep('Generando artefactos de Clean Architecture usando Mustache...'); - let generatedCount = { models: 0, repositories: 0, mappers: 0, useCases: 0, providers: 0 }; - const schemas = analysis.swagger.components?.schemas || {}; + 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 => { + 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 = schemas[schemaName].required || []; - const varsMap = Object.keys(rawProperties).map(k => { + 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() || 'any'; + 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()}>`; @@ -199,15 +213,17 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { }; }); const modelViewData = { - models: [{ + models: [ + { model: { classname: baseName, classFilename: baseName.toLowerCase(), classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1), - description: schemas[schemaName].description || '', + description: schemaObj.description || '', vars: varsMap } - }], + } + ], // Para plantillas que esperan allModels o importaciones (mapper) allModels: [{ model: { vars: varsMap } }] }; @@ -215,13 +231,15 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { const mapperViewData = { ...modelViewData, apiInfo: { - apis: [{ + apis: [ + { operations: { classname: baseName, classFilename: baseName.toLowerCase(), - classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1), + classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1) } - }] + } + ] } }; // Model (Entities) @@ -247,9 +265,9 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { // 2. Generar Casos de Uso y Repositorios a partir de Paths/Tags const tagsMap = {}; // Agrupar operaciones por Tag - Object.keys(analysis.paths).forEach(pathKey => { + Object.keys(analysis.paths).forEach((pathKey) => { const pathObj = analysis.paths[pathKey]; - Object.keys(pathObj).forEach(method => { + 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 @@ -258,17 +276,17 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { // Parsear parámetros const allParams = (op.parameters || []).map((p) => ({ paramName: p.name, - dataType: mapSwaggerTypeToTs(p.schema?.type), + 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'; + let bodyType = 'unknown'; const content = op.requestBody.content?.['application/json']?.schema; if (content) { if (content.$ref) - bodyType = content.$ref.split('/').pop() || 'any'; + bodyType = content.$ref.split('/').pop() || 'unknown'; else if (content.type) bodyType = mapSwaggerTypeToTs(content.type); } @@ -286,11 +304,11 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { const responseSchema = op.responses?.['200']?.content?.['application/json']?.schema; if (responseSchema) { if (responseSchema.$ref) { - returnType = responseSchema.$ref.split('/').pop() || 'any'; + returnType = responseSchema.$ref.split('/').pop() || 'unknown'; returnBaseType = returnType; } else if (responseSchema.type === 'array' && responseSchema.items?.$ref) { - returnBaseType = responseSchema.items.$ref.split('/').pop() || 'any'; + returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown'; returnType = `Array<${returnBaseType}>`; isListContainer = true; } @@ -303,7 +321,12 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { 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 })), + 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, @@ -315,18 +338,23 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { }); }); // Generar por cada Tag - Object.keys(tagsMap).forEach(tag => { + Object.keys(tagsMap).forEach((tag) => { // Buscar si ese tag cruza con alguna entidad para importarla const imports = []; - Object.keys(schemas).forEach(s => { + 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) }); + imports.push({ + classname: s, + classFilename: s.toLowerCase(), + classVarName: s.charAt(0).toLowerCase() + s.slice(1) + }); } }); const apiViewData = { apiInfo: { - apis: [{ + apis: [ + { operations: { classname: tag, classFilename: tag.toLowerCase(), @@ -334,7 +362,8 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir) { operation: tagsMap[tag], imports: imports } - }] + } + ] } }; // Use Case Contract @@ -415,7 +444,8 @@ function generateReport(outputDir, analysis) { 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 + 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'); @@ -486,7 +516,8 @@ async function main() { } // Ejecutar main().catch((error) => { - logError(`Error fatal: ${error.message}`); + const err = error; + logError(`Error fatal: ${err.message}`); console.error(error); process.exit(1); }); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..8f8e7c5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,30 @@ +const eslint = require('@eslint/js'); +const tseslint = require('typescript-eslint'); +const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); + +module.exports = tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname + } + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_' }] + } + }, + { + ignores: ['dist/', 'node_modules/', 'eslint.config.js'] + } +); diff --git a/generate.ts b/generate.ts index aec3ab9..4bcfc0e 100755 --- a/generate.ts +++ b/generate.ts @@ -8,7 +8,9 @@ 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; }; +(mustache as { escape: (text: string) => string }).escape = function (text: string): string { + return text; +}; // Colores para console (sin dependencias externas) const colors = { @@ -59,7 +61,7 @@ program .option('--dry-run', 'Simular sin generar archivos') .parse(process.argv); -interface CliOptions { +export interface CliOptions { input: string; output: string; templates: string; @@ -67,14 +69,14 @@ interface CliOptions { dryRun?: boolean; } -const options = program.opts() as CliOptions; +const options = program.opts(); // Validar que existe openapi-generator-cli function checkOpenApiGenerator(): boolean { try { execSync('openapi-generator-cli version', { stdio: 'ignore' }); return true; - } catch (error) { + } catch (_error) { return false; } } @@ -85,7 +87,7 @@ function installOpenApiGenerator(): void { try { execSync('npm install -g @openapitools/openapi-generator-cli', { stdio: 'inherit' }); logSuccess('OpenAPI Generator CLI instalado correctamente'); - } catch (error) { + } catch (_error) { logError('Error al instalar OpenAPI Generator CLI'); process.exit(1); } @@ -104,7 +106,7 @@ function createDirectoryStructure(baseDir: string): void { path.join(baseDir, 'entities/models') ]; - dirs.forEach(dir => { + dirs.forEach((dir) => { fs.ensureDirSync(dir); }); @@ -112,32 +114,34 @@ function createDirectoryStructure(baseDir: string): void { } interface SwaggerAnalysis { - tags: any[]; - paths: Record; - swagger: any; + tags: unknown[]; + paths: Record; + swagger: unknown; } // 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 || {}; - + const swagger = yaml.load(fileContent) as Record; + + const tags = Array.isArray(swagger.tags) ? swagger.tags : []; + const paths = (swagger.paths as Record) || {}; + 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'}`); + + tags.forEach((tag: unknown) => { + const t = tag as { name: string; description?: string }; + logInfo(` - ${t.name}: ${t.description || 'Sin descripción'}`); }); - + return { tags, paths, swagger }; - } catch (error: any) { - logError(`Error al leer el archivo Swagger: ${error.message}`); + } catch (error: unknown) { + const err = error as Error; + logError(`Error al leer el archivo Swagger: ${err.message}`); process.exit(1); } } @@ -145,14 +149,14 @@ function analyzeSwagger(swaggerFile: string): SwaggerAnalysis { // 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}" \ @@ -161,12 +165,12 @@ function generateCode(swaggerFile: string, templatesDir: string): string { -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) { + } catch (_error) { logError('Error al generar código'); if (fs.existsSync(tempDir)) { fs.removeSync(tempDir); @@ -178,40 +182,42 @@ function generateCode(swaggerFile: string, templatesDir: string): string { // 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 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 { +function mapSwaggerTypeToTs(type?: string): string { + if (!type) return 'unknown'; + const typeMap: Record = { - 'integer': 'number', - 'string': 'string', - 'boolean': 'boolean', - 'number': 'number', - 'array': 'Array', - 'object': 'any' + integer: 'number', + string: 'string', + boolean: 'boolean', + number: 'number', + array: 'Array', + object: 'unknown' }; - return typeMap[type] || 'any'; + return typeMap[type] || 'unknown'; } interface GeneratedCount { @@ -222,32 +228,118 @@ interface GeneratedCount { 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 }; +export interface OpenApiSchema { + properties?: Record< + string, + { + type?: string; + description?: string; + $ref?: string; + items?: { $ref?: string }; + } + >; + required?: string[]; + description?: string; +} + +export interface OpenApiOperation { + tags?: string[]; + operationId?: string; + summary?: string; + description?: string; + parameters?: Array<{ + name: string; + in: string; + required: boolean; + description?: string; + schema?: { type?: string }; + }>; + requestBody?: { + description?: string; + content?: Record< + string, + { + schema?: { + $ref?: string; + type?: string; + }; + } + >; + }; + responses?: Record< + string, + { + content?: Record< + string, + { + schema?: { + $ref?: string; + type?: string; + items?: { $ref?: string }; + }; + } + >; + } + >; +} + +export interface TagOperation { + nickname: string; + summary: string; + notes: string; + httpMethod: string; + path: string; + allParams: unknown[]; + hasQueryParams: boolean; + queryParams: unknown[]; + hasBodyParam: boolean; + bodyParam: string; + returnType: string | boolean; + returnBaseType: string | boolean; + isListContainer: boolean; + vendorExtensions: Record; +} + +// Generar Clean Architecture con Mustache +function generateCleanArchitecture( + analysis: SwaggerAnalysis, + outputDir: string, + templatesDir: string +): GeneratedCount { + logStep('Generando artefactos de Clean Architecture usando Mustache...'); + const generatedCount: GeneratedCount = { + models: 0, + repositories: 0, + mappers: 0, + useCases: 0, + providers: 0 + }; + + const schemas = + (analysis.swagger as { components?: { schemas?: Record } }).components + ?.schemas || {}; - const schemas = analysis.swagger.components?.schemas || {}; - // 1. Generar Modelos, Entidades y Mappers a partir de Schemas - Object.keys(schemas).forEach(schemaName => { + 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 => { + + const schemaObj = schemas[schemaName] as OpenApiSchema; + const rawProperties = schemaObj.properties || {}; + const requiredProps: string[] = 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() || 'any'; + 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, + return { + name: k, dataType: tsType, description: rawProperties[k].description || '', required: requiredProps.includes(k) @@ -255,15 +347,17 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, }); const modelViewData = { - models: [{ - model: { - classname: baseName, - classFilename: baseName.toLowerCase(), - classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1), - description: schemas[schemaName].description || '', - vars: varsMap + 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 } }] }; @@ -271,14 +365,16 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, // 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), - } - }] + apiInfo: { + apis: [ + { + operations: { + classname: baseName, + classFilename: baseName.toLowerCase(), + classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1) + } + } + ] } }; @@ -287,7 +383,11 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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`); + 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)}`); @@ -305,32 +405,32 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, }); // 2. Generar Casos de Uso y Repositorios a partir de Paths/Tags - const tagsMap: Record = {}; - + const tagsMap: Record = {}; + // Agrupar operaciones por Tag - Object.keys(analysis.paths).forEach(pathKey => { - const pathObj = analysis.paths[pathKey]; - Object.keys(pathObj).forEach(method => { - const op = pathObj[method]; + Object.keys(analysis.paths).forEach((pathKey) => { + const pathObj = analysis.paths[pathKey] as Record; + Object.keys(pathObj).forEach((method) => { + const op = pathObj[method] as OpenApiOperation; 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) => ({ + const allParams = (op.parameters || []).map((p) => ({ paramName: p.name, - dataType: mapSwaggerTypeToTs(p.schema?.type), + 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'; + let bodyType = 'unknown'; 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); + if (content.$ref) bodyType = content.$ref.split('/').pop() || 'unknown'; + else if (content.type) bodyType = mapSwaggerTypeToTs(content.type); } allParams.push({ paramName: 'body', @@ -339,21 +439,21 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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; - } + 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({ @@ -362,9 +462,17 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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 })), + allParams: allParams.map((p, i: number) => ({ + ...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: number, arr: unknown[]) => ({ + paramName: p.name, + '-last': i === arr.length - 1 + })), hasBodyParam: !!op.requestBody, bodyParam: 'body', returnType: returnType !== 'void' ? returnType : false, @@ -377,27 +485,33 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, }); // Generar por cada Tag - Object.keys(tagsMap).forEach(tag => { + Object.keys(tagsMap).forEach((tag) => { // Buscar si ese tag cruza con alguna entidad para importarla - const imports: any[] = []; - Object.keys(schemas).forEach(s => { + const imports: { classname: string; classFilename: string; classVarName: string }[] = []; + 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) }); + 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 + apis: [ + { + operations: { + classname: tag, + classFilename: tag.toLowerCase(), + constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'), + operation: tagsMap[tag], + imports: imports + } } - }] + ] } }; @@ -406,7 +520,11 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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`); + const destPath = path.join( + outputDir, + 'domain/use-cases', + `${tag.toLowerCase()}.use-cases.contract.ts` + ); fs.writeFileSync(destPath, output); generatedCount.useCases++; } @@ -416,7 +534,11 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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`); + const destPath = path.join( + outputDir, + 'domain/use-cases', + `${tag.toLowerCase()}.use-cases.impl.ts` + ); fs.writeFileSync(destPath, output); generatedCount.useCases++; } @@ -426,7 +548,11 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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`); + const destPath = path.join( + outputDir, + 'domain/repositories', + `${tag.toLowerCase()}.repository.contract.ts` + ); fs.writeFileSync(destPath, output); generatedCount.repositories++; } @@ -436,7 +562,11 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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`); + const destPath = path.join( + outputDir, + 'data/repositories', + `${tag.toLowerCase()}.repository.impl.ts` + ); fs.writeFileSync(destPath, output); generatedCount.repositories++; } @@ -446,7 +576,11 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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`); + const destPath = path.join( + outputDir, + 'di/use-cases', + `${tag.toLowerCase()}.use-cases.provider.ts` + ); fs.writeFileSync(destPath, output); generatedCount.providers++; } @@ -456,13 +590,19 @@ function generateCleanArchitecture(analysis: SwaggerAnalysis, outputDir: string, 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`); + 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`); + logSuccess( + `${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers generados con Mustache` + ); return generatedCount; } @@ -491,7 +631,7 @@ interface GenerationReport { // 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, @@ -502,15 +642,17 @@ function generateReport(outputDir: string, analysis: SwaggerAnalysis): Generatio 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 + 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; } @@ -520,60 +662,62 @@ async function main(): Promise { 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'); + 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'); @@ -588,8 +732,9 @@ async function main(): Promise { } // Ejecutar -main().catch((error: any) => { - logError(`Error fatal: ${error.message}`); +main().catch((error: unknown) => { + const err = error as Error; + logError(`Error fatal: ${err.message}`); console.error(error); process.exit(1); }); diff --git a/package.json b/package.json index d9c1b25..b52f857 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "prepublishOnly": "npm run build", "generate": "node dist/generate.js", "generate:dev": "ts-node generate.ts", + "lint": "eslint 'generate.ts' -f unix", + "lint:fix": "eslint 'generate.ts' --fix -f unix", + "format": "prettier --write .", "setup": "npm install -g @openapitools/openapi-generator-cli" }, "keywords": [ @@ -33,11 +36,20 @@ "node": ">=14.0.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9", "@types/mustache": "^4.2.6", "@types/node": "^25.5.0", + "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "eslint-formatter-unix": "^9.0.1", + "eslint-plugin-prettier": "^5.5.5", + "prettier": "^3.8.1", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.1" } } From 72536d4a860a9250231c5ef50a4363b2030e2278 Mon Sep 17 00:00:00 2001 From: didavila Date: Mon, 23 Mar 2026 17:26:04 +0100 Subject: [PATCH 3/3] feat: change name --- generate.ts => main.ts | 0 package.json | 6 +++--- tsconfig.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename generate.ts => main.ts (100%) diff --git a/generate.ts b/main.ts similarity index 100% rename from generate.ts rename to main.ts diff --git a/package.json b/package.json index b52f857..a70818a 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "build": "tsc", "prepublishOnly": "npm run build", "generate": "node dist/generate.js", - "generate:dev": "ts-node generate.ts", - "lint": "eslint 'generate.ts' -f unix", - "lint:fix": "eslint 'generate.ts' --fix -f unix", + "generate:dev": "ts-node main.ts", + "lint": "eslint 'main.ts' -f unix", + "lint:fix": "eslint 'main.ts' --fix -f unix", "format": "prettier --write .", "setup": "npm install -g @openapitools/openapi-generator-cli" }, diff --git a/tsconfig.json b/tsconfig.json index 1d988ae..e413ed6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,6 @@ "moduleResolution": "node", "resolveJsonModule": true }, - "include": ["generate.ts"], + "include": ["main.ts"], "exclude": ["node_modules", "dist"] }