import * as fs from 'fs'; import * as path from 'path'; import { FsHelpers } from '../mod_fs/index.js'; /** * Interface for folder mapping between source and destination */ export interface IFolderMapping { sourceFolder: string; // e.g., 'ts_shared' destFolder: string; // e.g., 'dist_ts_shared' } /** * TsPathRewriter handles rewriting import paths in compiled JavaScript files. * * When TypeScript compiles files that import from sibling directories like: * import { helper } from '../ts_shared/helper.js'; * * This class rewrites them to point to the compiled output directories: * import { helper } from '../dist_ts_shared/helper.js'; * * This is necessary because the unpack feature flattens nested output structures, * changing the relative paths between modules. */ export class TsPathRewriter { private mappings: IFolderMapping[]; constructor(mappings: IFolderMapping[]) { this.mappings = mappings; } /** * Create a TsPathRewriter from glob patterns used in compilation * * @param globPatterns - Map of source patterns to destination directories * e.g., { './ts_core/**\/*.ts': './dist_ts_core', './ts_shared/**\/*.ts': './dist_ts_shared' } * @returns TsPathRewriter instance with extracted folder mappings */ public static fromGlobPatterns(globPatterns: Record): TsPathRewriter { const mappings: IFolderMapping[] = []; for (const [sourcePattern, destDir] of Object.entries(globPatterns)) { const sourceFolder = FsHelpers.extractSourceFolder(sourcePattern); const destFolder = FsHelpers.extractSourceFolder(destDir); if (sourceFolder && destFolder) { mappings.push({ sourceFolder, destFolder }); } } return new TsPathRewriter(mappings); } /** * Get the current folder mappings */ public getMappings(): IFolderMapping[] { return [...this.mappings]; } /** * Rewrite import paths in a single file * * @param filePath - Absolute path to the file to rewrite * @returns true if file was modified, false otherwise */ public async rewriteFile(filePath: string): Promise { // Only process .js and .d.ts files if (!filePath.endsWith('.js') && !filePath.endsWith('.d.ts')) { return false; } try { const content = await fs.promises.readFile(filePath, 'utf8'); const rewritten = this.rewriteContent(content); if (rewritten !== content) { await fs.promises.writeFile(filePath, rewritten, 'utf8'); return true; } return false; } catch (error) { // File doesn't exist or other error return false; } } /** * Rewrite import paths in all .js and .d.ts files in a directory (recursively) * * @param dirPath - Absolute path to the directory * @returns Number of files modified */ public async rewriteDirectory(dirPath: string): Promise { let modifiedCount = 0; const processDir = async (currentDir: string): Promise => { const entries = await FsHelpers.listDirectory(currentDir); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory) { await processDir(fullPath); } else if (entry.isFile && (entry.name.endsWith('.js') || entry.name.endsWith('.d.ts'))) { const wasModified = await this.rewriteFile(fullPath); if (wasModified) { modifiedCount++; } } } }; if (await FsHelpers.directoryExists(dirPath)) { await processDir(dirPath); } return modifiedCount; } /** * Rewrite import paths in content string * * This method only modifies actual import/export/require statements, * not arbitrary strings or comments that might contain similar patterns. * * @param content - File content to process * @returns Rewritten content */ public rewriteContent(content: string): string { if (this.mappings.length === 0) { return content; } let result = content; for (const { sourceFolder, destFolder } of this.mappings) { // Skip if source and dest are the same if (sourceFolder === destFolder) { continue; } // Pattern 1: ES Module imports/exports with 'from' // Matches: from '../ts_shared/...' or from '../../ts_shared/...' // Also handles: export { x } from '../ts_shared/...' const fromPattern = new RegExp( `(from\\s*['"])((?:\\.\\.\\/)+)${this.escapeRegex(sourceFolder)}(\\/[^'"]*['"])`, 'g' ); result = result.replace(fromPattern, `$1$2${destFolder}$3`); // Pattern 2: Dynamic imports // Matches: import('../ts_shared/...') const dynamicImportPattern = new RegExp( `(import\\s*\\(\\s*['"])((?:\\.\\.\\/)+)${this.escapeRegex(sourceFolder)}(\\/[^'"]*['"]\\s*\\))`, 'g' ); result = result.replace(dynamicImportPattern, `$1$2${destFolder}$3`); // Pattern 3: CommonJS require // Matches: require('../ts_shared/...') const requirePattern = new RegExp( `(require\\s*\\(\\s*['"])((?:\\.\\.\\/)+)${this.escapeRegex(sourceFolder)}(\\/[^'"]*['"]\\s*\\))`, 'g' ); result = result.replace(requirePattern, `$1$2${destFolder}$3`); } return result; } /** * Escape special regex characters in a string */ private escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } } /** * Convenience function to rewrite import paths in output directories * * @param globPatterns - Map of source patterns to destination directories * @param outputDirs - List of output directories to process * @returns Total number of files modified */ export async function rewriteImportPaths( globPatterns: Record, outputDirs: string[] ): Promise { const rewriter = TsPathRewriter.fromGlobPatterns(globPatterns); let totalModified = 0; for (const dir of outputDirs) { totalModified += await rewriter.rewriteDirectory(dir); } return totalModified; }