Compare commits

..

4 Commits

4 changed files with 64 additions and 61 deletions

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## 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) ## 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 add diagnostic interception of fs operations to detect and report unexpected file system changes in previously compiled output directories during compilation

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsbuild", "name": "@git.zone/tsbuild",
"version": "4.2.0", "version": "4.2.2",
"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.0', version: '4.2.2',
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,7 @@
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 fs from 'fs';
import { execSync } from 'child_process';
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';
@@ -380,45 +381,33 @@ export class TsCompiler {
fileCount: absoluteFiles.length, fileCount: absoluteFiles.length,
}; };
// Diagnostic: intercept fs operations during compilation to detect // Diagnostic: intercept TypeScript sys operations during compilation to detect
// any unexpected deletions in previously compiled output directories // any unexpected writes/deletes in previously compiled output directories
const watchedDirs = successfulOutputDirs.filter(d => d !== destDir); const watchedDirs = successfulOutputDirs.filter(d => d !== destDir);
const origUnlink = fs.unlinkSync;
const origRm = fs.rmSync;
const origRmdir = fs.rmdirSync;
const origRename = fs.renameSync;
const origWriteFile = fs.writeFileSync;
let interceptedOps: string[] = []; let interceptedOps: string[] = [];
const origSysWriteFile = typescript.sys.writeFile;
const origSysDeleteFile = typescript.sys.deleteFile;
const origSysCreateDir = typescript.sys.createDirectory;
if (watchedDirs.length > 0 && !isQuiet && !isJson) { if (watchedDirs.length > 0 && !isQuiet && !isJson) {
(fs as any).unlinkSync = (p: string, ...args: any[]) => { typescript.sys.writeFile = (p: string, data: string, writeBom?: boolean) => {
if (watchedDirs.some(d => String(p).startsWith(d + '/'))) { if (watchedDirs.some(d => p.startsWith(d + '/'))) {
interceptedOps.push(`unlink: ${p}`); interceptedOps.push(`sys.writeFile: ${p}`);
} }
return origUnlink.call(fs, p, ...args); return origSysWriteFile.call(typescript.sys, p, data, writeBom);
}; };
(fs as any).rmSync = (p: string, ...args: any[]) => { if (origSysDeleteFile) {
if (watchedDirs.some(d => String(p).startsWith(d + '/'))) { typescript.sys.deleteFile = (p: string) => {
interceptedOps.push(`rm: ${p}`); if (watchedDirs.some(d => p.startsWith(d + '/'))) {
interceptedOps.push(`sys.deleteFile: ${p}`);
}
return origSysDeleteFile!.call(typescript.sys, p);
};
}
typescript.sys.createDirectory = (p: string) => {
if (watchedDirs.some(d => p.startsWith(d + '/'))) {
interceptedOps.push(`sys.createDirectory: ${p}`);
} }
return origRm.call(fs, p, ...args); return origSysCreateDir.call(typescript.sys, p);
};
(fs as any).rmdirSync = (p: string, ...args: any[]) => {
if (watchedDirs.some(d => String(p).startsWith(d + '/'))) {
interceptedOps.push(`rmdir: ${p}`);
}
return origRmdir.call(fs, p, ...args);
};
(fs as any).renameSync = (src: string, dest: string, ...args: any[]) => {
if (watchedDirs.some(d => String(src).startsWith(d + '/') || String(dest).startsWith(d + '/'))) {
interceptedOps.push(`rename: ${src}${dest}`);
}
return origRename.call(fs, src, dest, ...args);
};
(fs as any).writeFileSync = (p: string, ...args: any[]) => {
if (watchedDirs.some(d => String(p).startsWith(d + '/'))) {
interceptedOps.push(`write: ${p}`);
}
return origWriteFile.call(fs, p, ...args);
}; };
} }
@@ -426,21 +415,21 @@ export class TsCompiler {
emittedFiles.push(...result.emittedFiles); emittedFiles.push(...result.emittedFiles);
errorSummaries.push(result.errorSummary); errorSummaries.push(result.errorSummary);
// Restore original fs methods and report any intercepted operations // Restore original sys methods and report any intercepted operations
if (watchedDirs.length > 0 && !isQuiet && !isJson) { if (watchedDirs.length > 0 && !isQuiet && !isJson) {
(fs as any).unlinkSync = origUnlink; typescript.sys.writeFile = origSysWriteFile;
(fs as any).rmSync = origRm; typescript.sys.deleteFile = origSysDeleteFile;
(fs as any).rmdirSync = origRmdir; typescript.sys.createDirectory = origSysCreateDir;
(fs as any).renameSync = origRename;
(fs as any).writeFileSync = origWriteFile;
if (interceptedOps.length > 0) { if (interceptedOps.length > 0) {
console.log(` ⚠️ [diag] ${interceptedOps.length} ops on previous output dirs during compilation:`); console.log(` ⚠️ [diag] ${interceptedOps.length} TypeScript sys ops on previous output dirs:`);
for (const op of interceptedOps.slice(0, 30)) { for (const op of interceptedOps.slice(0, 30)) {
console.log(` ${op.replace(this.cwd + '/', '')}`); console.log(` ${op.replace(this.cwd + '/', '')}`);
} }
if (interceptedOps.length > 30) { if (interceptedOps.length > 30) {
console.log(` ... and ${interceptedOps.length - 30} more`); console.log(` ... and ${interceptedOps.length - 30} more`);
} }
} else {
console.log(` [diag] No TypeScript sys ops on previous output dirs during this compilation`);
} }
} }
diagSnap('post-compile'); diagSnap('post-compile');
@@ -465,29 +454,28 @@ export class TsCompiler {
successfulOutputDirs.push(destDir); successfulOutputDirs.push(destDir);
} }
// Fsync all output directories to force XFS metadata commit // Force global filesystem sync to flush XFS delayed logging (CIL)
// before the next compilation step. Without this, XFS delayed logging // before the next compilation step. Per-directory fsync was insufficient;
// can cause directory entries from previous compilations to become // XFS defers parent B+tree metadata commits which can make sibling
// invisible or corrupted during subsequent TypeScript emit operations. // directory entries invisible to readdirSync after heavy write activity.
for (const dir of successfulOutputDirs) { execSync('sync');
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 // Diagnostic: compare readdirSync vs external ls to detect Node.js caching
if (!isQuiet && !isJson) { if (!isQuiet && !isJson) {
for (const prevDir of successfulOutputDirs) { for (const prevDir of successfulOutputDirs) {
try { try {
const entries = fs.readdirSync(prevDir); const nodeEntries = fs.readdirSync(prevDir);
const dirs = entries.filter(e => { const nodeDirs = nodeEntries.filter(e => { try { return fs.statSync(prevDir + '/' + e).isDirectory(); } catch { return false; } });
try { return fs.statSync(prevDir + '/' + e).isDirectory(); } catch { return false; } const lsOutput = execSync(`ls -1 "${prevDir}"`, { encoding: 'utf8' }).trim();
}); const lsEntries = lsOutput ? lsOutput.split('\n') : [];
console.log(` 📋 [diag] ${prevDir.replace(this.cwd + '/', '')}: ${entries.length} entries, ${dirs.length} dirs`); const shortDir = prevDir.replace(this.cwd + '/', '');
if (nodeEntries.length !== lsEntries.length) {
console.log(` ⚠️ [diag] ${shortDir}: readdirSync=${nodeEntries.length}, ls=${lsEntries.length} (MISMATCH!)`);
console.log(` readdirSync: [${nodeEntries.sort().join(', ')}]`);
console.log(` ls: [${lsEntries.sort().join(', ')}]`);
} else {
console.log(` 📋 [diag] ${shortDir}: ${nodeEntries.length} entries, ${nodeDirs.length} dirs ✓`);
}
} catch { } catch {
console.log(` 📋 [diag] ${prevDir.replace(this.cwd + '/', '')}: MISSING!`); console.log(` 📋 [diag] ${prevDir.replace(this.cwd + '/', '')}: MISSING!`);
} }