feat: enhance logging and linting functionality with detailed reports
All checks were successful
Lint / lint (pull_request) Successful in 16s

This commit is contained in:
2026-03-26 13:03:10 +01:00
parent b54a94c6d3
commit 79ea7dfc7e
8 changed files with 133 additions and 44 deletions

41
main.ts
View File

@@ -5,7 +5,15 @@ import mustache from 'mustache';
import path from 'path';
import { program } from 'commander';
import { log, logSuccess, logInfo, logWarning, logError, colors } from './src/utils/logger';
import {
log,
logSuccess,
logWarning,
logError,
logDetail,
initGenerationLog,
colors
} from './src/utils/logger';
import { checkOpenApiGenerator, installOpenApiGenerator } from './src/utils/openapi-generator';
import { createDirectoryStructure, cleanup } from './src/utils/filesystem';
import { analyzeSwagger } from './src/swagger/analyzer';
@@ -18,7 +26,7 @@ import { generateReport } from './src/generators/report.generator';
import { lintGeneratedFiles } from './src/generators/lint.generator';
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
import type { SelectionFilter } from './src/types';
import type { SelectionFilter, LintResult } from './src/types';
import type { CliOptions } from './src/types';
import packageJson from './package.json';
@@ -52,15 +60,17 @@ async function main(): Promise<void> {
log(' Angular + Clean Architecture Code Generator', 'cyan');
console.log('='.repeat(60) + '\n');
const logPath = path.join(process.cwd(), 'generation.log');
initGenerationLog(logPath);
logDetail('config', `Input: ${options.input}`);
logDetail('config', `Output: ${options.output}`);
logDetail('config', `Templates: ${options.templates}`);
if (!fs.existsSync(options.input)) {
logError(`File not found: ${options.input}`);
process.exit(1);
}
logInfo(`Input file: ${options.input}`);
logInfo(`Output directory: ${options.output}`);
logInfo(`Templates: ${options.templates}`);
if (options.dryRun) {
logWarning('DRY RUN mode — no files will be generated');
}
@@ -82,7 +92,7 @@ async function main(): Promise<void> {
const analysis = analyzeSwagger(options.input);
if (options.dryRun) {
logInfo('Finishing in DRY RUN mode');
logWarning('Finishing in DRY RUN mode');
return;
}
@@ -110,9 +120,7 @@ async function main(): Promise<void> {
logSuccess(
`environment.ts found: ${colors.cyan}${path.relative(process.cwd(), envFile)}${colors.reset}`
);
if (apiKeys.length > 0) {
logInfo(`Detected API keys: ${apiKeys.map((k) => k.key).join(', ')}`);
} else {
if (apiKeys.length === 0) {
logWarning('No keys containing "api" found in environment.ts. Will be requested manually.');
}
} else {
@@ -120,6 +128,9 @@ async function main(): Promise<void> {
}
const tagApiKeyMap = await askApiKeysForTags(selectedTags, apiKeys);
Object.entries(tagApiKeyMap).forEach(([tag, key]) => {
logDetail('config', `API key for "${tag}": environment.${key}.url`);
});
// ──────────────────────────────────────────────────────────────────────────
const tempDir = generateCode(options.input, options.templates);
@@ -134,11 +145,13 @@ async function main(): Promise<void> {
);
cleanup(tempDir);
if (!options.skipLint) {
lintGeneratedFiles(options.output);
}
const noLintResult: LintResult = {
prettier: { ran: false, filesFormatted: 0 },
eslint: { ran: false, filesFixed: 0 }
};
const lintResult = options.skipLint ? noLintResult : lintGeneratedFiles(options.output);
const report = generateReport(options.output, analysis);
const report = generateReport(options.output, analysis, lintResult);
console.log('\n' + '='.repeat(60));
log(' ✨ Generation completed successfully', 'green');

View File

@@ -1,7 +1,7 @@
import fs from 'fs-extra';
import path from 'path';
import mustache from 'mustache';
import { logStep, logSuccess, logInfo } from '../utils/logger';
import { logStep, logSuccess, logDetail } from '../utils/logger';
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
import { toCamelCase } from '../utils/name-formatter';
import { resolveMockValue } from '../utils/mock-value-resolver';
@@ -157,7 +157,7 @@ export function generateCleanArchitecture(
const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`);
fs.writeFileSync(destPath, output);
generatedCount.models++;
logInfo(` ${toCamelCase(baseName)}.model.ts${path.relative(process.cwd(), destPath)}`);
logDetail('generate', `model-entity${path.relative(process.cwd(), destPath)}`);
}
// Mapper
@@ -514,6 +514,10 @@ function renderTemplate(
const output = mustache.render(template, viewData);
fs.writeFileSync(destPath, output);
counter[key]++;
logDetail(
'generate',
`${templateName.replace('.mustache', '')}${path.relative(process.cwd(), destPath)}`
);
}
}

View File

@@ -1,7 +1,7 @@
import { execSync } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
import { logStep, logSuccess, logError, logInfo } from '../utils/logger';
import { logStep, logSuccess, logError, logDetail } from '../utils/logger';
/** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
export function generateCode(swaggerFile: string, templatesDir: string): string {
@@ -22,7 +22,7 @@ export function generateCode(swaggerFile: string, templatesDir: string): string
-o "${tempDir}" \
--additional-properties=ngVersion=17.0.0,modelFileSuffix=.dto,modelNameSuffix=Dto`;
execSync(command, { stdio: 'inherit' });
execSync(command, { stdio: 'pipe' });
logSuccess('Code generated successfully');
return tempDir;
@@ -51,10 +51,9 @@ export function organizeFiles(tempDir: string, outputDir: string): void {
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)}`);
logDetail('dto', `${file}${path.relative(process.cwd(), destPath)}`);
});
}
@@ -119,7 +118,7 @@ export function addDtoImports(outputDir: string): void {
if (content !== originalContent) {
fs.writeFileSync(filePath, content);
filesProcessed++;
logInfo(` Procesado ${file}`);
logDetail('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
}
});

View File

@@ -1,7 +1,8 @@
import fs from 'fs-extra';
import path from 'path';
import { spawnSync } from 'child_process';
import { logStep, logSuccess, logWarning, logInfo } from '../utils/logger';
import { logStep, logSuccess, logWarning, logDetail } from '../utils/logger';
import type { LintResult } from '../types';
/**
* Walks up the directory tree from `startDir` to find the nearest
@@ -36,17 +37,19 @@ function collectTsFiles(dir: string): string[] {
}
/**
* Runs a command synchronously and returns whether it succeeded.
* Prints stdout/stderr to the console only on failure.
* Runs a command synchronously. Only prints to console on fatal failure (exit >= 2).
* Exit code 1 from ESLint means "warnings remain after --fix" — not a fatal error.
* Returns captured output for logging to file.
*/
function run(cmd: string, args: string[], cwd: string): boolean {
function run(cmd: string, args: string[], cwd: string): { success: boolean; output: string } {
const result = spawnSync(cmd, args, { cwd, encoding: 'utf8', shell: true });
if (result.status !== 0) {
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
const fatalError = result.status === null || result.status >= 2;
if (fatalError) {
if (result.stderr) process.stderr.write(result.stderr);
if (result.stdout) process.stdout.write(result.stdout);
return false;
}
return true;
return { success: !fatalError, output };
}
/**
@@ -56,28 +59,39 @@ function run(cmd: string, args: string[], cwd: string): boolean {
*
* - Prettier: always attempted; logs a warning if not found.
* - ESLint: optional; silently skipped if no config is found in the project root.
*
* Returns a `LintResult` with the outcome of each tool for inclusion in the report.
*/
export function lintGeneratedFiles(outputDir: string): void {
export function lintGeneratedFiles(outputDir: string): LintResult {
logStep('Linting generated files...');
const result: LintResult = {
prettier: { ran: false, filesFormatted: 0 },
eslint: { ran: false, filesFixed: 0 }
};
const projectRoot = findProjectRoot(outputDir);
if (!projectRoot) {
logWarning('Could not locate a project root (package.json). Skipping lint.');
return;
return result;
}
logInfo(` Project root: ${projectRoot}`);
const files = collectTsFiles(outputDir);
if (files.length === 0) {
logWarning('No TypeScript files found in output directory. Skipping lint.');
return;
return result;
}
logDetail('lint', `Project root: ${projectRoot}`);
logDetail('lint', `Files to process: ${files.length}`);
const relativePaths = files.map((f) => path.relative(projectRoot, f));
// --- Prettier ---
const prettierOk = run('npx', ['prettier', '--write', ...relativePaths], projectRoot);
if (prettierOk) {
const prettier = run('npx', ['prettier', '--write', ...relativePaths], projectRoot);
if (prettier.output) logDetail('prettier', prettier.output);
if (prettier.success) {
result.prettier = { ran: true, filesFormatted: files.length };
logSuccess(`Prettier formatted ${files.length} files`);
} else {
logWarning('Prettier not available or encountered errors. Skipping formatting.');
@@ -94,13 +108,17 @@ export function lintGeneratedFiles(outputDir: string): void {
if (!hasEslintConfig) {
logWarning('No ESLint config found in project root. Skipping ESLint fix.');
return;
return result;
}
const eslintOk = run('npx', ['eslint', '--fix', ...relativePaths], projectRoot);
if (eslintOk) {
const eslint = run('npx', ['eslint', '--fix', ...relativePaths], projectRoot);
if (eslint.output) logDetail('eslint', eslint.output);
if (eslint.success) {
result.eslint = { ran: true, filesFixed: files.length };
logSuccess(`ESLint fixed ${files.length} files`);
} else {
logWarning('ESLint reported errors that could not be auto-fixed. Review the output above.');
}
return result;
}

View File

@@ -1,7 +1,7 @@
import fs from 'fs-extra';
import path from 'path';
import { logStep, logSuccess } from '../utils/logger';
import type { SwaggerAnalysis, GenerationReport } from '../types';
import type { SwaggerAnalysis, GenerationReport, LintResult } from '../types';
/** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
function countMockFiles(dir: string): number {
@@ -22,14 +22,32 @@ function countSpecFiles(dir: string): number {
}
/** Generates and persists the `generation-report.json` file with process statistics. */
export function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport {
export function generateReport(
outputDir: string,
analysis: SwaggerAnalysis,
lintResult: LintResult
): GenerationReport {
logStep('Generating report...');
const tags = Array.isArray(analysis.tags) ? analysis.tags : [];
const tagDetails = tags.map((tag: unknown) => {
const t = tag as { name: string; description?: string };
const endpointCount = Object.values(analysis.paths).filter((pathObj) =>
Object.values(pathObj as Record<string, unknown>).some((op) => {
const operation = op as { tags?: string[] };
return operation.tags?.includes(t.name);
})
).length;
return { name: t.name, description: t.description || '', endpoints: endpointCount };
});
const report: GenerationReport = {
timestamp: new Date().toISOString(),
tags: analysis.tags.length,
endpoints: Object.keys(analysis.paths).length,
tagDetails,
outputDirectory: outputDir,
linting: lintResult,
structure: {
dtos: fs.readdirSync(path.join(outputDir, 'data/dtos')).length,
repositories: fs.readdirSync(path.join(outputDir, 'data/repositories')).length,

View File

@@ -1,6 +1,6 @@
import fs from 'fs-extra';
import yaml from 'js-yaml';
import { logStep, logInfo, logError } from '../utils/logger';
import { logStep, logSuccess, logError, logDetail } from '../utils/logger';
import type { SwaggerAnalysis } from '../types';
/** Parses an OpenAPI/Swagger file and extracts tags, paths and the full document. */
@@ -14,14 +14,15 @@ export function analyzeSwagger(swaggerFile: string): SwaggerAnalysis {
const tags = Array.isArray(swagger.tags) ? swagger.tags : [];
const paths = (swagger.paths as Record<string, unknown>) || {};
logInfo(`Found ${tags.length} tags in the API`);
logInfo(`Found ${Object.keys(paths).length} endpoints`);
logDetail('analyze', `Input: ${swaggerFile}`);
logDetail('analyze', `Found ${tags.length} tags, ${Object.keys(paths).length} endpoints`);
tags.forEach((tag: unknown) => {
const t = tag as { name: string; description?: string };
logInfo(` - ${t.name}: ${t.description || 'No description'}`);
logDetail('analyze', ` - ${t.name}: ${t.description || 'No description'}`);
});
logSuccess(`${tags.length} tags, ${Object.keys(paths).length} endpoints found`);
return { tags, paths, swagger };
} catch (error: unknown) {
const err = error as Error;

View File

@@ -11,6 +11,23 @@ export interface GeneratedCount {
specs: number;
}
/**
* Result returned by the lint/format step.
*/
export interface LintResult {
prettier: { ran: boolean; filesFormatted: number };
eslint: { ran: boolean; filesFixed: number };
}
/**
* Per-tag summary included in the generation report.
*/
export interface TagDetail {
name: string;
description: string;
endpoints: number;
}
/**
* Final generation report persisted as `generation-report.json`.
*/
@@ -18,7 +35,9 @@ export interface GenerationReport {
timestamp: string;
tags: number;
endpoints: number;
tagDetails: TagDetail[];
outputDirectory: string;
linting: LintResult;
structure: {
dtos: number;
repositories: number;

View File

@@ -1,3 +1,5 @@
import fs from 'fs-extra';
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
@@ -10,6 +12,21 @@ const colors = {
type Color = keyof typeof colors;
let _logFilePath: string | null = null;
/** Initialises the generation log file, overwriting any previous run. */
export function initGenerationLog(filePath: string): void {
_logFilePath = filePath;
fs.writeFileSync(filePath, `Generation log — ${new Date().toISOString()}\n${'='.repeat(60)}\n`);
}
/** Writes a detailed entry to the generation log file (not to console). */
export function logDetail(category: string, message: string): void {
if (!_logFilePath) return;
const line = `[${new Date().toISOString()}] [${category.toUpperCase().padEnd(8)}] ${message}\n`;
fs.appendFileSync(_logFilePath, line);
}
/** Prints a console message with the given ANSI colour. */
export function log(message: string, color: Color = 'reset'): void {
console.log(`${colors[color]}${message}${colors.reset}`);