Compare commits

..

6 Commits

6 changed files with 75 additions and 26 deletions

View File

@@ -1,5 +1,28 @@
# Changelog # Changelog
## 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) ## 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 handle partial readdir results from signal-interrupted getdents64 when unpacking to ensure sibling removal and nested moves complete

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsbuild", "name": "@git.zone/tsbuild",
"version": "4.1.16", "version": "4.1.19",
"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.1.16', version: '4.1.19',
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

@@ -1,5 +1,6 @@
import type { CompilerOptions, Diagnostic, Program } from 'typescript'; import type { CompilerOptions, Diagnostic, Program } from 'typescript';
import typescript from 'typescript'; import typescript from 'typescript';
import * as fs from 'fs';
import * as smartdelay from '@push.rocks/smartdelay'; import * as smartdelay from '@push.rocks/smartdelay';
import * as smartpromise from '@push.rocks/smartpromise'; import * as smartpromise from '@push.rocks/smartpromise';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
@@ -371,6 +372,21 @@ export class TsCompiler {
successfulOutputDirs.push(destDir); successfulOutputDirs.push(destDir);
} }
// Diagnostic: log all output directory states after each compilation
if (!isQuiet && !isJson) {
for (const prevDir of successfulOutputDirs) {
try {
const entries = fs.readdirSync(prevDir);
const dirs = entries.filter(e => {
try { return fs.statSync(prevDir + '/' + e).isDirectory(); } catch { return false; }
});
console.log(` 📋 [diag] ${prevDir.replace(this.cwd + '/', '')}: ${entries.length} entries, ${dirs.length} dirs`);
} catch {
console.log(` 📋 [diag] ${prevDir.replace(this.cwd + '/', '')}: MISSING!`);
}
}
}
} }
// Rewrite import paths in all output directories to handle cross-module references // Rewrite import paths in all output directories to handle cross-module references
@@ -385,6 +401,21 @@ export class TsCompiler {
if (totalRewritten > 0 && !isQuiet && !isJson) { if (totalRewritten > 0 && !isQuiet && !isJson) {
console.log(` 🔄 Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`); console.log(` 🔄 Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`);
} }
// Diagnostic: log output directory states after path rewriting
if (!isQuiet && !isJson) {
for (const dir of successfulOutputDirs) {
try {
const entries = fs.readdirSync(dir);
const dirs = entries.filter(e => {
try { return fs.statSync(dir + '/' + e).isDirectory(); } catch { return false; }
});
console.log(` 📋 [diag-post-rewrite] ${dir.replace(this.cwd + '/', '')}: ${entries.length} entries, ${dirs.length} dirs`);
} catch {
console.log(` 📋 [diag-post-rewrite] ${dir.replace(this.cwd + '/', '')}: MISSING!`);
}
}
}
} }
// Merge all error summaries // Merge all error summaries

View File

@@ -122,10 +122,13 @@ 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> {
await fs.promises.rm(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
} }
/** /**

View File

@@ -90,9 +90,9 @@ export class TsUnpacker {
* 2. Moving contents of the nested source folder up to the dest dir * 2. Moving contents of the nested source folder up to the dest dir
* 3. Removing the now-empty nested source folder * 3. Removing the now-empty nested source folder
* *
* This approach never creates temporary sibling directories and never * Uses synchronous fs operations to avoid race conditions with
* removes the destination directory itself, avoiding filesystem race * async readdir returning partial/stale results under signal pressure
* conditions observed with the previous rename-rm-rename pattern. * or XFS metadata lag (observed in process-group environments like gitzone).
* *
* Returns true if unpacking was performed, false if skipped. * Returns true if unpacking was performed, false if skipped.
*/ */
@@ -108,32 +108,24 @@ export class TsUnpacker {
const nestedPath = this.getNestedPath(); const nestedPath = this.getNestedPath();
// Step 1: Remove sibling entries (everything in dest except the source folder) // Step 1: Remove sibling entries (everything in dest except the source folder)
// Loop handles partial readdir results from signal-interrupted getdents64 const destEntries = fs.readdirSync(this.destDir);
let destEntries = await fs.promises.readdir(this.destDir); for (const entry of destEntries) {
while (destEntries.some(e => e !== this.sourceFolderName)) { if (entry !== this.sourceFolderName) {
for (const entry of destEntries) { fs.rmSync(path.join(this.destDir, entry), { recursive: true, force: true });
if (entry !== this.sourceFolderName) {
await fs.promises.rm(path.join(this.destDir, entry), { recursive: true, force: true });
}
} }
destEntries = await fs.promises.readdir(this.destDir);
} }
// Step 2: Move all contents from nested dir up to dest dir // Step 2: Move all contents from nested dir up to dest dir
// Loop handles partial readdir results from signal-interrupted getdents64 const nestedEntries = fs.readdirSync(nestedPath);
let nestedEntries = await fs.promises.readdir(nestedPath); for (const entry of nestedEntries) {
while (nestedEntries.length > 0) { fs.renameSync(
for (const entry of nestedEntries) { path.join(nestedPath, entry),
await fs.promises.rename( path.join(this.destDir, entry),
path.join(nestedPath, entry), );
path.join(this.destDir, entry),
);
}
nestedEntries = await fs.promises.readdir(nestedPath);
} }
// Step 3: Remove the now-empty nested directory // Step 3: Remove the now-empty nested directory
await fs.promises.rmdir(nestedPath); fs.rmdirSync(nestedPath);
return true; return true;
} }