Files
openapi-clean-arch-gen/src/generators/lint.generator.ts
didavila 79ea7dfc7e
All checks were successful
Lint / lint (pull_request) Successful in 16s
feat: enhance logging and linting functionality with detailed reports
2026-03-26 13:03:10 +01:00

125 lines
4.5 KiB
TypeScript

import fs from 'fs-extra';
import path from 'path';
import { spawnSync } from 'child_process';
import { logStep, logSuccess, logWarning, logDetail } from '../utils/logger';
import type { LintResult } from '../types';
/**
* Walks up the directory tree from `startDir` to find the nearest
* directory containing a `package.json` (i.e. the project root).
* Returns `null` if none is found before reaching the filesystem root.
*/
function findProjectRoot(startDir: string): string | null {
let current = path.resolve(startDir);
while (true) {
if (fs.existsSync(path.join(current, 'package.json'))) return current;
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
}
}
/**
* Collects all `.ts` files recursively inside a directory.
*/
function collectTsFiles(dir: string): string[] {
if (!fs.existsSync(dir)) return [];
const results: string[] = [];
fs.readdirSync(dir).forEach((entry) => {
const full = path.join(dir, entry);
if (fs.statSync(full).isDirectory()) {
results.push(...collectTsFiles(full));
} else if (entry.endsWith('.ts')) {
results.push(full);
}
});
return results;
}
/**
* 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): { success: boolean; output: string } {
const result = spawnSync(cmd, args, { cwd, encoding: 'utf8', shell: true });
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 { success: !fatalError, output };
}
/**
* Runs Prettier and ESLint (--fix) on all generated `.ts` files inside `outputDir`.
* Both tools are looked up via `npx` in the nearest project root so that the
* target Angular project's own configuration and plugins are used.
*
* - 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): 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 result;
}
const files = collectTsFiles(outputDir);
if (files.length === 0) {
logWarning('No TypeScript files found in output directory. Skipping lint.');
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 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.');
}
// --- ESLint (only if a config exists in the project root) ---
const hasEslintConfig =
fs.existsSync(path.join(projectRoot, 'eslint.config.js')) ||
fs.existsSync(path.join(projectRoot, 'eslint.config.mjs')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.js')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.json')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.yml')) ||
fs.existsSync(path.join(projectRoot, '.eslintrc.yaml'));
if (!hasEslintConfig) {
logWarning('No ESLint config found in project root. Skipping ESLint fix.');
return result;
}
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;
}