Compare commits

...

7 Commits

Author SHA1 Message Date
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
5 changed files with 52 additions and 26 deletions

View File

@@ -1,5 +1,24 @@
# Changelog # Changelog
## 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) ## 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 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", "name": "@git.zone/tsbuild",
"version": "4.2.3", "version": "4.2.6",
"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.3', version: '4.2.6',
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

@@ -244,6 +244,12 @@ export class TsCompiler {
// If no pre-emit errors, proceed with emit // If no pre-emit errors, proceed with emit
const emitResult = program.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); const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
// Combine error summaries // Combine error summaries
@@ -324,26 +330,25 @@ export class TsCompiler {
console.log(''); 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 // This ensures no rm/rmSync activity overlaps with TypeScript compilation,
// activity during subsequent compilations can make freshly-renamed entries // preventing XFS metadata corruption from concurrent metadata operations.
// in previously-unpacked directories invisible or lost. interface IResolvedTask {
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = []; pattern: string;
destPath: 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}`); console.log(`🧹 Clearing output directory: ${destPath}`);
@@ -351,10 +356,16 @@ export class TsCompiler {
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. No filesystem cleanup happens during this phase.
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,
}; };
@@ -362,23 +373,22 @@ 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 // This ensures no output directory is modified while subsequent compilations
// are performing heavy filesystem writes to sibling directories. // are performing heavy filesystem writes to sibling directories.
for (const { pattern, destDir } of pendingUnpacks) { for (const { pattern, destDir } of pendingUnpacks) {

View File

@@ -123,9 +123,6 @@ export class FsHelpers {
/** /**
* Remove a directory recursively. * 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> { public static async removeDirectory(dirPath: string): Promise<void> {
fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });