Files
tsbuild/ts/mod_unpack/classes.tsunpacker.ts

164 lines
4.9 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import { TsPublishConfig } from '../mod_config/index.js';
import { FsHelpers } from '../mod_fs/index.js';
/**
* TsUnpacker handles flattening of nested TypeScript output directories.
*
* When TypeScript compiles files that import from sibling directories,
* it creates a nested structure like:
* dist_ts_core/ts_core/index.js
* dist_ts_core/ts_shared/helper.js
*
* This class flattens it to:
* dist_ts_core/index.js
*/
export class TsUnpacker {
private sourceFolderName: string;
private destDir: string;
private cwd: string;
private config: TsPublishConfig;
constructor(sourceFolderName: string, destDir: string, cwd: string = process.cwd()) {
this.sourceFolderName = sourceFolderName;
this.destDir = destDir;
this.cwd = cwd;
this.config = new TsPublishConfig(path.join(cwd, sourceFolderName));
}
/**
* Create an unpacker from a glob pattern
* './ts_core/**\/*.ts' → sourceFolderName = 'ts_core'
*/
public static fromGlobPattern(
sourcePattern: string,
destDir: string,
cwd: string = process.cwd()
): TsUnpacker | null {
const sourceFolderName = FsHelpers.extractSourceFolder(sourcePattern);
if (!sourceFolderName) {
return null;
}
return new TsUnpacker(sourceFolderName, destDir, cwd);
}
/**
* Get the source folder name
*/
public getSourceFolderName(): string {
return this.sourceFolderName;
}
/**
* Get the destination directory
*/
public getDestDir(): string {
return this.destDir;
}
/**
* Check if unpacking should be performed based on tspublish.json config
* Default is true if not specified
*/
public async shouldUnpack(): Promise<boolean> {
return this.config.shouldUnpack;
}
/**
* Check if nested structure exists in the destination directory
*/
public async detectNesting(): Promise<boolean> {
const nestedPath = path.join(this.destDir, this.sourceFolderName);
return FsHelpers.directoryExists(nestedPath);
}
/**
* Get the path to the nested directory
*/
public getNestedPath(): string {
return path.join(this.destDir, this.sourceFolderName);
}
/**
* Perform the unpack operation - flatten nested output directories.
*
* When TypeScript compiles files that import from sibling directories,
* it creates a nested structure like dist_ts/ts/ with siblings like
* dist_ts/ts_interfaces/. This method flattens by:
* 1. Removing sibling directories (non-source folders)
* 2. Moving contents of the nested source folder up to the dest dir
* 3. Removing the now-empty nested source folder
*
* 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.
*/
public async unpack(): Promise<boolean> {
if (!(await this.shouldUnpack())) {
return false;
}
if (!(await this.detectNesting())) {
return false;
}
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)
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 });
}
}
// Step 2: Move all contents from nested dir up to dest dir
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
fs.rmdirSync(nestedPath);
// Diagnostic: verify final state
const finalEntries = fs.readdirSync(this.destDir);
console.log(` 📦 Unpacked ${this.sourceFolderName}: moved ${nestedEntries.length} entries, final: ${finalEntries.length} entries`);
return true;
}
}
/**
* Convenience function to perform unpack operation
* Can be used directly without instantiating the class
*/
export async function performUnpack(
sourcePattern: string,
destDir: string,
cwd: string = process.cwd()
): Promise<boolean> {
const unpacker = TsUnpacker.fromGlobPattern(sourcePattern, destDir, cwd);
if (!unpacker) {
return false;
}
return unpacker.unpack();
}