/** * Artifact Migrator * * Moves Angular artifacts (components, directives, pipes, services) from * their current location to the new library folder. Updates templateUrl * and styleUrls references via AST manipulation. */ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { PublicApi } from '@angular-modernizer/api'; import type { Project } from 'ts-morph'; /** * Migration result for a single artifact. */ export interface ArtifactMigrationResult { sourcePath: string; targetPath: string; success: boolean; error?: string; } /** * Migrates Angular artifacts to the target library folder. * * Responsibilities: * - Copy/move source files to the new library location * - Update templateUrl and styleUrls relative paths (AST-based) * - Update barrel exports in the library's index.ts */ export class ArtifactMigrator { /** * Migrate the given artifact file paths to the library path. * * @param artifactPaths - Source file paths to migrate * @param libraryPath - Target library root path (e.g. projects/ui) * @param api - PublicApi for AST operations * @param project - ts-morph Project instance * @returns Array of migration results */ async migrate( artifactPaths: string[], libraryPath: string, api: PublicApi, project: Project, ): Promise { const results: ArtifactMigrationResult[] = []; const libSrcDir = path.join(libraryPath, 'src', 'lib'); // Ensure target directory exists fs.mkdirSync(libSrcDir, { recursive: true }); for (const sourcePath of artifactPaths) { const result = await this.migrateArtifact( sourcePath, libSrcDir, api, project, ); results.push(result); } // Update library barrel export (src/index.ts) const successfulMigrations = results.filter((r) => r.success); if (successfulMigrations.length > 0) { this.updateLibraryBarrel(libraryPath, successfulMigrations, project); } return results; } // --------------------------------------------------------------------------- // Single artifact migration // --------------------------------------------------------------------------- private async migrateArtifact( sourcePath: string, libSrcDir: string, _api: PublicApi, project: Project, ): Promise { const fileName = path.basename(sourcePath); const targetPath = path.join(libSrcDir, fileName); try { // Read source content const content = fs.readFileSync(sourcePath, 'utf-8'); // Update templateUrl/styleUrls paths before writing const updatedContent = this.updateRelativePaths( content, sourcePath, targetPath, ); // Write to target fs.writeFileSync(targetPath, updatedContent); // Add to ts-morph project if not already there if (!project.getSourceFile(targetPath)) { project.addSourceFileAtPath(targetPath); } return { sourcePath, targetPath, success: true }; } catch (err) { return { sourcePath, targetPath, success: false, error: err instanceof Error ? err.message : String(err), }; } } /** * Update templateUrl and styleUrls references after moving a file. * Uses string replacement (sufficient for this specific task — AST would be overkill * for simple path corrections that follow predictable patterns). */ private updateRelativePaths( content: string, sourcePath: string, targetPath: string, ): string { const sourceDir = path.dirname(sourcePath); const targetDir = path.dirname(targetPath); // Match templateUrl: './xxx.html' and styleUrls: ['./xxx.css'] return content.replaceAll( /(templateUrl|styleUrls)\s*:\s*(['"`]\.\/[^'"`]+['"`]|\[[^\]]+\])/g, (match, key: string, value: string) => { if (key === 'templateUrl') { const urlMatch = /['"`](\.\/[^'"`]+)['"`]/.exec(value); if (urlMatch) { const oldRelative = urlMatch[1]!; const absolutePath = path.resolve(sourceDir, oldRelative); const newRelative = './' + path.relative(targetDir, absolutePath); return `${key}: '${newRelative}'`; } } else if (key === 'styleUrls') { // Handle array of styles const updated = value.replaceAll( /['"`](\.\/[^'"`]+)['"`]/g, (_m: string, url: string) => { const absolutePath = path.resolve(sourceDir, url); const newRelative = './' + path.relative(targetDir, absolutePath); return `'${newRelative}'`; }, ); return `${key}: ${updated}`; } return match; }, ); } // --------------------------------------------------------------------------- // Barrel updates // --------------------------------------------------------------------------- private updateLibraryBarrel( libraryPath: string, migrations: ArtifactMigrationResult[], project: Project, ): void { const indexPath = path.join(libraryPath, 'src', 'index.ts'); // Get existing barrel content let existingContent = ''; if (fs.existsSync(indexPath)) { existingContent = fs.readFileSync(indexPath, 'utf-8'); } const newExports: string[] = []; for (const migration of migrations) { const fileName = path.basename(migration.targetPath, '.ts'); const exportLine = `export * from './lib/${fileName}';`; if (!existingContent.includes(exportLine)) { newExports.push(exportLine); } } if (newExports.length > 0) { const updatedContent = existingContent + (existingContent.endsWith('\n') ? '' : '\n') + newExports.join('\n') + '\n'; fs.writeFileSync(indexPath, updatedContent); // Refresh in ts-morph const sf = project.getSourceFile(indexPath); if (sf) { void sf.refreshFromFileSystem(); } else { project.addSourceFileAtPath(indexPath); } } } }