Compare commits

...

41 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
fb5421a8c4 v4.2.3 2026-03-05 15:55:42 +00:00
79d48b0875 fix(compiler): defer unpacking until after all compilations and remove diagnostic filesystem syncs to avoid XFS metadata visibility issues 2026-03-05 15:55:42 +00:00
8977ff4525 v4.2.2 2026-03-05 15:35:57 +00:00
9dc74fd392 fix(compiler): force global filesystem sync to flush XFS delayed logging and add diagnostics comparing Node's readdirSync with system ls to detect directory entry inconsistencies 2026-03-05 15:35:57 +00:00
85a33021e4 v4.2.1 2026-03-05 15:28:18 +00:00
e8e64a4ef3 fix(compiler): use TypeScript sys hooks instead of fs monkeypatching to detect writes/deletes in previous output directories 2026-03-05 15:28:18 +00:00
a5dd5252db v4.2.0 2026-03-05 15:22:05 +00:00
dc9a9640df feat(mod_compiler): add diagnostic interception of fs operations to detect and report unexpected file system changes in previously compiled output directories during compilation 2026-03-05 15:22:04 +00:00
431c82a186 v4.1.26 2026-03-05 15:17:58 +00:00
05184179a4 fix(compiler): fsync output directories after unpack to avoid XFS delayed logging causing corrupt or invisible directory entries during subsequent TypeScript emits 2026-03-05 15:17:58 +00:00
f15ab3a6f9 v4.1.25 2026-03-05 15:13:36 +00:00
67d29a8e77 fix(mod_unpack): flush directory metadata on XFS before reading and use readdirSync-based iteration to avoid missing entries when unpacking 2026-03-05 15:13:36 +00:00
0e1db4bb85 v4.1.24 2026-03-05 15:09:53 +00:00
7a1c2d82b9 fix(mod_unpack): iterate directories with opendirSync/readSync to avoid missing entries on XFS and ensure directory handles are closed 2026-03-05 15:09:53 +00:00
9f5e4ad76e v4.1.23 2026-03-05 15:01:36 +00:00
4feb074c03 fix(mod_unpack): handle partial readdirSync results when moving nested directory entries and add diagnostic log 2026-03-05 15:01:36 +00:00
95e4f1f036 v4.1.22 2026-03-05 14:54:47 +00:00
eaa66dff1d fix(mod_compiler): improve logging of successful output directories to include a sorted list of entries and use a shortened relative path 2026-03-05 14:54:47 +00:00
8bc4f173e5 v4.1.21 2026-03-05 14:51:14 +00:00
fba2cba8e8 fix(compiler): log emitted files written outside expected destination directory for diagnostics 2026-03-05 14:51:14 +00:00
1033996cb5 v4.1.20 2026-03-05 14:48:05 +00:00
7e8b5c4467 fix(mod_compiler): add diagnostic snapshots for output directories around clear and compile steps 2026-03-05 14:48:05 +00:00
a738716d98 v4.1.19 2026-03-05 14:44:14 +00:00
d9c79ae4eb fix(mod_fs): use synchronous rm to avoid XFS metadata corruption when removing directories 2026-03-05 14:44:14 +00:00
a3255fd1fb v4.1.18 2026-03-05 14:40:05 +00:00
d6fb6e527e fix(mod_compiler): add diagnostic logging of output directory states after compilation and after import-path rewriting to aid debugging 2026-03-05 14:40:05 +00:00
96bafec720 v4.1.17 2026-03-05 14:35:05 +00:00
86f47ff743 fix(tsunpacker): use synchronous fs operations in tsunpacker to avoid readdir race conditions 2026-03-05 14:35:05 +00:00
38c134f084 v4.1.16 2026-03-05 14:30:33 +00:00
25372bf97d fix(mod_unpack): handle partial readdir results from signal-interrupted getdents64 when unpacking to ensure sibling removal and nested moves complete 2026-03-05 14:30:33 +00:00
f521530eed v4.1.15 2026-03-05 14:26:29 +00:00
dd81d65958 fix(mod_unpack): flatten nested output directory without temporary rename steps to avoid race conditions 2026-03-05 14:26:29 +00:00
10 changed files with 370 additions and 192 deletions

