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

160 lines
5.1 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.
*
* 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.
*
* 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();
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)`);
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();
}