Compare commits

...

8 Commits

5 changed files with 51 additions and 99 deletions

View File

@@ -1,5 +1,36 @@
# Changelog # Changelog
## 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) ## 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 fsync output directories after unpack to avoid XFS delayed logging causing corrupt or invisible directory entries during subsequent TypeScript emits

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsbuild", "name": "@git.zone/tsbuild",
"version": "4.1.26", "version": "4.2.3",
"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.26', version: '4.2.3',
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,6 +1,5 @@
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';
@@ -325,6 +324,12 @@ export class TsCompiler {
console.log(''); console.log('');
} }
// Collect unpack tasks to perform AFTER all compilations complete.
// This prevents filesystem metadata corruption on XFS where heavy write
// activity during subsequent compilations can make freshly-renamed entries
// in previously-unpacked directories invisible or lost.
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = [];
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;
@@ -338,31 +343,13 @@ export class TsCompiler {
// Get destination directory as absolute path // 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;
// Diagnostic helper
const diagSnap = (label: string) => {
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; } });
const shortDir = prevDir.replace(this.cwd + '/', '');
console.log(` 📋 [${label}] ${shortDir}: ${entries.length} entries, ${dirs.length} dirs [${entries.sort().join(', ')}]`);
} catch {
console.log(` 📋 [${label}] ${prevDir.replace(this.cwd + '/', '')}: MISSING!`);
}
}
}
};
// Clear the destination directory before compilation if it exists // Clear the destination directory before compilation if it exists
diagSnap('pre-clear');
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}`);
} }
await FsHelpers.removeDirectory(destDir); await FsHelpers.removeDirectory(destDir);
} }
diagSnap('post-clear');
// Update compiler options with the output directory // Update compiler options with the output directory
const options: CompilerOptions = { const options: CompilerOptions = {
@@ -383,57 +370,19 @@ export class TsCompiler {
const result = await this.compileFiles(absoluteFiles, options, taskInfo); const result = await this.compileFiles(absoluteFiles, options, taskInfo);
emittedFiles.push(...result.emittedFiles); emittedFiles.push(...result.emittedFiles);
errorSummaries.push(result.errorSummary); errorSummaries.push(result.errorSummary);
diagSnap('post-compile');
// Diagnostic: log emitted files that went to unexpected directories // Queue unpack for after all compilations (don't modify output dirs between compilations)
if (!isQuiet && !isJson && result.emittedFiles.length > 0) {
const unexpectedFiles = result.emittedFiles.filter(f => !f.startsWith(destDir + '/') && !f.startsWith(destDir + '\\'));
if (unexpectedFiles.length > 0) {
console.log(` ⚠️ [diag] ${unexpectedFiles.length} files emitted OUTSIDE ${destPath}:`);
for (const f of unexpectedFiles.slice(0, 20)) {
console.log(` ${f.replace(this.cwd + '/', '')}`);
}
if (unexpectedFiles.length > 20) {
console.log(` ... and ${unexpectedFiles.length - 20} more`);
}
}
}
// Perform unpack if compilation succeeded
if (result.errorSummary.totalErrors === 0) { if (result.errorSummary.totalErrors === 0) {
await performUnpack(pattern, destDir, this.cwd); pendingUnpacks.push({ pattern, destDir });
successfulOutputDirs.push(destDir); successfulOutputDirs.push(destDir);
} }
}
// Fsync all output directories to force XFS metadata commit // Perform all unpacks after all compilations are done.
// before the next compilation step. Without this, XFS delayed logging // This ensures no output directory is modified while subsequent compilations
// can cause directory entries from previous compilations to become // are performing heavy filesystem writes to sibling directories.
// invisible or corrupted during subsequent TypeScript emit operations. for (const { pattern, destDir } of pendingUnpacks) {
for (const dir of successfulOutputDirs) { await performUnpack(pattern, destDir, this.cwd);
try {
const fd = fs.openSync(dir, 'r');
fs.fsyncSync(fd);
fs.closeSync(fd);
} catch {
// Directory might not exist yet
}
}
// 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
@@ -448,21 +397,6 @@ 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

@@ -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
* *
* Uses synchronous fs operations to avoid race conditions with * Uses synchronous fs operations for reliability.
* async readdir returning partial/stale results under signal pressure * Called after all compilations are complete (not between compilations)
* or XFS metadata lag (observed in process-group environments like gitzone). * to avoid filesystem metadata issues on XFS.
* *
* Returns true if unpacking was performed, false if skipped. * Returns true if unpacking was performed, false if skipped.
*/ */
@@ -107,17 +107,6 @@ export class TsUnpacker {
const nestedPath = this.getNestedPath(); const nestedPath = this.getNestedPath();
// Force XFS to flush pending directory metadata before reading.
// XFS delayed logging (CIL) can defer metadata commits, causing
// readdirSync/opendirSync to return incomplete results immediately
// after TypeScript's emit() creates files via writeFileSync.
const destFd = fs.openSync(this.destDir, 'r');
fs.fsyncSync(destFd);
fs.closeSync(destFd);
const nestedFd = fs.openSync(nestedPath, 'r');
fs.fsyncSync(nestedFd);
fs.closeSync(nestedFd);
// Step 1: Remove sibling entries (everything in dest except the source folder) // Step 1: Remove sibling entries (everything in dest except the source folder)
const destEntries = fs.readdirSync(this.destDir); const destEntries = fs.readdirSync(this.destDir);
for (const entry of destEntries) { for (const entry of destEntries) {
@@ -138,9 +127,7 @@ export class TsUnpacker {
// Step 3: Remove the now-empty nested directory // Step 3: Remove the now-empty nested directory
fs.rmdirSync(nestedPath); fs.rmdirSync(nestedPath);
// Diagnostic: verify final state console.log(` 📦 Unpacked ${this.sourceFolderName}: ${nestedEntries.length} entries`);
const finalEntries = fs.readdirSync(this.destDir);
console.log(` 📦 Unpacked ${this.sourceFolderName}: moved ${nestedEntries.length} entries, final: ${finalEntries.length} entries`);
return true; return true;
} }