Compare commits

...

9 Commits

Author SHA1 Message Date
cfe2eafe89 v4.3.0 2026-03-06 07:32:35 +00:00
b3d1982170 feat(mod_logger): add centralized TsBuildLogger and replace ad-hoc console output with structured, colored logging 2026-03-06 07:32:35 +00:00
83e0e9b5dd v4.2.6 2026-03-05 16:45:53 +00:00
094f9df55f fix(meta): no changes 2026-03-05 16:45:53 +00:00
a6a006aaec fix(compiler): move output directory cleaning to separate phase before compilation
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.
2026-03-05 16:45:07 +00:00
9477875c1d v4.2.5 2026-03-05 16:03:49 +00:00
2b73f3d582 fix(compiler): yield to the event loop after TypeScript emit to allow pending microtasks and I/O to settle before reading or modifying the output directory 2026-03-05 16:03:49 +00:00
0ffdcf852f v4.2.4 2026-03-05 16:00:01 +00:00
f8f20be4f4 fix(fshelpers): remove outdated comment about using synchronous rm to avoid XFS metadata corruption 2026-03-05 16:00:01 +00:00
10 changed files with 217 additions and 159 deletions

View File

@@ -1,5 +1,32 @@
# Changelog
## 2026-03-06 - 4.3.0 - feat(mod_logger)
add centralized TsBuildLogger and replace ad-hoc console output with structured, colored logging
- Add ts/mod_logger/classes.logger.ts providing header/step/detail/success/error/warn/indent utilities with ANSI color support
- Export logger from ts/mod_logger/index.ts and re-export from ts/index.ts
- Replace console.log/console.error/console.warn calls in mod_cli, mod_compiler, and mod_unpack with TsBuildLogger methods for consistent, hierarchical output
- Refactor error-summary, emit and type-check output to use logger separators, colors, and structured messages
## 2026-03-05 - 4.2.6 - fix(meta)
no changes
- Current package version: 4.2.5
- No code or file changes detected in this commit; no release required
## 2026-03-05 - 4.2.5 - fix(compiler)
yield to the event loop after TypeScript emit to allow pending microtasks and I/O to settle before reading or modifying the output directory
- Added await new Promise(resolve => process.nextTick(resolve)) immediately after program.emit()
- Prevents race conditions by allowing libuv write completions and other deferred callbacks to complete before accessing the output directory
- File changed: ts/mod_compiler/classes.tscompiler.ts
## 2026-03-05 - 4.2.4 - fix(fshelpers)
remove outdated comment about using synchronous rm to avoid XFS metadata corruption
- Comment-only change in ts/mod_fs/classes.fshelpers.ts; no runtime or API behavior changes
- Bump patch version from 4.2.3 to 4.2.4
## 2026-03-05 - 4.2.3 - fix(compiler)
defer unpacking until after all compilations and remove diagnostic filesystem syncs to avoid XFS metadata visibility issues

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsbuild",
"version": "4.2.3",
"version": "4.3.0",
"private": false,
"description": "A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.",
"main": "dist_ts/index.js",

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsbuild',
version: '4.2.3',
version: '4.3.0',
description: 'A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.'
}

View File

@@ -6,6 +6,7 @@ export * from './mod_fs/index.js';
export * from './mod_config/index.js';
export * from './mod_unpack/index.js';
export * from './mod_pathrewrite/index.js';
export * from './mod_logger/index.js';
export * from './mod_compiler/index.js';
export * from './mod_cli/index.js';

View File

