/** * Import Path Rewriter * * Updates import statements across the entire codebase after artifact migration. * Converts relative paths pointing to migrated files to library package imports. * * Example: * import { ButtonComponent } from '../../shared/ui/button/button.component'; * → import { ButtonComponent } from '@myapp/ui/button'; */ import * as path from 'node:path'; import type { Project, SourceFile } from 'ts-morph'; import type { PublicApi } from '@angular-modernizer/api'; import type { LibraryExtractionOptions } from './library-extraction.orchestrator.js'; /** * Result of rewriting imports in a single file. */ export interface ImportRewriteResult { filePath: string; importsUpdated: number; updatedFrom: string[]; updatedTo: string[]; } /** * Rewrites import paths across the codebase after artifact migration. * * Algorithm: * 1. Build a map: absoluteArtifactPath → newImportSpecifier * 2. For each source file in the project, find imports whose resolved path * matches a migrated artifact * 3. Replace the import specifier with the library path */ export class ImportPathRewriter { /** * Rewrite all imports referencing the migrated artifacts. * * @param project - ts-morph Project for AST manipulation * @param libraryPath - Path of the created library * @param options - Library extraction options * @param api - PublicApi (for future use with SymbolLocator) * @returns Results per file that was updated */ async rewrite( project: Project, libraryPath: string, options: LibraryExtractionOptions, _api: PublicApi, ): Promise { // Build the artifact → new specifier map const artifactMap = this.buildArtifactMap(options, libraryPath); if (artifactMap.size === 0) { return []; } const results: ImportRewriteResult[] = []; const allSourceFiles = project.getSourceFiles(); for (const sourceFile of allSourceFiles) { const result = this.rewriteImportsInFile(sourceFile, artifactMap); if (result.importsUpdated > 0) { results.push(result); // Persist changes await sourceFile.save(); } } return results; } // --------------------------------------------------------------------------- // Artifact map // --------------------------------------------------------------------------- /** * Build a map from artifact absolute paths to new library import specifiers. * * For each artifact path, we derive the import specifier based on the * secondary entry point it belongs to or the root library package. * * Example: * /project/src/shared/ui/button.component.ts → @myapp/ui/button */ private buildArtifactMap( options: LibraryExtractionOptions, libraryPath: string, ): Map { const map = new Map(); const libSrcDir = path.join(libraryPath, 'src', 'lib'); for (const artifactPath of options.artifacts) { // Determine which secondary entry point this belongs to const entryPoint = this.findEntryPoint( artifactPath, options.secondaryEntryPoints ?? [], ); const importSpecifier = entryPoint ? `${options.libraryPrefix}/${options.libraryName}/${entryPoint}` : `${options.libraryPrefix}/${options.libraryName}`; // Map both the original path and the new library path map.set(path.resolve(artifactPath), importSpecifier); // Also map the path under the new library (for self-imports within the lib) const newPath = path.join(libSrcDir, path.basename(artifactPath, '.ts')); map.set(path.resolve(newPath), importSpecifier); } return map; } private findEntryPoint( artifactPath: string, secondaryEntryPoints: string[], ): string | undefined { const lowerPath = artifactPath.toLowerCase(); return secondaryEntryPoints.find( (ep) => lowerPath.includes(`/${ep}/`) || lowerPath.includes(`/${ep}.`), ); } // --------------------------------------------------------------------------- // Per-file rewriting // --------------------------------------------------------------------------- private rewriteImportsInFile( sourceFile: SourceFile, artifactMap: Map, ): ImportRewriteResult { const filePath = sourceFile.getFilePath(); const fileDir = path.dirname(filePath); let importsUpdated = 0; const updatedFrom: string[] = []; const updatedTo: string[] = []; for (const importDecl of sourceFile.getImportDeclarations()) { const specifier = importDecl.getModuleSpecifierValue(); // Only process relative imports if (!specifier.startsWith('.')) { continue; } // Resolve the absolute path of the imported module const resolvedPath = path.resolve(fileDir, specifier); // Also try with .ts extension const resolvedWithTs = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'; const newSpecifier = artifactMap.get(resolvedWithTs) ?? artifactMap.get(resolvedPath); if (newSpecifier) { updatedFrom.push(specifier); importDecl.setModuleSpecifier(newSpecifier); updatedTo.push(newSpecifier); importsUpdated++; } } return { filePath, importsUpdated, updatedFrom, updatedTo }; } }