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; }