@@ -5,6 +5,7 @@ import * as tspublish from '@git.zone/tspublish';
import { TsCompiler } from '../mod_compiler/index.js';
import { FsHelpers } from '../mod_fs/index.js';
import { TsBuildLogger as log } from '../mod_logger/index.js';
/**
* TsBuildCli handles all CLI commands for tsbuild.
@@ -78,21 +79,20 @@ export class TsBuildCli {
const patterns = argvArg._.slice(1);
if (patterns.length === 0) {
console.error('\n❌ Error: Please provide at least one TypeScript file path or glob pattern');
console.error(' Usage: tsbuild emitcheck <file_or_glob_pattern> [additional_patterns ...]\n');
console.error(' Example: tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"\n');
log.error('Please provide at least one TypeScript file path or glob pattern');
log.indent('Usage: tsbuild emitcheck <pattern> [...]');
log.indent('Example: tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"');
process.exit(1);
}
const allFiles = await this.collectFilesFromPatterns(patterns);
if (allFiles.length === 0) {
console.error('\n❌ Error: No TypeScript files found to check');
console.error(' Please verify your file paths or glob patterns.\n');
log.error('No TypeScript files found to check');
process.exit(1);
}
console.log(`\n🔎 Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
log.step('🔎', `Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
const compiler = new TsCompiler(this.cwd, argvArg);
const success = await compiler.checkEmit(allFiles);
@@ -153,19 +153,12 @@ export class TsBuildCli {
// Display compilation plan
const folderCount = sortedTsFolders.length;
console.log(`\n📂 TypeScript Folder Compilation Plan (${folderCount} folder${folderCount !== 1 ? 's' : ''})`);
console.log('┌' + '─'.repeat(60) + '┐');
console.log('│ 🔄 Compilation Order │');
console.log('├' + '─'.repeat(60) + '┤');
log.header('📂', `TypeScript Folder Compilation Plan (${folderCount} folder${folderCount !== 1 ? 's' : ''})`);
sortedTsFolders.forEach((folder, index) => {
const prefix = index === folderCount - 1 ? '└─' : '├─';
const position = `${index + 1}/${folderCount}`;
console.log(`${prefix} ${position.padStart(5)} ${folder.padEnd(46)}`);
log.indent(`${index + 1}. ${folder}`);
});
console.log('└' + '─'.repeat(60) + '┘\n');
// Build compilation object
const compilationCommandObject: Record<string, string> = {};
for (const tsFolder of sortedTsFolders) {
@@ -198,12 +191,11 @@ export class TsBuildCli {
const allFiles = await this.collectFilesFromPatterns(patterns);
if (allFiles.length === 0) {
console.error('\n❌ Error: No TypeScript files found to check');
console.error(' Please verify your file paths or glob patterns.\n');
log.error('No TypeScript files found to check');
process.exit(1);
}
console.log(`\n🔎 Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
log.step('🔎', `Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
const compiler = new TsCompiler(this.cwd, argvArg);
const success = await compiler.checkTypes(allFiles);
@@ -216,34 +208,34 @@ export class TsBuildCli {
* Run default type checks for ts/ and test/ directories
*/
private async runDefaultTypeChecks(argvArg: any): Promise<void> {
console.log('\n🔬 Running default type checking sequence...\n');
log.header('🔬', 'Default type checking sequence');
// First check ts/**/* without skiplibcheck
console.log('📂 Checking ts/**/* files...');
log.step('📂', 'Checking ts/**/* files...');
const tsTsFiles = await FsHelpers.listFilesWithGlob(this.cwd, 'ts/**/*.ts');
if (tsTsFiles.length > 0) {
console.log(` Found ${tsTsFiles.length} TypeScript files in ts/`);
log.detail('📄', `${tsTsFiles.length} TypeScript files`);
const tsAbsoluteFiles = smartpath.transform.toAbsolute(tsTsFiles, this.cwd) as string[];
const tsCompiler = new TsCompiler(this.cwd, argvArg);
const tsSuccess = await tsCompiler.checkTypes(tsAbsoluteFiles);
if (!tsSuccess) {
console.error('Type checking failed for ts/**/*');
log.error('Type checking failed for ts/**/*');
process.exit(1);
}
console.log('Type checking passed for ts/**/*\n');
log.success('Type checking passed for ts/**/*');
} else {
console.log(' No TypeScript files found in ts/\n');
log.detail('📄', 'No TypeScript files found in ts/');
}
// Then check test/**/* with skiplibcheck
console.log('📂 Checking test/**/* files with --skiplibcheck...');
log.step('📂', 'Checking test/**/* files with --skiplibcheck...');
const testTsFiles = await FsHelpers.listFilesWithGlob(this.cwd, 'test/**/*.ts');
if (testTsFiles.length > 0) {
console.log(` Found ${testTsFiles.length} TypeScript files in test/`);
log.detail('📄', `${testTsFiles.length} TypeScript files`);
const testAbsoluteFiles = smartpath.transform.toAbsolute(testTsFiles, this.cwd) as string[];
const testArgvArg = { ...argvArg, skiplibcheck: true };
@@ -251,15 +243,15 @@ export class TsBuildCli {
const testSuccess = await testCompiler.checkTypes(testAbsoluteFiles);
if (!testSuccess) {
console.error('Type checking failed for test/**/*');
log.error('Type checking failed for test/**/*');
process.exit(1);
}
console.log('Type checking passed for test/**/*\n');
log.success('Type checking passed for test/**/*');
} else {
console.log(' No TypeScript files found in test/\n');
log.detail('📄', 'No TypeScript files found in test/');
}
console.log('All default type checks passed!\n');
log.success('All default type checks passed!');
process.exit(0);
}
@@ -272,19 +264,19 @@ export class TsBuildCli {
for (const pattern of patterns) {
if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) {
// Handle as glob pattern
console.log(`Processing glob pattern: ${pattern}`);
log.step('🔎', `Processing glob pattern: ${pattern}`);
try {
const stringMatchedFiles = await FsHelpers.listFilesWithGlob(this.cwd, pattern);
if (stringMatchedFiles.length === 0) {
console.warn(`⚠️ Warning: No files matched the pattern '${pattern}'`);
log.warn(`No files matched pattern '${pattern}'`);
} else {
console.log(`📂 Found ${stringMatchedFiles.length} files matching pattern '${pattern}'`);
log.detail('📂', `${stringMatchedFiles.length} files matching '${pattern}'`);
const absoluteMatchedFiles = smartpath.transform.toAbsolute(stringMatchedFiles, this.cwd) as string[];
allFiles = allFiles.concat(absoluteMatchedFiles);
}
} catch (err) {
console.error(`Error processing glob pattern '${pattern}': ${err}`);
log.error(`Error processing glob pattern '${pattern}': ${err}`);
}
} else {
// Handle as direct file path
@@ -293,7 +285,7 @@ export class TsBuildCli {
if (fileExists) {
allFiles.push(filePath);
} else {
console.error(`❌ Error: File not found: ${filePath}`);
log.error(`File not found: ${filePath}`);
process.exit(1);
}
}

View File

@@ -8,6 +8,7 @@ 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';
import { TsBuildLogger as log } from '../mod_logger/index.js';
/**
* Interface for error summary data
@@ -117,33 +118,23 @@ export class TsCompiler {
}
const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary;
const c = log.c;
// Print error summary header
console.log('\n' + '='.repeat(80));
console.log('');
console.log(c.dim + log.separator() + c.reset);
console.log(
`❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}:`
`${c.red}❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}${c.reset}`
);
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',
};
console.log(c.dim + log.separator() + c.reset);
// 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}`
`\n ${c.cyan}File: ${displayPath}${c.reset} ${c.yellow}(${fileErrors.length} error${fileErrors.length !== 1 ? 's' : ''})${c.reset}`
);
console.log('-'.repeat(80));
console.log(` ${c.dim}${''.repeat(40)}${c.reset}`);
fileErrors.forEach((diagnostic) => {
if (diagnostic.file && diagnostic.start !== undefined) {
@@ -152,19 +143,18 @@ export class TsCompiler {
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}`
` ${c.white}Line ${line + 1}, Col ${character + 1}${c.reset}: ${c.brightRed}${errorCode}${c.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}`);
console.log(` ${lineContent.trimEnd()}`);
const indicator = ' '.repeat(character) + `${c.red}^${c.reset}`;
console.log(` ${indicator}`);
}
} catch {
// Failed to get source text, skip showing the code snippet
// Failed to get source text
}
}
});
@@ -172,17 +162,18 @@ export class TsCompiler {
// Print general errors
if (generalErrors.length > 0) {
console.log(`\n${colors.yellow}General Errors:${colors.reset}`);
console.log('-'.repeat(80));
console.log(`\n ${c.yellow}General Errors:${c.reset}`);
console.log(` ${c.dim}${''.repeat(40)}${c.reset}`);
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(` ${c.brightRed}${errorCode}${c.reset}: ${message}`);
});
}
console.log('\n' + '='.repeat(80) + '\n');
console.log('');
console.log(c.dim + log.separator() + c.reset);
}
/**
@@ -190,12 +181,11 @@ export class TsCompiler {
*/
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');
log.warn('WARNING: You are skipping libcheck... Is that really wanted?');
log.indent('Continuing in 5 seconds...');
await smartdelay.delayFor(5000);
} else if (!this.argvArg?.quiet && !this.argvArg?.json) {
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
log.warn('skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
}
}
@@ -213,17 +203,14 @@ export class TsCompiler {
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}`);
log.step('🔨', `[${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`);
log.detail('📁', `Output: ${relativeDestDir}`);
} else {
console.log(`🔨 Compiling ${fileNames.length} files...`);
log.step('🔨', `Compiling ${fileNames.length} files...`);
}
const done = smartpromise.defer<ICompileResult>();
@@ -233,17 +220,21 @@ export class TsCompiler {
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');
log.error('Pre-emit checks failed');
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
@@ -261,9 +252,9 @@ export class TsCompiler {
if (taskInfo) {
const { taskNumber, totalTasks } = taskInfo;
console.log(`[${taskNumber}/${totalTasks}] Task completed in ${duration}ms`);
log.success(`[${taskNumber}/${totalTasks}] Completed in ${duration}ms`);
} else {
console.log(`TypeScript emit succeeded! (${duration}ms)`);
log.success(`TypeScript emit succeeded (${duration}ms)`);
}
// Get count of emitted files by type
@@ -272,16 +263,13 @@ export class TsCompiler {
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`
);
log.detail('📄', `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');
log.error('TypeScript emit failed');
done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary });
}
@@ -317,44 +305,46 @@ export class TsCompiler {
const isJson = this.argvArg?.json === true;
if (!isQuiet && !isJson) {
console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`);
log.header('👷', `TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''})`);
Object.entries(globPatterns).forEach(([source, dest]) => {
console.log(` 📂 ${source}${dest}`);
log.detail('📂', `${source}${dest}`);
});
console.log('');
}
// Collect unpack tasks to perform AFTER all compilations complete.
// This prevents filesystem metadata corruption on XFS where heavy write
// activity during subsequent compilations can make freshly-renamed entries
// in previously-unpacked directories invisible or lost.
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = [];
// Phase 1: Resolve glob patterns and clean ALL output directories upfront.
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;
// Get files matching the glob pattern
const files = await FsHelpers.listFilesWithGlob(this.cwd, pattern);
// Transform to absolute paths
const absoluteFiles = smartpath.transform.toAbsolute(files, this.cwd) as string[];
// Get destination directory as absolute path
const destDir = smartpath.transform.toAbsolute(destPath, this.cwd) as string;
// Clear the destination directory before compilation if it exists
if (await FsHelpers.directoryExists(destDir)) {
if (!isQuiet && !isJson) {
console.log(`🧹 Clearing output directory: ${destPath}`);
log.step('🧹', `Clearing output directory: ${destPath}`);
}
await FsHelpers.removeDirectory(destDir);
}
// Update compiler options with the output directory
resolvedTasks.push({ pattern, destPath, destDir, absoluteFiles });
}
// Phase 2: Compile all tasks.
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = [];
for (const task of resolvedTasks) {
const options: CompilerOptions = {
...customOptions,
outDir: destDir,
outDir: task.destDir,
listEmittedFiles: true,
};
@@ -362,32 +352,27 @@ export class TsCompiler {
const taskInfo: ITaskInfo = {
taskNumber: currentTask,
totalTasks,
sourcePattern: pattern,
destDir: destPath,
fileCount: absoluteFiles.length,
sourcePattern: task.pattern,
destDir: task.destPath,
fileCount: task.absoluteFiles.length,
};
const result = await this.compileFiles(absoluteFiles, options, taskInfo);
const result = await this.compileFiles(task.absoluteFiles, options, taskInfo);
emittedFiles.push(...result.emittedFiles);
errorSummaries.push(result.errorSummary);
// Queue unpack for after all compilations (don't modify output dirs between compilations)
if (result.errorSummary.totalErrors === 0) {
pendingUnpacks.push({ pattern, destDir });
successfulOutputDirs.push(destDir);
pendingUnpacks.push({ pattern: task.pattern, destDir: task.destDir });
successfulOutputDirs.push(task.destDir);
}
}
// 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.
// Phase 3: Perform all unpacks after all compilations are done.
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
// Rewrite import paths in all output directories
if (successfulOutputDirs.length > 0) {
const rewriter = await TsPathRewriter.fromProjectDirectory(this.cwd);
let totalRewritten = 0;
@@ -395,7 +380,7 @@ export class TsCompiler {
totalRewritten += await rewriter.rewriteDirectory(outputDir);
}
if (totalRewritten > 0 && !isQuiet && !isJson) {
console.log(` 🔄 Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`);
log.detail('🔄', `Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`);
}
}
@@ -444,7 +429,7 @@ export class TsCompiler {
const options = { ...this.createOptions(customOptions), noEmit: true };
const fileCount = fileNames.length;
console.log(`\n🔍 Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`);
log.step('🔍', `Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`);
const program = this.createProgram(fileNames, options);
@@ -464,12 +449,10 @@ export class TsCompiler {
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`);
log.success(`Emit check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`);
} 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');
log.error('Emit check failed');
}
return success;
@@ -482,7 +465,7 @@ export class TsCompiler {
const options = { ...this.createOptions(customOptions), noEmit: true };
const fileCount = fileNames.length;
console.log(`\n🔍 Type checking ${fileCount} TypeScript file${fileCount !== 1 ? 's' : ''}...`);
log.step('🔍', `Type checking ${fileCount} file${fileCount !== 1 ? 's' : ''}...`);
const program = this.createProgram(fileNames, options);
const diagnostics = typescript.getPreEmitDiagnostics(program);
@@ -491,12 +474,10 @@ export class TsCompiler {
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`);
log.success(`Type check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`);
} 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');
log.error('Type check failed');
}
return success;
@@ -534,42 +515,28 @@ export class TsCompiler {
* Display final compilation summary
*/
private displayFinalSummary(errorSummary: IErrorSummary): void {
const c = log.c;
if (errorSummary.totalErrors === 0) {
console.log('\n📊 \x1b[32mCompilation Summary: All tasks completed successfully! ✅\x1b[0m\n');
log.header('📊', `${c.green}Compilation Summary: All tasks completed successfully! ✅${c.reset}`);
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}`);
}
log.header('📊', 'Compilation Summary');
console.log(
`\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}`
`${c.brightRed} ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${c.reset}`
);
console.log('='.repeat(80) + '\n');
Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => {
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
log.indent(
`${c.red}${c.reset} ${c.cyan}${displayPath}${c.reset} ${c.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${c.reset}`
);
});
if (errorSummary.generalErrors.length > 0) {
console.log(`${c.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${c.reset}`);
}
}
}

View File

@@ -123,9 +123,6 @@ export class FsHelpers {
/**
* Remove a directory recursively.
* Uses synchronous rm to avoid XFS metadata corruption observed with
* async fs.promises.rm affecting sibling directory entries on the
* libuv thread pool under signal pressure.
*/
public static async removeDirectory(dirPath: string): Promise<void> {
fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });

View File

@@ -0,0 +1,72 @@
/**
* Centralized console output for tsbuild.
*
* Visual hierarchy (4 levels):
* HEADER — top-level section start (emoji + bold text + separator line)
* STEP — major action within a section (emoji + text, no indent)
* DETAIL — supplementary info under a step (3-space indent + emoji + text)
* SUCCESS/ERROR/WARN — outcome indicators (emoji + text, no indent)
*/
export class TsBuildLogger {
static readonly c = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
white: '\x1b[37m',
brightRed: '\x1b[91m',
brightGreen: '\x1b[92m',
brightYellow: '\x1b[93m',
};
static readonly SEPARATOR_WIDTH = 70;
static separator(char = '─'): string {
return char.repeat(this.SEPARATOR_WIDTH);
}
/** Level 1: Section header. Blank line before, separator after. */
static header(emoji: string, text: string): void {
console.log('');
console.log(`${emoji} ${this.c.bold}${text}${this.c.reset}`);
console.log(this.c.dim + this.separator() + this.c.reset);
}
/** Level 2: Step within a section. No indent. */
static step(emoji: string, text: string): void {
console.log(`${emoji} ${text}`);
}
/** Level 3: Detail under a step. 3-space indent. */
static detail(emoji: string, text: string): void {
console.log(` ${emoji} ${text}`);
}
/** Outcome: success */
static success(text: string): void {
console.log(`${this.c.green}${text}${this.c.reset}`);
}
/** Outcome: error (goes to stderr) */
static error(text: string): void {
console.error(`${this.c.red}${text}${this.c.reset}`);
}
/** Outcome: warning */
static warn(text: string): void {
console.log(`${this.c.yellow}⚠️ ${text}${this.c.reset}`);
}
/** Plain indented line (for code snippets, list items, etc.) */
static indent(text: string, level = 1): void {
console.log(' '.repeat(level) + text);
}
/** Blank line */
static blank(): void {
console.log('');
}
}

1
ts/mod_logger/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './classes.logger.js';

View File

@@ -2,6 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { TsPublishConfig } from '../mod_config/index.js';
import { FsHelpers } from '../mod_fs/index.js';
import { TsBuildLogger as log } from '../mod_logger/index.js';
/**
* TsUnpacker handles flattening of nested TypeScript output directories.
@@ -127,7 +128,7 @@ export class TsUnpacker {
// Step 3: Remove the now-empty nested directory
fs.rmdirSync(nestedPath);
console.log(` 📦 Unpacked ${this.sourceFolderName}: ${nestedEntries.length} entries`);
log.detail('📦', `Unpacked ${this.sourceFolderName}: ${nestedEntries.length} entries`);
return true;
}