Restructured compileGlob() into three distinct phases: 1. Resolve glob patterns and clean ALL output directories upfront 2. Compile all TypeScript tasks (no filesystem cleanup during this phase) 3. Unpack all outputs after all compilations complete This prevents XFS metadata corruption from rm operations overlapping with TypeScript compilation writes to sibling directories.
586 lines
21 KiB
TypeScript
586 lines
21 KiB
TypeScript
import type { CompilerOptions, Diagnostic, Program } from 'typescript';
|
||
import typescript from 'typescript';
|
||
import * as smartdelay from '@push.rocks/smartdelay';
|
||
import * as smartpromise from '@push.rocks/smartpromise';
|
||
import * as smartpath from '@push.rocks/smartpath';
|
||
|
||
import { TsConfig } from '../mod_config/index.js';
|
||
import { FsHelpers } from '../mod_fs/index.js';
|
||
import { performUnpack } from '../mod_unpack/index.js';
|
||
import { TsPathRewriter } from '../mod_pathrewrite/index.js';
|
||
|
||
/**
|
||
* Interface for error summary data
|
||
*/
|
||
export interface IErrorSummary {
|
||
errorsByFile: Record<string, Diagnostic[]>;
|
||
generalErrors: Diagnostic[];
|
||
totalErrors: number;
|
||
totalFiles: number;
|
||
}
|
||
|
||
/**
|
||
* Interface for task information
|
||
*/
|
||
export interface ITaskInfo {
|
||
taskNumber: number;
|
||
totalTasks: number;
|
||
sourcePattern: string;
|
||
destDir: string;
|
||
fileCount: number;
|
||
}
|
||
|
||
/**
|
||
* Interface for compilation result
|
||
*/
|
||
export interface ICompileResult {
|
||
emittedFiles: string[];
|
||
errorSummary: IErrorSummary;
|
||
}
|
||
|
||
/**
|
||
* TsCompiler handles TypeScript compilation with error tracking,
|
||
* configuration management, and output unpacking.
|
||
*/
|
||
export class TsCompiler {
|
||
private config: TsConfig;
|
||
private cwd: string;
|
||
private argvArg?: any;
|
||
|
||
constructor(cwd: string = process.cwd(), argvArg?: any) {
|
||
this.cwd = cwd;
|
||
this.config = new TsConfig(cwd);
|
||
this.argvArg = argvArg;
|
||
}
|
||
|
||
/**
|
||
* Get the current working directory
|
||
*/
|
||
public getCwd(): string {
|
||
return this.cwd;
|
||
}
|
||
|
||
/**
|
||
* Get the TsConfig instance
|
||
*/
|
||
public getConfig(): TsConfig {
|
||
return this.config;
|
||
}
|
||
|
||
/**
|
||
* Create compiler options by merging defaults, tsconfig.json, and custom options
|
||
*/
|
||
public createOptions(customOptions: CompilerOptions = {}): CompilerOptions {
|
||
return this.config.merge(customOptions, this.argvArg);
|
||
}
|
||
|
||
/**
|
||
* Create a TypeScript program from file names and options
|
||
*/
|
||
private createProgram(fileNames: string[], options: CompilerOptions): Program {
|
||
return typescript.createProgram(fileNames, options);
|
||
}
|
||
|
||
/**
|
||
* Process TypeScript diagnostics and return error summary
|
||
*/
|
||
private processDiagnostics(diagnostics: readonly Diagnostic[]): IErrorSummary {
|
||
const errorsByFile: Record<string, Diagnostic[]> = {};
|
||
const generalErrors: Diagnostic[] = [];
|
||
|
||
diagnostics.forEach((diagnostic) => {
|
||
if (diagnostic.file) {
|
||
const fileName = diagnostic.file.fileName;
|
||
if (!errorsByFile[fileName]) {
|
||
errorsByFile[fileName] = [];
|
||
}
|
||
errorsByFile[fileName].push(diagnostic);
|
||
} else {
|
||
generalErrors.push(diagnostic);
|
||
}
|
||
});
|
||
|
||
return {
|
||
errorsByFile,
|
||
generalErrors,
|
||
totalErrors: diagnostics.length,
|
||
totalFiles: Object.keys(errorsByFile).length,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Display error summary to console
|
||
*/
|
||
private displayErrorSummary(errorSummary: IErrorSummary): void {
|
||
if (errorSummary.totalErrors === 0) {
|
||
return;
|
||
}
|
||
|
||
const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary;
|
||
|
||
// Print error summary header
|
||
console.log('\n' + '='.repeat(80));
|
||
console.log(
|
||
`❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}:`
|
||
);
|
||
console.log('='.repeat(80));
|
||
|
||
// Color codes for error formatting
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
red: '\x1b[31m',
|
||
yellow: '\x1b[33m',
|
||
cyan: '\x1b[36m',
|
||
white: '\x1b[37m',
|
||
brightRed: '\x1b[91m',
|
||
};
|
||
|
||
// Print file-specific errors
|
||
Object.entries(errorsByFile).forEach(([fileName, fileErrors]) => {
|
||
// Show relative path if possible for cleaner output
|
||
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
|
||
|
||
console.log(
|
||
`\n${colors.cyan}File: ${displayPath} ${colors.yellow}(${fileErrors.length} error${fileErrors.length !== 1 ? 's' : ''})${colors.reset}`
|
||
);
|
||
console.log('-'.repeat(80));
|
||
|
||
fileErrors.forEach((diagnostic) => {
|
||
if (diagnostic.file && diagnostic.start !== undefined) {
|
||
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
||
const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
||
const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error';
|
||
|
||
console.log(
|
||
`${colors.white}Line ${line + 1}, Col ${character + 1}${colors.reset}: ${colors.brightRed}${errorCode}${colors.reset} - ${message}`
|
||
);
|
||
|
||
// Try to show the code snippet if possible
|
||
try {
|
||
const lineContent = diagnostic.file.text.split('\n')[line];
|
||
if (lineContent) {
|
||
console.log(` ${lineContent.trimEnd()}`);
|
||
const indicator = ' '.repeat(character) + `${colors.red}^${colors.reset}`;
|
||
console.log(` ${indicator}`);
|
||
}
|
||
} catch {
|
||
// Failed to get source text, skip showing the code snippet
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Print general errors
|
||
if (generalErrors.length > 0) {
|
||
console.log(`\n${colors.yellow}General Errors:${colors.reset}`);
|
||
console.log('-'.repeat(80));
|
||
|
||
generalErrors.forEach((diagnostic) => {
|
||
const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
||
const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error';
|
||
console.log(`${colors.brightRed}${errorCode}${colors.reset}: ${message}`);
|
||
});
|
||
}
|
||
|
||
console.log('\n' + '='.repeat(80) + '\n');
|
||
}
|
||
|
||
/**
|
||
* Handle skipLibCheck warning display
|
||
*/
|
||
private async handleSkipLibCheckWarning(): Promise<void> {
|
||
if (this.argvArg?.confirmskiplibcheck) {
|
||
console.log('\n⚠️ WARNING ⚠️');
|
||
console.log('You are skipping libcheck... Is that really wanted?');
|
||
console.log('Continuing in 5 seconds...\n');
|
||
await smartdelay.delayFor(5000);
|
||
} else if (!this.argvArg?.quiet && !this.argvArg?.json) {
|
||
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Compile files with error tracking (returns result instead of throwing)
|
||
*/
|
||
public async compileFiles(
|
||
fileNames: string[],
|
||
customOptions: CompilerOptions = {},
|
||
taskInfo?: ITaskInfo
|
||
): Promise<ICompileResult> {
|
||
const options = this.createOptions(customOptions);
|
||
|
||
if (options.skipLibCheck) {
|
||
await this.handleSkipLibCheckWarning();
|
||
}
|
||
|
||
// Enhanced logging with task info
|
||
const startTime = Date.now();
|
||
if (taskInfo) {
|
||
const { taskNumber, totalTasks, sourcePattern, fileCount } = taskInfo;
|
||
const relativeDestDir = taskInfo.destDir.replace(process.cwd(), '').replace(/^\//, '');
|
||
console.log(
|
||
`\n🔨 [${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`
|
||
);
|
||
console.log(` 📁 Output: ${relativeDestDir}`);
|
||
} else {
|
||
console.log(`🔨 Compiling ${fileNames.length} files...`);
|
||
}
|
||
|
||
const done = smartpromise.defer<ICompileResult>();
|
||
const program = this.createProgram(fileNames, options);
|
||
|
||
// Check for pre-emit diagnostics first
|
||
const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program);
|
||
const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics);
|
||
|
||
// Only continue to emit phase if no pre-emit errors
|
||
if (preEmitErrorSummary.totalErrors > 0) {
|
||
this.displayErrorSummary(preEmitErrorSummary);
|
||
console.error('\n❌ TypeScript pre-emit checks failed. Please fix the issues listed above before proceeding.');
|
||
console.error(' Type errors must be resolved before the compiler can emit output files.\n');
|
||
done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary });
|
||
return done.promise;
|
||
}
|
||
|
||
// If no pre-emit errors, proceed with emit
|
||
const emitResult = program.emit();
|
||
|
||
// Yield to the event loop so any pending microtasks, nextTick callbacks,
|
||
// or deferred I/O from TypeScript's emit (e.g. libuv write completions)
|
||
// can settle before we read or modify the output directory.
|
||
await new Promise<void>((resolve) => process.nextTick(resolve));
|
||
|
||
const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
|
||
|
||
// Combine error summaries
|
||
const combinedErrorSummary: IErrorSummary = {
|
||
errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile },
|
||
generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors],
|
||
totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors,
|
||
totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length,
|
||
};
|
||
|
||
const exitCode = emitResult.emitSkipped ? 1 : 0;
|
||
if (exitCode === 0) {
|
||
const endTime = Date.now();
|
||
const duration = endTime - startTime;
|
||
|
||
if (taskInfo) {
|
||
const { taskNumber, totalTasks } = taskInfo;
|
||
console.log(`✅ [${taskNumber}/${totalTasks}] Task completed in ${duration}ms`);
|
||
} else {
|
||
console.log(`✅ TypeScript emit succeeded! (${duration}ms)`);
|
||
}
|
||
|
||
// Get count of emitted files by type
|
||
const jsFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.js')).length || 0;
|
||
const dtsFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.d.ts')).length || 0;
|
||
const mapFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.map')).length || 0;
|
||
|
||
if (emitResult.emittedFiles && emitResult.emittedFiles.length > 0) {
|
||
console.log(
|
||
` 📄 Generated ${emitResult.emittedFiles.length} files: ${jsFiles} .js, ${dtsFiles} .d.ts, ${mapFiles} source maps`
|
||
);
|
||
}
|
||
|
||
done.resolve({ emittedFiles: emitResult.emittedFiles || [], errorSummary: combinedErrorSummary });
|
||
} else {
|
||
this.displayErrorSummary(combinedErrorSummary);
|
||
console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!');
|
||
console.error(' No output files have been generated.\n');
|
||
done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary });
|
||
}
|
||
|
||
return done.promise;
|
||
}
|
||
|
||
/**
|
||
* Compile files (throws on error)
|
||
*/
|
||
public async compileFilesOrThrow(fileNames: string[], customOptions: CompilerOptions = {}): Promise<string[]> {
|
||
const result = await this.compileFiles(fileNames, customOptions);
|
||
if (result.errorSummary.totalErrors > 0) {
|
||
throw new Error('TypeScript compilation failed.');
|
||
}
|
||
return result.emittedFiles;
|
||
}
|
||
|
||
/**
|
||
* Compile glob patterns with automatic unpacking
|
||
*/
|
||
public async compileGlob(
|
||
globPatterns: Record<string, string>,
|
||
customOptions: CompilerOptions = {}
|
||
): Promise<ICompileResult> {
|
||
const emittedFiles: string[] = [];
|
||
const errorSummaries: IErrorSummary[] = [];
|
||
const successfulOutputDirs: string[] = [];
|
||
|
||
const totalTasks = Object.keys(globPatterns).length;
|
||
let currentTask = 0;
|
||
|
||
const isQuiet = this.argvArg?.quiet === true;
|
||
const isJson = this.argvArg?.json === true;
|
||
|
||
if (!isQuiet && !isJson) {
|
||
console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`);
|
||
Object.entries(globPatterns).forEach(([source, dest]) => {
|
||
console.log(` 📂 ${source} → ${dest}`);
|
||
});
|
||
console.log('');
|
||
}
|
||
|
||
// Phase 1: Resolve glob patterns and clean ALL output directories upfront.
|
||
// This ensures no rm/rmSync activity overlaps with TypeScript compilation,
|
||
// preventing XFS metadata corruption from concurrent metadata operations.
|
||
interface IResolvedTask {
|
||
pattern: string;
|
||
destPath: string;
|
||
destDir: string;
|
||
absoluteFiles: string[];
|
||
}
|
||
const resolvedTasks: IResolvedTask[] = [];
|
||
|
||
for (const pattern of Object.keys(globPatterns)) {
|
||
const destPath = globPatterns[pattern];
|
||
if (!pattern || !destPath) continue;
|
||
|
||
const files = await FsHelpers.listFilesWithGlob(this.cwd, pattern);
|
||
const absoluteFiles = smartpath.transform.toAbsolute(files, this.cwd) as string[];
|
||
const destDir = smartpath.transform.toAbsolute(destPath, this.cwd) as string;
|
||
|
||
if (await FsHelpers.directoryExists(destDir)) {
|
||
if (!isQuiet && !isJson) {
|
||
console.log(`🧹 Clearing output directory: ${destPath}`);
|
||
}
|
||
await FsHelpers.removeDirectory(destDir);
|
||
}
|
||
|
||
resolvedTasks.push({ pattern, destPath, destDir, absoluteFiles });
|
||
}
|
||
|
||
// Phase 2: Compile all tasks. No filesystem cleanup happens during this phase.
|
||
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = [];
|
||
|
||
for (const task of resolvedTasks) {
|
||
const options: CompilerOptions = {
|
||
...customOptions,
|
||
outDir: task.destDir,
|
||
listEmittedFiles: true,
|
||
};
|
||
|
||
currentTask++;
|
||
const taskInfo: ITaskInfo = {
|
||
taskNumber: currentTask,
|
||
totalTasks,
|
||
sourcePattern: task.pattern,
|
||
destDir: task.destPath,
|
||
fileCount: task.absoluteFiles.length,
|
||
};
|
||
|
||
const result = await this.compileFiles(task.absoluteFiles, options, taskInfo);
|
||
emittedFiles.push(...result.emittedFiles);
|
||
errorSummaries.push(result.errorSummary);
|
||
|
||
if (result.errorSummary.totalErrors === 0) {
|
||
pendingUnpacks.push({ pattern: task.pattern, destDir: task.destDir });
|
||
successfulOutputDirs.push(task.destDir);
|
||
}
|
||
}
|
||
|
||
// Phase 3: Perform all unpacks after all compilations are done.
|
||
// This ensures no output directory is modified while subsequent compilations
|
||
// are performing heavy filesystem writes to sibling directories.
|
||
for (const { pattern, destDir } of pendingUnpacks) {
|
||
await performUnpack(pattern, destDir, this.cwd);
|
||
}
|
||
|
||
// Rewrite import paths in all output directories to handle cross-module references
|
||
// This must happen after ALL compilations so all destination folders exist
|
||
// Use fromProjectDirectory to detect ALL ts_* folders, not just the ones being compiled
|
||
if (successfulOutputDirs.length > 0) {
|
||
const rewriter = await TsPathRewriter.fromProjectDirectory(this.cwd);
|
||
let totalRewritten = 0;
|
||
for (const outputDir of successfulOutputDirs) {
|
||
totalRewritten += await rewriter.rewriteDirectory(outputDir);
|
||
}
|
||
if (totalRewritten > 0 && !isQuiet && !isJson) {
|
||
console.log(` 🔄 Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`);
|
||
}
|
||
}
|
||
|
||
// Merge all error summaries
|
||
const finalErrorSummary = this.mergeErrorSummaries(errorSummaries);
|
||
|
||
// Output summary based on mode
|
||
if (isJson) {
|
||
const result = {
|
||
success: finalErrorSummary.totalErrors === 0,
|
||
totals: {
|
||
errors: finalErrorSummary.totalErrors,
|
||
filesWithErrors: finalErrorSummary.totalFiles,
|
||
tasks: totalTasks,
|
||
},
|
||
errorsByFile: Object.fromEntries(
|
||
Object.entries(finalErrorSummary.errorsByFile).map(([file, diags]) => [
|
||
file,
|
||
diags.map((d) => ({
|
||
code: d.code,
|
||
message: typescript.flattenDiagnosticMessageText(d.messageText as any, '\n'),
|
||
})),
|
||
])
|
||
),
|
||
};
|
||
console.log(JSON.stringify(result));
|
||
} else if (!isQuiet) {
|
||
this.displayFinalSummary(finalErrorSummary);
|
||
}
|
||
|
||
// Attach summary to argvArg for CLI exit behavior
|
||
if (this.argvArg && typeof this.argvArg === 'object') {
|
||
(this.argvArg as any).__tsbuildFinalErrorSummary = finalErrorSummary;
|
||
}
|
||
|
||
return {
|
||
emittedFiles,
|
||
errorSummary: finalErrorSummary,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Check if files can be emitted without actually emitting
|
||
*/
|
||
public async checkEmit(fileNames: string[], customOptions: CompilerOptions = {}): Promise<boolean> {
|
||
const options = { ...this.createOptions(customOptions), noEmit: true };
|
||
const fileCount = fileNames.length;
|
||
|
||
console.log(`\n🔍 Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`);
|
||
|
||
const program = this.createProgram(fileNames, options);
|
||
|
||
const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program);
|
||
const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics);
|
||
|
||
const emitResult = program.emit(undefined, undefined, undefined, true);
|
||
const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
|
||
|
||
const combinedErrorSummary: IErrorSummary = {
|
||
errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile },
|
||
generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors],
|
||
totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors,
|
||
totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length,
|
||
};
|
||
|
||
const success = combinedErrorSummary.totalErrors === 0 && !emitResult.emitSkipped;
|
||
|
||
if (success) {
|
||
console.log('\n✅ TypeScript emit check passed! All files can be emitted successfully.');
|
||
console.log(` ${fileCount} file${fileCount !== 1 ? 's' : ''} ${fileCount !== 1 ? 'are' : 'is'} ready to be compiled.\n`);
|
||
} else {
|
||
this.displayErrorSummary(combinedErrorSummary);
|
||
console.error('\n❌ TypeScript emit check failed. Please fix the issues listed above.');
|
||
console.error(' The compilation cannot proceed until these errors are resolved.\n');
|
||
}
|
||
|
||
return success;
|
||
}
|
||
|
||
/**
|
||
* Check TypeScript files for type errors without emission
|
||
*/
|
||
public async checkTypes(fileNames: string[], customOptions: CompilerOptions = {}): Promise<boolean> {
|
||
const options = { ...this.createOptions(customOptions), noEmit: true };
|
||
const fileCount = fileNames.length;
|
||
|
||
console.log(`\n🔍 Type checking ${fileCount} TypeScript file${fileCount !== 1 ? 's' : ''}...`);
|
||
|
||
const program = this.createProgram(fileNames, options);
|
||
const diagnostics = typescript.getPreEmitDiagnostics(program);
|
||
const errorSummary = this.processDiagnostics(diagnostics);
|
||
|
||
const success = errorSummary.totalErrors === 0;
|
||
|
||
if (success) {
|
||
console.log('\n✅ TypeScript type check passed! No type errors found.');
|
||
console.log(` All ${fileCount} file${fileCount !== 1 ? 's' : ''} passed type checking successfully.\n`);
|
||
} else {
|
||
this.displayErrorSummary(errorSummary);
|
||
console.error('\n❌ TypeScript type check failed. Please fix the type errors listed above.');
|
||
console.error(' The type checker found issues that need to be resolved.\n');
|
||
}
|
||
|
||
return success;
|
||
}
|
||
|
||
/**
|
||
* Merge multiple error summaries into one
|
||
*/
|
||
private mergeErrorSummaries(summaries: IErrorSummary[]): IErrorSummary {
|
||
const mergedErrorsByFile: Record<string, Diagnostic[]> = {};
|
||
const mergedGeneralErrors: Diagnostic[] = [];
|
||
let totalErrors = 0;
|
||
|
||
summaries.forEach((summary) => {
|
||
Object.entries(summary.errorsByFile).forEach(([fileName, errors]) => {
|
||
if (!mergedErrorsByFile[fileName]) {
|
||
mergedErrorsByFile[fileName] = [];
|
||
}
|
||
mergedErrorsByFile[fileName] = mergedErrorsByFile[fileName].concat(errors);
|
||
});
|
||
|
||
mergedGeneralErrors.push(...summary.generalErrors);
|
||
totalErrors += summary.totalErrors;
|
||
});
|
||
|
||
return {
|
||
errorsByFile: mergedErrorsByFile,
|
||
generalErrors: mergedGeneralErrors,
|
||
totalErrors,
|
||
totalFiles: Object.keys(mergedErrorsByFile).length,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Display final compilation summary
|
||
*/
|
||
private displayFinalSummary(errorSummary: IErrorSummary): void {
|
||
if (errorSummary.totalErrors === 0) {
|
||
console.log('\n📊 \x1b[32mCompilation Summary: All tasks completed successfully! ✅\x1b[0m\n');
|
||
return;
|
||
}
|
||
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
red: '\x1b[31m',
|
||
yellow: '\x1b[33m',
|
||
cyan: '\x1b[36m',
|
||
brightRed: '\x1b[91m',
|
||
brightYellow: '\x1b[93m',
|
||
};
|
||
|
||
console.log('\n' + '='.repeat(80));
|
||
console.log(`📊 ${colors.brightYellow}Final Compilation Summary${colors.reset}`);
|
||
console.log('='.repeat(80));
|
||
|
||
if (errorSummary.totalFiles > 0) {
|
||
console.log(`${colors.brightRed}❌ Files with errors (${errorSummary.totalFiles}):${colors.reset}`);
|
||
|
||
Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => {
|
||
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
|
||
console.log(
|
||
` ${colors.red}•${colors.reset} ${colors.cyan}${displayPath}${colors.reset} ${colors.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${colors.reset}`
|
||
);
|
||
});
|
||
}
|
||
|
||
if (errorSummary.generalErrors.length > 0) {
|
||
console.log(`${colors.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${colors.reset}`);
|
||
}
|
||
|
||
console.log(
|
||
`\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}`
|
||
);
|
||
console.log('='.repeat(80) + '\n');
|
||
}
|
||
}
|