diff --git a/ts/index.ts b/ts/index.ts index 3704fc5..10a1979 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -5,6 +5,7 @@ early.start('tsbuild'); export * from './mod_fs/index.js'; export * from './mod_config/index.js'; export * from './mod_unpack/index.js'; +export * from './mod_pathrewrite/index.js'; export * from './mod_compiler/index.js'; export * from './mod_cli/index.js'; diff --git a/ts/mod_compiler/classes.tscompiler.ts b/ts/mod_compiler/classes.tscompiler.ts index ae00f4e..3a308bb 100644 --- a/ts/mod_compiler/classes.tscompiler.ts +++ b/ts/mod_compiler/classes.tscompiler.ts @@ -7,6 +7,7 @@ import * as smartpath from '@push.rocks/smartpath'; import { TsConfig } from '../mod_config/index.js'; import { FsHelpers } from '../mod_fs/index.js'; import { performUnpack } from '../mod_unpack/index.js'; +import { TsPathRewriter } from '../mod_pathrewrite/index.js'; /** * Interface for error summary data @@ -307,6 +308,7 @@ export class TsCompiler { ): Promise { const emittedFiles: string[] = []; const errorSummaries: IErrorSummary[] = []; + const successfulOutputDirs: string[] = []; const totalTasks = Object.keys(globPatterns).length; let currentTask = 0; @@ -365,6 +367,20 @@ export class TsCompiler { // Perform unpack if compilation succeeded if (result.errorSummary.totalErrors === 0) { await performUnpack(pattern, destDir, this.cwd); + successfulOutputDirs.push(destDir); + } + } + + // Rewrite import paths in all output directories to handle cross-module references + // This must happen after ALL compilations so all destination folders exist + if (successfulOutputDirs.length > 0) { + const rewriter = TsPathRewriter.fromGlobPatterns(globPatterns); + let totalRewritten = 0; + for (const outputDir of successfulOutputDirs) { + totalRewritten += await rewriter.rewriteDirectory(outputDir); + } + if (totalRewritten > 0 && !isQuiet && !isJson) { + console.log(` 🔄 Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`); } } diff --git a/ts/mod_pathrewrite/classes.tspathrewriter.ts b/ts/mod_pathrewrite/classes.tspathrewriter.ts new file mode 100644 index 0000000..e4ba726 --- /dev/null +++ b/ts/mod_pathrewrite/classes.tspathrewriter.ts @@ -0,0 +1,200 @@ +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; +} diff --git a/ts/mod_pathrewrite/index.ts b/ts/mod_pathrewrite/index.ts new file mode 100644 index 0000000..f944279 --- /dev/null +++ b/ts/mod_pathrewrite/index.ts @@ -0,0 +1 @@ +export * from './classes.tspathrewriter.js';