View File

@@ -1,5 +1,150 @@
# 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
- Queue pending unpack operations during compilation and run them after all compile tasks complete to avoid modifying output directories while other compilations are writing.
- Remove TypeScript sys interception, execSync('sync') calls, and per-unpack fs.fsyncSync usage that attempted to work around XFS delayed metadata commits; rely on performing all unpacks after compilation instead.
- Clean up noisy diagnostic code (external 'ls' comparisons, readdir snapshots) and simplify logging of unpack results.
- Remove unused imports (fs and child_process.execSync) from the compiler module.
## 2026-03-05 - 4.2.2 - fix(compiler)
force global filesystem sync to flush XFS delayed logging and add diagnostics comparing Node's readdirSync with system ls to detect directory entry inconsistencies
- Replace per-directory fs.fsyncSync loop with execSync('sync') to ensure parent B+tree metadata is flushed on XFS
- Import execSync from child_process
- Add diagnostic comparison: run ls -1 and compare its entries to fs.readdirSync; log mismatches and full entry lists for debugging Node.js caching/readdir inconsistencies
## 2026-03-05 - 4.2.1 - fix(compiler)
use TypeScript sys hooks instead of fs monkeypatching to detect writes/deletes in previous output directories
- Replace direct fs.* monkeypatching with interception of typescript.sys.writeFile, typescript.sys.deleteFile and typescript.sys.createDirectory
- Add guards for optional sys.deleteFile before overriding it and preserve original sys methods to restore after compilation
- Update diagnostic messages to reference TypeScript sys ops and add an informational message when no ops are observed
- Reduce surface area of changes by avoiding global fs changes and focusing on TypeScript's sys API for safer interception
## 2026-03-05 - 4.2.0 - feat(mod_compiler)
add diagnostic interception of fs operations to detect and report unexpected file system changes in previously compiled output directories during compilation
- Wraps fs.unlinkSync, fs.rmSync, fs.rmdirSync, fs.renameSync and fs.writeFileSync to record operations targeting other successful output directories during a compile.
- Enabled only when there are previously compiled output dirs and when not running in quiet or JSON mode; original fs methods are restored after compilation.
- Logs up to 30 intercepted operations and prints a summary count if any ops were observed; intercepted calls still perform the original fs action (diagnostic-only).
- No functional change to compilation behavior beyond additional diagnostic reporting.
## 2026-03-05 - 4.1.26 - fix(compiler)
fsync output directories after unpack to avoid XFS delayed logging causing corrupt or invisible directory entries during subsequent TypeScript emits
- Force fsync on each successful output directory (open, fsync, close) before the next compilation step.
- Prevents XFS delayed logging from making directory entries invisible or corrupted during TypeScript emit operations.
- Operation swallows errors if a directory doesn't exist yet; non-breaking fix with small additional IO.
## 2026-03-05 - 4.1.25 - fix(mod_unpack)
flush directory metadata on XFS before reading and use readdirSync-based iteration to avoid missing entries when unpacking
- Call fs.fsyncSync on destination and nested directory file descriptors to force XFS to commit delayed directory metadata (addresses XFS CIL delayed logging causing incomplete readdir/opendir results).
- Replace opendirSync/readSync loops with readdirSync-based iteration for simpler, deterministic directory listing.
- Remove unused moved counter and update diagnostic log to report nestedEntries.length for moved entry count.
## 2026-03-05 - 4.1.24 - fix(mod_unpack)
iterate directories with opendirSync/readSync to avoid missing entries on XFS and ensure directory handles are closed
- Replaced readdirSync loops with opendirSync + readSync for destination and nested directories to provide a single stable directory handle during iteration
- Added explicit closeSync() calls to close directory handles and avoid resource leaks
- Avoids partial results/missed entries that can occur when repeatedly calling readdirSync (observed on XFS with delayed metadata)
- Preserves existing renameSync move logic and increments moved counter while cleaning up the now-empty nested directory
## 2026-03-05 - 4.1.23 - fix(mod_unpack)
handle partial readdirSync results when moving nested directory entries and add diagnostic log
- Loop over readdirSync results until the nested directory is empty to avoid missing entries from partial reads
- Count moved entries and print a diagnostic message with the final destination entry count
- Keep removal of the now-empty nested directory (fs.rmdirSync) after moving contents
## 2026-03-05 - 4.1.22 - fix(mod_compiler)
improve logging of successful output directories to include a sorted list of entries and use a shortened relative path
- Adds shortDir variable to display relative path instead of repeating inline replace(this.cwd + '/')
- Appends a sorted, comma-separated list of directory entries to the log output for easier inspection
- Change located in ts/mod_compiler/classes.tscompiler.ts
## 2026-03-05 - 4.1.21 - fix(compiler)
log emitted files written outside expected destination directory for diagnostics
- Adds diagnostic logging for emitted files that are not under the configured destDir, listing up to 20 example paths and reporting the remaining count.
- Logging is conditional: only when not in quiet mode and not emitting JSON.
- Diagnostic runs after compilation (post-compile) and before unpacking of outputs; paths are trimmed using the process cwd for readability.
## 2026-03-05 - 4.1.20 - fix(mod_compiler)
add diagnostic snapshots for output directories around clear and compile steps
- Introduce diagSnap helper to log entry and directory counts for successful output directories when not in quiet or JSON mode
- Call diagSnap before clearing the destination directory, after clearing, and after compilation to aid debugging of missing or unexpected outputs
- Behavior for emitted files and unpacking is unchanged; this is observational/logging-only instrumentation
## 2026-03-05 - 4.1.19 - fix(mod_fs)
use synchronous rm to avoid XFS metadata corruption when removing directories
- Replaced async fs.promises.rm with synchronous fs.rmSync in removeDirectory to avoid observed XFS metadata corruption affecting sibling entries under libuv thread-pool and signal pressure
- Retains previous options: recursive, force, maxRetries, retryDelay
- Adds inline comment documenting the rationale for using a synchronous removal
## 2026-03-05 - 4.1.18 - fix(mod_compiler)
add diagnostic logging of output directory states after compilation and after import-path rewriting to aid debugging
- Imported fs to allow reading output directories for diagnostics
- Logs entries and directory counts for each successful output directory both pre- and post-import-path-rewrite
- Diagnostics are gated by !isQuiet && !isJson and are read-only (no behavior change)
- Tags used: 'diag' (post-compilation) and 'diag-post-rewrite' (after rewriting) to help identify missing or unexpected output folders
## 2026-03-05 - 4.1.17 - fix(tsunpacker)
use synchronous fs operations in tsunpacker to avoid readdir race conditions
- Replaced async fs.promises.readdir/rename/rm/rmdir loops with fs.readdirSync/renameSync/rmSync/rmdirSync
- Removed readdir retry loops that attempted to handle partial/stale readdir results
- Updated comment to document rationale: avoid race conditions under signal pressure and XFS metadata lag
- Note: function remains async but now performs blocking sync filesystem calls which may block the event loop during unpack
## 2026-03-05 - 4.1.16 - fix(mod_unpack)
handle partial readdir results from signal-interrupted getdents64 when unpacking to ensure sibling removal and nested moves complete
- Loop readdir calls for destination directory until only the source folder remains to avoid partial-listing leftovers
- Loop readdir calls for nested directory and repeatedly rename entries until the nested directory is empty
- Prevents leftover files and incomplete moves when readdir returns partial results under signals
## 2026-03-05 - 4.1.15 - fix(mod_unpack)
flatten nested output directory without temporary rename steps to avoid race conditions
- Replace rename-rm-rename strategy with: remove sibling entries in destination, move nested source entries up into the destination, then remove the now-empty nested folder.
- Avoid creating temporary sibling directories and avoid removing the destination directory to reduce filesystem race conditions and metadata lag issues (XFS/NFS/etc.).
- Remove removed removeEmptyDirectory helper and stop using FsHelpers.move/removeDirectory in unpack; import and use fs.promises methods (readdir, rm, rename, rmdir) directly.
## 2026-03-05 - 4.1.14 - fix(fs)
replace execSync and fsync workarounds with atomic async FsHelpers operations to avoid XFS races and shell dependencies

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsbuild",
"version": "4.1.14",
"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.1.14',
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}`;
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,38 +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('');
}
// 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,
};
@@ -356,26 +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);
// Perform unpack if compilation succeeded
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.
for (const { pattern, destDir } of pendingUnpacks) {
await performUnpack(pattern, destDir, this.cwd);
successfulOutputDirs.push(destDir);
}
}
// 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;
@@ -383,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' : ''}`);
}
}
@@ -432,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);
@@ -452,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;
@@ -470,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);
@@ -479,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;
@@ -522,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',
};
log.header('📊', 'Compilation Summary');
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}`);
console.log(
`${c.brightRed} ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${c.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}`
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(`${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');
console.log(`${c.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${c.reset}`);
}
}
}

