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 { return this.config.shouldUnpack; } /** * Check if nested structure exists in the destination directory */ public async detectNesting(): Promise { 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 { 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 { const unpacker = TsUnpacker.fromGlobPattern(sourcePattern, destDir, cwd); if (!unpacker) { return false; } return unpacker.unpack(); }