fix(TsPathRewriter): rewrite cross-module import paths after compilation
When compiling multiple ts_* source directories to dist_ts_* outputs, imports like '../ts_shared/helper.js' now correctly become '../dist_ts_shared/helper.js' in the compiled output. Uses targeted regex patterns that only match import/export/require statements, avoiding false positives in strings or comments.
This commit is contained in:
@@ -5,6 +5,7 @@ early.start('tsbuild');
|
|||||||
export * from './mod_fs/index.js';
|
export * from './mod_fs/index.js';
|
||||||
export * from './mod_config/index.js';
|
export * from './mod_config/index.js';
|
||||||
export * from './mod_unpack/index.js';
|
export * from './mod_unpack/index.js';
|
||||||
|
export * from './mod_pathrewrite/index.js';
|
||||||
export * from './mod_compiler/index.js';
|
export * from './mod_compiler/index.js';
|
||||||
export * from './mod_cli/index.js';
|
export * from './mod_cli/index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as smartpath from '@push.rocks/smartpath';
|
|||||||
import { TsConfig } from '../mod_config/index.js';
|
import { TsConfig } from '../mod_config/index.js';
|
||||||
import { FsHelpers } from '../mod_fs/index.js';
|
import { FsHelpers } from '../mod_fs/index.js';
|
||||||
import { performUnpack } from '../mod_unpack/index.js';
|
import { performUnpack } from '../mod_unpack/index.js';
|
||||||
|
import { TsPathRewriter } from '../mod_pathrewrite/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for error summary data
|
* Interface for error summary data
|
||||||
@@ -307,6 +308,7 @@ export class TsCompiler {
|
|||||||
): Promise<ICompileResult> {
|
): Promise<ICompileResult> {
|
||||||
const emittedFiles: string[] = [];
|
const emittedFiles: string[] = [];
|
||||||
const errorSummaries: IErrorSummary[] = [];
|
const errorSummaries: IErrorSummary[] = [];
|
||||||
|
const successfulOutputDirs: string[] = [];
|
||||||
|
|
||||||
const totalTasks = Object.keys(globPatterns).length;
|
const totalTasks = Object.keys(globPatterns).length;
|
||||||
let currentTask = 0;
|
let currentTask = 0;
|
||||||
@@ -365,6 +367,20 @@ export class TsCompiler {
|
|||||||
// Perform unpack if compilation succeeded
|
// Perform unpack if compilation succeeded
|
||||||
if (result.errorSummary.totalErrors === 0) {
|
if (result.errorSummary.totalErrors === 0) {
|
||||||
await performUnpack(pattern, destDir, this.cwd);
|
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' : ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
200
ts/mod_pathrewrite/classes.tspathrewriter.ts
Normal file
200
ts/mod_pathrewrite/classes.tspathrewriter.ts
Normal file
@@ -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<string, string>): 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<boolean> {
|
||||||
|
// 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<number> {
|
||||||
|
let modifiedCount = 0;
|
||||||
|
|
||||||
|
const processDir = async (currentDir: string): Promise<void> => {
|
||||||
|
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<string, string>,
|
||||||
|
outputDirs: string[]
|
||||||
|
): Promise<number> {
|
||||||
|
const rewriter = TsPathRewriter.fromGlobPatterns(globPatterns);
|
||||||
|
let totalModified = 0;
|
||||||
|
|
||||||
|
for (const dir of outputDirs) {
|
||||||
|
totalModified += await rewriter.rewriteDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalModified;
|
||||||
|
}
|
||||||
1
ts/mod_pathrewrite/index.ts
Normal file
1
ts/mod_pathrewrite/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './classes.tspathrewriter.js';
|
||||||
Reference in New Issue
Block a user