View File

@@ -122,10 +122,10 @@ export class FsHelpers {
}
/**
* Remove a directory recursively
* Remove a directory recursively.
*/
public static async removeDirectory(dirPath: string): Promise<void> {
await fs.promises.rm(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
}
/**
@@ -134,34 +134,4 @@ export class FsHelpers {
public static async move(src: string, dest: string): Promise<void> {
await fs.promises.rename(src, dest);
}
/**
* Remove an empty directory
*/
public static async removeEmptyDirectory(dirPath: string): Promise<void> {
// Retry rmdir with delays to handle filesystem metadata lag (XFS, NFS, etc.)
// NEVER use recursive rm here — if rmdir fails with ENOTEMPTY, entries may
// still be valid references to renamed files/dirs that haven't fully detached
for (let attempt = 0; attempt < 5; attempt++) {
try {
await fs.promises.rmdir(dirPath);
return;
} catch (err: any) {
if (err.code === 'ENOENT') {
return; // Already gone
}
if (err.code === 'ENOTEMPTY' && attempt < 4) {
// Wait for filesystem metadata to catch up
await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
continue;
}
// Final attempt failed or non-retryable error — leave directory in place
// It will be cleaned up by the next build's "clear output directory" step
if (err.code === 'ENOTEMPTY') {
return;
}
throw err;
}
}
}
}

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

