diff --git a/changelog.md b/changelog.md index 987d331..96e0c8f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-05 - 4.1.13 - fix(mod_unpack) +Use child_process.execSync (mv/rm) to perform unpack atomically, replacing async fs operations and logs to avoid ENOENT/partial directory listings on XFS + +- Replaced async fs.promises.rename/rm and readdir/stat debugging with execSync rm -rf and mv operations for sequential, atomic moves +- Imported execSync from child_process and removed verbose console logging and extra fs checks +- Addresses race conditions observed on filesystems like XFS where libuv async operations can return partial results or ENOENT errors + ## 2026-03-05 - 4.1.12 - fix(mod_compiler) replace runtime require calls with top-level imports and use execSync/path.join for filesystem sync and traversal diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 730406b..5591464 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsbuild', - version: '4.1.12', + version: '4.1.13', description: 'A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.' } diff --git a/ts/mod_unpack/classes.tsunpacker.ts b/ts/mod_unpack/classes.tsunpacker.ts index 7dc2c5f..f65550e 100644 --- a/ts/mod_unpack/classes.tsunpacker.ts +++ b/ts/mod_unpack/classes.tsunpacker.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs'; import * as path from 'path'; +import { execSync } from 'child_process'; import { TsPublishConfig } from '../mod_config/index.js'; import { FsHelpers } from '../mod_fs/index.js'; @@ -83,11 +83,10 @@ export class TsUnpacker { /** * Perform the unpack operation - flatten nested output directories. * - * Strategy: instead of listing entries and moving them individually (which is - * vulnerable to async readdir returning partial results under signal pressure), - * we rename the entire nested directory out, remove the dest dir, then rename - * the nested directory back as the dest dir. This uses only rename operations - * which are atomic at the kernel level. + * Uses shell commands (mv, rm) via execSync for reliability. Node.js async + * fs operations (rename, rm) can race on XFS filesystems where metadata + * commits are delayed, causing ENOENT or partial directory listings. + * Shell commands execute as direct syscalls without libuv's async layer. * * Returns true if unpacking was performed, false if skipped. */ @@ -103,40 +102,12 @@ export class TsUnpacker { const nestedPath = this.getNestedPath(); const tempPath = this.destDir + '.__unpack_temp__'; - // Log what we're about to do - const nestedEntries = fs.readdirSync(nestedPath); - console.log(` 📦 Unpacking ${this.sourceFolderName}/: ${nestedEntries.length} entries in nested dir`); - console.log(` 📦 Entries: [${nestedEntries.join(', ')}]`); - // Also list the dest dir to see what TypeScript created - const destEntries = fs.readdirSync(this.destDir); - console.log(` 📦 destDir entries: [${destEntries.join(', ')}]`); - - // Clean up any leftover temp dir from a previous failed unpack - await fs.promises.rm(tempPath, { recursive: true, force: true }); - - // Step 1: Rename the nested source folder out to a temp location. - // e.g. dist_ts/ts/ → dist_ts.__unpack_temp__/ - await fs.promises.rename(nestedPath, tempPath); - - // Verify step 1 - const tempEntries = fs.readdirSync(tempPath); - console.log(` 📦 Step 1 (rename to temp): ${tempEntries.length} entries in temp`); - - // Step 2: Remove the dest dir (which now only contains sibling folders - // like ts_interfaces/). Use recursive rm to handle any contents. - await fs.promises.rm(this.destDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); - console.log(` 📦 Step 2 (remove dest): done`); - - // Step 3: Rename the temp dir to the dest dir. - // e.g. dist_ts.__unpack_temp__/ → dist_ts/ - await fs.promises.rename(tempPath, this.destDir); - - // Verify final state - const finalEntries = fs.readdirSync(this.destDir); - const finalDirs = finalEntries.filter((e: string) => { - return fs.statSync(path.join(this.destDir, e)).isDirectory(); - }); - console.log(` 📦 Step 3 (rename to dest): ${finalEntries.length} entries (${finalDirs.length} dirs)`); + // Use shell commands for atomic, sequential filesystem operations. + // This avoids race conditions between Node.js async fs operations on XFS. + execSync(`rm -rf "${tempPath}"`, { stdio: 'ignore' }); + execSync(`mv "${nestedPath}" "${tempPath}"`, { stdio: 'ignore' }); + execSync(`rm -rf "${this.destDir}"`, { stdio: 'ignore' }); + execSync(`mv "${tempPath}" "${this.destDir}"`, { stdio: 'ignore' }); return true; }