import { execSync } from 'child_process'; import fs from 'fs-extra'; import path from 'path'; import { logStep, logSuccess, logError, logDetail } from '../utils/logger'; import { toPascalCase } from '../utils/name-formatter'; /** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */ export function generateCode(swaggerFile: string, templatesDir: string): string { logStep('Generating code from OpenAPI spec...'); const tempDir = path.join(process.cwd(), '.temp-generated'); if (fs.existsSync(tempDir)) { fs.removeSync(tempDir); } try { const command = `openapi-generator-cli generate \ -i "${swaggerFile}" \ -g typescript-angular \ --global-property models \ -t "${templatesDir}" \ -o "${tempDir}" \ --additional-properties=ngVersion=17.0.0,modelFileSuffix=.dto,modelNameSuffix=Dto`; execSync(command, { stdio: 'pipe' }); logSuccess('Code generated successfully'); return tempDir; } catch (_error) { logError('Error generating code'); if (fs.existsSync(tempDir)) { fs.removeSync(tempDir); } process.exit(1); } } /** Copies the generated DTOs from the temporary directory to the output directory, organised by tag subfolder. */ export function organizeFiles( tempDir: string, outputDir: string, schemaTagMap: Record = {} ): void { logStep('Organising generated DTO files...'); 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) => { // file is like "userResponse.dto.ts" → derive PascalCase schema name to look up tag const camelName = file.replace('.dto.ts', ''); const pascalName = toPascalCase(camelName); const tagFolder = schemaTagMap[pascalName] || 'shared'; const sourcePath = path.join(sourceDir, file); const destPath = path.join(destDir, tagFolder, file); fs.ensureDirSync(path.dirname(destPath)); fs.copySync(sourcePath, destPath); filesMoved++; logDetail('dto', `${file} → ${path.relative(process.cwd(), destPath)}`); }); } logSuccess(`${filesMoved} DTOs moved successfully`); } /** Post-processes the generated DTOs: adds cross-DTO imports and normalises Array → T[]. */ export function addDtoImports(outputDir: string): void { logStep('Post-processing generated DTOs...'); const dtosDir = path.join(outputDir, 'data/dtos'); if (!fs.existsSync(dtosDir)) return; // Collect all .dto.ts files from all subfolders (1 level deep) const allFiles: { subfolder: string; file: string; fullPath: string }[] = []; const entries = fs.readdirSync(dtosDir); entries.forEach((entry) => { const entryPath = path.join(dtosDir, entry); if (fs.statSync(entryPath).isDirectory()) { fs.readdirSync(entryPath) .filter((f) => f.endsWith('.dto.ts')) .forEach((file) => allFiles.push({ subfolder: entry, file, fullPath: path.join(entryPath, file) }) ); } else if (entry.endsWith('.dto.ts')) { allFiles.push({ subfolder: '', file: entry, fullPath: entryPath }); } }); // Build map: ClassName → { subfolder, fileBase } const dtoMap: Record = {}; allFiles.forEach(({ subfolder, file, fullPath }) => { const content = fs.readFileSync(fullPath, 'utf8'); const match = content.match(/export interface (\w+)/); if (match) dtoMap[match[1]] = { subfolder, fileBase: file.replace('.ts', '') }; }); let filesProcessed = 0; allFiles.forEach(({ subfolder, file, fullPath }) => { const originalContent = fs.readFileSync(fullPath, 'utf8'); let content = originalContent; const selfMatch = content.match(/export interface (\w+)/); const selfName = selfMatch ? selfMatch[1] : ''; content = content.replace(/Array<(\w+)>/g, '$1[]'); const references = new Set(); const typeRegex = /\b(\w+Dto)\b/g; let match; while ((match = typeRegex.exec(content)) !== null) { if (match[1] !== selfName) references.add(match[1]); } const imports: string[] = []; references.forEach((ref) => { if (dtoMap[ref]) { const { subfolder: refSubfolder, fileBase: refFileBase } = dtoMap[ref]; const fromDir = subfolder ? path.join(dtosDir, subfolder) : dtosDir; const toFile = refSubfolder ? path.join(dtosDir, refSubfolder, refFileBase) : path.join(dtosDir, refFileBase); let relPath = path.relative(fromDir, toFile).replace(/\\/g, '/'); if (!relPath.startsWith('.')) relPath = './' + relPath; imports.push(`import { ${ref} } from '${relPath}';`); } }); if (imports.length > 0) content = imports.join('\n') + '\n' + content; if (content !== originalContent) { fs.writeFileSync(fullPath, content); filesProcessed++; logDetail('dto', `Post-processed ${file} (added ${imports.length} import(s))`); } }); logSuccess(`${filesProcessed} DTOs post-processed`); }