Compare commits

...

6 Commits

6 changed files with 75 additions and 26 deletions

View File

@@ -1,5 +1,28 @@
# 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)
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",
"version": "4.1.16",
"version": "4.1.19",
"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.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.'
}

View File

@@ -1,5 +1,6 @@
import type { CompilerOptions, Diagnostic, Program } from 'typescript';
import typescript from 'typescript';
import * as fs from 'fs';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartpath from '@push.rocks/smartpath';
@@ -371,6 +372,21 @@ export class TsCompiler {
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
@@ -385,6 +401,21 @@ export class TsCompiler {
if (totalRewritten > 0 && !isQuiet && !isJson) {
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

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