fix(compiler): move output directory cleaning to separate phase before compilation

Restructured compileGlob() into three distinct phases:
1. Resolve glob patterns and clean ALL output directories upfront
2. Compile all TypeScript tasks (no filesystem cleanup during this phase)
3. Unpack all outputs after all compilations complete

This prevents XFS metadata corruption from rm operations overlapping
with TypeScript compilation writes to sibling directories.
This commit is contained in:
2026-03-05 16:45:07 +00:00
parent 9477875c1d
commit a6a006aaec

View File

@@ -330,26 +330,25 @@ export class TsCompiler {
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 }> = [];
// Phase 1: Resolve glob patterns and clean ALL output directories upfront.
// This ensures no rm/rmSync activity overlaps with TypeScript compilation,
// preventing XFS metadata corruption from concurrent metadata operations.
interface IResolvedTask {
pattern: string;
destPath: string;
destDir: string;
absoluteFiles: string[];
}
const resolvedTasks: IResolvedTask[] = [];
for (const pattern of Object.keys(globPatterns)) {
const destPath = globPatterns[pattern];
if (!pattern || !destPath) continue;
// Get files matching the glob pattern
const files = await FsHelpers.listFilesWithGlob(this.cwd, pattern);
// Transform to absolute paths
const absoluteFiles = smartpath.transform.toAbsolute(files, this.cwd) as string[];
// Get destination directory as absolute path
const destDir = smartpath.transform.toAbsolute(destPath, this.cwd) as string;
// Clear the destination directory before compilation if it exists
if (await FsHelpers.directoryExists(destDir)) {
if (!isQuiet && !isJson) {
console.log(`🧹 Clearing output directory: ${destPath}`);
@@ -357,10 +356,16 @@ export class TsCompiler {
await FsHelpers.removeDirectory(destDir);
}
// Update compiler options with the output directory
resolvedTasks.push({ pattern, destPath, destDir, absoluteFiles });
}
// Phase 2: Compile all tasks. No filesystem cleanup happens during this phase.
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = [];
for (const task of resolvedTasks) {
const options: CompilerOptions = {
...customOptions,
outDir: destDir,
outDir: task.destDir,
listEmittedFiles: true,
};
@@ -368,23 +373,22 @@ export class TsCompiler {
const taskInfo: ITaskInfo = {
taskNumber: currentTask,
totalTasks,
sourcePattern: pattern,
destDir: destPath,
fileCount: absoluteFiles.length,
sourcePattern: task.pattern,
destDir: task.destPath,
fileCount: task.absoluteFiles.length,
};
const result = await this.compileFiles(absoluteFiles, options, taskInfo);
const result = await this.compileFiles(task.absoluteFiles, options, taskInfo);
emittedFiles.push(...result.emittedFiles);
errorSummaries.push(result.errorSummary);
// Queue unpack for after all compilations (don't modify output dirs between compilations)
if (result.errorSummary.totalErrors === 0) {
pendingUnpacks.push({ pattern, destDir });
successfulOutputDirs.push(destDir);
pendingUnpacks.push({ pattern: task.pattern, destDir: task.destDir });
successfulOutputDirs.push(task.destDir);
}
}
// Perform all unpacks after all compilations are done.
// Phase 3: Perform all unpacks after all compilations are done.
// This ensures no output directory is modified while subsequent compilations
// are performing heavy filesystem writes to sibling directories.
for (const { pattern, destDir } of pendingUnpacks) {