@@ -1,6 +1,8 @@
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.
@@ -82,9 +84,16 @@ export class TsUnpacker {
/**
* Perform the unpack operation - flatten nested output directories.
*
* Renames the nested directory to a temp location, removes the dest dir,
* then renames the temp dir back as dest. Uses only rename operations
* which are atomic at the kernel level.
* When TypeScript compiles files that import from sibling directories,
* it creates a nested structure like dist_ts/ts/ with siblings like
* dist_ts/ts_interfaces/. This method flattens by:
* 1. Removing sibling directories (non-source folders)
* 2. Moving contents of the nested source folder up to the dest dir
* 3. Removing the now-empty nested source folder
*
* Uses synchronous fs operations for reliability.
* Called after all compilations are complete (not between compilations)
* to avoid filesystem metadata issues on XFS.
*
* Returns true if unpacking was performed, false if skipped.
*/
@@ -98,19 +107,28 @@ export class TsUnpacker {
}
const nestedPath = this.getNestedPath();
const tempPath = this.destDir + '.__unpack_temp__';
// Step 1: Clean up any leftover temp dir from a previous failed run
await FsHelpers.removeDirectory(tempPath);
// Step 1: Remove sibling entries (everything in dest except the source folder)
const destEntries = fs.readdirSync(this.destDir);
for (const entry of destEntries) {
if (entry !== this.sourceFolderName) {
fs.rmSync(path.join(this.destDir, entry), { recursive: true, force: true });
}
}
// Step 2: Rename nested → temp
await FsHelpers.move(nestedPath, tempPath);
// Step 2: Move all contents from nested dir up to dest dir
const nestedEntries = fs.readdirSync(nestedPath);
for (const entry of nestedEntries) {
fs.renameSync(
path.join(nestedPath, entry),
path.join(this.destDir, entry),
);
}
// Step 3: Remove dest dir (now contains only sibling folders)
await FsHelpers.removeDirectory(this.destDir);
// Step 3: Remove the now-empty nested directory
fs.rmdirSync(nestedPath);
// Step 4: Rename temp → dest
await FsHelpers.move(tempPath, this.destDir);
log.detail('📦', `Unpacked ${this.sourceFolderName}: ${nestedEntries.length} entries`);
return true;
}