Compare commits

...

5 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
9 changed files with 198 additions and 156 deletions

View File

@@ -1,5 +1,19 @@
# Changelog # 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) ## 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 yield to the event loop after TypeScript emit to allow pending microtasks and I/O to settle before reading or modifying the output directory

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import { TsConfig } from '../mod_config/index.js';
import { FsHelpers } from '../mod_fs/index.js'; import { FsHelpers } from '../mod_fs/index.js';
import { performUnpack } from '../mod_unpack/index.js'; import { performUnpack } from '../mod_unpack/index.js';
import { TsPathRewriter } from '../mod_pathrewrite/index.js'; import { TsPathRewriter } from '../mod_pathrewrite/index.js';
import { TsBuildLogger as log } from '../mod_logger/index.js';
/** /**
* Interface for error summary data * Interface for error summary data
@@ -117,33 +118,23 @@ export class TsCompiler {
} }
const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary; const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary;
const c = log.c;
// Print error summary header console.log('');
console.log('\n' + '='.repeat(80)); console.log(c.dim + log.separator() + c.reset);
console.log( 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)); console.log(c.dim + log.separator() + c.reset);
// 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 // Print file-specific errors
Object.entries(errorsByFile).forEach(([fileName, fileErrors]) => { Object.entries(errorsByFile).forEach(([fileName, fileErrors]) => {
// Show relative path if possible for cleaner output
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, ''); const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
console.log( 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) => { fileErrors.forEach((diagnostic) => {
if (diagnostic.file && diagnostic.start !== undefined) { if (diagnostic.file && diagnostic.start !== undefined) {
@@ -152,19 +143,18 @@ export class TsCompiler {
const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error'; const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error';
console.log( 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 { try {
const lineContent = diagnostic.file.text.split('\n')[line]; const lineContent = diagnostic.file.text.split('\n')[line];
if (lineContent) { if (lineContent) {
console.log(` ${lineContent.trimEnd()}`); console.log(` ${lineContent.trimEnd()}`);
const indicator = ' '.repeat(character) + `${colors.red}^${colors.reset}`; const indicator = ' '.repeat(character) + `${c.red}^${c.reset}`;
console.log(` ${indicator}`); console.log(` ${indicator}`);
} }
} catch { } 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 // Print general errors
if (generalErrors.length > 0) { if (generalErrors.length > 0) {
console.log(`\n${colors.yellow}General Errors:${colors.reset}`); console.log(`\n ${c.yellow}General Errors:${c.reset}`);
console.log('-'.repeat(80)); console.log(` ${c.dim}${''.repeat(40)}${c.reset}`);
generalErrors.forEach((diagnostic) => { generalErrors.forEach((diagnostic) => {
const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error'; 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> { private async handleSkipLibCheckWarning(): Promise<void> {
if (this.argvArg?.confirmskiplibcheck) { if (this.argvArg?.confirmskiplibcheck) {
console.log('\n⚠ WARNING ⚠️'); log.warn('WARNING: You are skipping libcheck... Is that really wanted?');
console.log('You are skipping libcheck... Is that really wanted?'); log.indent('Continuing in 5 seconds...');
console.log('Continuing in 5 seconds...\n');
await smartdelay.delayFor(5000); await smartdelay.delayFor(5000);
} else if (!this.argvArg?.quiet && !this.argvArg?.json) { } 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(); await this.handleSkipLibCheckWarning();
} }
// Enhanced logging with task info
const startTime = Date.now(); const startTime = Date.now();
if (taskInfo) { if (taskInfo) {
const { taskNumber, totalTasks, sourcePattern, fileCount } = taskInfo; const { taskNumber, totalTasks, sourcePattern, fileCount } = taskInfo;
const relativeDestDir = taskInfo.destDir.replace(process.cwd(), '').replace(/^\//, ''); const relativeDestDir = taskInfo.destDir.replace(process.cwd(), '').replace(/^\//, '');
console.log( log.step('🔨', `[${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`);
`\n🔨 [${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}` log.detail('📁', `Output: ${relativeDestDir}`);
);
console.log(` 📁 Output: ${relativeDestDir}`);
} else { } else {
console.log(`🔨 Compiling ${fileNames.length} files...`); log.step('🔨', `Compiling ${fileNames.length} files...`);
} }
const done = smartpromise.defer<ICompileResult>(); const done = smartpromise.defer<ICompileResult>();
@@ -233,11 +220,9 @@ export class TsCompiler {
const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program); const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program);
const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics); const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics);
// Only continue to emit phase if no pre-emit errors
if (preEmitErrorSummary.totalErrors > 0) { if (preEmitErrorSummary.totalErrors > 0) {
this.displayErrorSummary(preEmitErrorSummary); this.displayErrorSummary(preEmitErrorSummary);
console.error('\n❌ TypeScript pre-emit checks failed. Please fix the issues listed above before proceeding.'); log.error('Pre-emit checks failed');
console.error(' Type errors must be resolved before the compiler can emit output files.\n');
done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary }); done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary });
return done.promise; return done.promise;
} }
@@ -267,9 +252,9 @@ export class TsCompiler {
if (taskInfo) { if (taskInfo) {
const { taskNumber, totalTasks } = taskInfo; const { taskNumber, totalTasks } = taskInfo;
console.log(`[${taskNumber}/${totalTasks}] Task completed in ${duration}ms`); log.success(`[${taskNumber}/${totalTasks}] Completed in ${duration}ms`);
} else { } else {
console.log(`TypeScript emit succeeded! (${duration}ms)`); log.success(`TypeScript emit succeeded (${duration}ms)`);
} }
// Get count of emitted files by type // Get count of emitted files by type
@@ -278,16 +263,13 @@ export class TsCompiler {
const mapFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.map')).length || 0; const mapFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.map')).length || 0;
if (emitResult.emittedFiles && emitResult.emittedFiles.length > 0) { if (emitResult.emittedFiles && emitResult.emittedFiles.length > 0) {
console.log( log.detail('📄', `Generated ${emitResult.emittedFiles.length} files: ${jsFiles} .js, ${dtsFiles} .d.ts, ${mapFiles} source maps`);
` 📄 Generated ${emitResult.emittedFiles.length} files: ${jsFiles} .js, ${dtsFiles} .d.ts, ${mapFiles} source maps`
);
} }
done.resolve({ emittedFiles: emitResult.emittedFiles || [], errorSummary: combinedErrorSummary }); done.resolve({ emittedFiles: emitResult.emittedFiles || [], errorSummary: combinedErrorSummary });
} else { } else {
this.displayErrorSummary(combinedErrorSummary); this.displayErrorSummary(combinedErrorSummary);
console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!'); log.error('TypeScript emit failed');
console.error(' No output files have been generated.\n');
done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary }); done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary });
} }
@@ -323,44 +305,46 @@ export class TsCompiler {
const isJson = this.argvArg?.json === true; const isJson = this.argvArg?.json === true;
if (!isQuiet && !isJson) { 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]) => { 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. // Phase 1: Resolve glob patterns and clean ALL output directories upfront.
// This prevents filesystem metadata corruption on XFS where heavy write interface IResolvedTask {
// activity during subsequent compilations can make freshly-renamed entries pattern: string;
// in previously-unpacked directories invisible or lost. destPath: string;
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = []; destDir: string;
absoluteFiles: string[];
}
const resolvedTasks: IResolvedTask[] = [];
for (const pattern of Object.keys(globPatterns)) { for (const pattern of Object.keys(globPatterns)) {
const destPath = globPatterns[pattern]; const destPath = globPatterns[pattern];
if (!pattern || !destPath) continue; if (!pattern || !destPath) continue;
// Get files matching the glob pattern
const files = await FsHelpers.listFilesWithGlob(this.cwd, pattern); const files = await FsHelpers.listFilesWithGlob(this.cwd, pattern);
// Transform to absolute paths
const absoluteFiles = smartpath.transform.toAbsolute(files, this.cwd) as string[]; 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; 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 (await FsHelpers.directoryExists(destDir)) {
if (!isQuiet && !isJson) { if (!isQuiet && !isJson) {
console.log(`🧹 Clearing output directory: ${destPath}`); log.step('🧹', `Clearing output directory: ${destPath}`);
} }
await FsHelpers.removeDirectory(destDir); 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 = { const options: CompilerOptions = {
...customOptions, ...customOptions,
outDir: destDir, outDir: task.destDir,
listEmittedFiles: true, listEmittedFiles: true,
}; };
@@ -368,32 +352,27 @@ export class TsCompiler {
const taskInfo: ITaskInfo = { const taskInfo: ITaskInfo = {
taskNumber: currentTask, taskNumber: currentTask,
totalTasks, totalTasks,
sourcePattern: pattern, sourcePattern: task.pattern,
destDir: destPath, destDir: task.destPath,
fileCount: absoluteFiles.length, 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); emittedFiles.push(...result.emittedFiles);
errorSummaries.push(result.errorSummary); errorSummaries.push(result.errorSummary);
// Queue unpack for after all compilations (don't modify output dirs between compilations)
if (result.errorSummary.totalErrors === 0) { if (result.errorSummary.totalErrors === 0) {
pendingUnpacks.push({ pattern, destDir }); pendingUnpacks.push({ pattern: task.pattern, destDir: task.destDir });
successfulOutputDirs.push(destDir); successfulOutputDirs.push(task.destDir);
} }
} }
// Perform all unpacks after all compilations are done. // 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) { for (const { pattern, destDir } of pendingUnpacks) {
await performUnpack(pattern, destDir, this.cwd); await performUnpack(pattern, destDir, this.cwd);
} }
// Rewrite import paths in all output directories to handle cross-module references // Rewrite import paths in all output directories
// 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) { if (successfulOutputDirs.length > 0) {
const rewriter = await TsPathRewriter.fromProjectDirectory(this.cwd); const rewriter = await TsPathRewriter.fromProjectDirectory(this.cwd);
let totalRewritten = 0; let totalRewritten = 0;
@@ -401,7 +380,7 @@ export class TsCompiler {
totalRewritten += await rewriter.rewriteDirectory(outputDir); totalRewritten += await rewriter.rewriteDirectory(outputDir);
} }
if (totalRewritten > 0 && !isQuiet && !isJson) { 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' : ''}`);
} }
} }
@@ -450,7 +429,7 @@ export class TsCompiler {
const options = { ...this.createOptions(customOptions), noEmit: true }; const options = { ...this.createOptions(customOptions), noEmit: true };
const fileCount = fileNames.length; 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); const program = this.createProgram(fileNames, options);
@@ -470,12 +449,10 @@ export class TsCompiler {
const success = combinedErrorSummary.totalErrors === 0 && !emitResult.emitSkipped; const success = combinedErrorSummary.totalErrors === 0 && !emitResult.emitSkipped;
if (success) { if (success) {
console.log('\n✅ TypeScript emit check passed! All files can be emitted successfully.'); log.success(`Emit check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`);
console.log(` ${fileCount} file${fileCount !== 1 ? 's' : ''} ${fileCount !== 1 ? 'are' : 'is'} ready to be compiled.\n`);
} else { } else {
this.displayErrorSummary(combinedErrorSummary); this.displayErrorSummary(combinedErrorSummary);
console.error('\n❌ TypeScript emit check failed. Please fix the issues listed above.'); log.error('Emit check failed');
console.error(' The compilation cannot proceed until these errors are resolved.\n');
} }
return success; return success;
@@ -488,7 +465,7 @@ export class TsCompiler {
const options = { ...this.createOptions(customOptions), noEmit: true }; const options = { ...this.createOptions(customOptions), noEmit: true };
const fileCount = fileNames.length; 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 program = this.createProgram(fileNames, options);
const diagnostics = typescript.getPreEmitDiagnostics(program); const diagnostics = typescript.getPreEmitDiagnostics(program);
@@ -497,12 +474,10 @@ export class TsCompiler {
const success = errorSummary.totalErrors === 0; const success = errorSummary.totalErrors === 0;
if (success) { if (success) {
console.log('\n✅ TypeScript type check passed! No type errors found.'); log.success(`Type check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`);
console.log(` All ${fileCount} file${fileCount !== 1 ? 's' : ''} passed type checking successfully.\n`);
} else { } else {
this.displayErrorSummary(errorSummary); this.displayErrorSummary(errorSummary);
console.error('\n❌ TypeScript type check failed. Please fix the type errors listed above.'); log.error('Type check failed');
console.error(' The type checker found issues that need to be resolved.\n');
} }
return success; return success;
@@ -540,42 +515,28 @@ export class TsCompiler {
* Display final compilation summary * Display final compilation summary
*/ */
private displayFinalSummary(errorSummary: IErrorSummary): void { private displayFinalSummary(errorSummary: IErrorSummary): void {
const c = log.c;
if (errorSummary.totalErrors === 0) { 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; return;
} }
const colors = { log.header('📊', 'Compilation Summary');
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(
console.log(`📊 ${colors.brightYellow}Final Compilation Summary${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)); );
if (errorSummary.totalFiles > 0) {
console.log(`${colors.brightRed}❌ Files with errors (${errorSummary.totalFiles}):${colors.reset}`);
Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => { Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => {
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, ''); const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
console.log( log.indent(
` ${colors.red}${colors.reset} ${colors.cyan}${displayPath}${colors.reset} ${colors.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${colors.reset}` `${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) { if (errorSummary.generalErrors.length > 0) {
console.log(`${colors.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${colors.reset}`); console.log(`${c.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${c.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');
} }
} }

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