/** * Library Extraction Orchestrator — Coordinator * * Coordinates the full library extraction pipeline: * 1. Pre-condition validation * 2. Angular library creation via CLI * 3. Artifact migration (file moves + templateUrl/styleUrls rewriting) * 4. Import path rewriting across codebase * 5. Secondary entry point creation * 6. Build configuration updates (tsconfig paths, ng-package.json) * 7. Post-condition validation * * Options arrive via context.config (not context.rootPath/libraryName which * do not exist on TransformContext). * * Three guarantees: * - Idempotent: checks state before each step (skips if already done) * - Atomic: rollback on error via sourceFile.replaceWithText(originalText) * - Reversible: originalText stored in metadata for potential rollback */ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { TransformContext, TransformResult, } from '@angular-modernizer/plugin-system'; import type { PublicApi } from '@angular-modernizer/api'; import { AngularCliLibraryGenerator } from './angular-cli-library-generator.js'; import { ArtifactMigrator } from './artifact-migrator.js'; import { ImportPathRewriter } from './import-path-rewriter.js'; import { BuildConfigUpdater } from './build-config-updater.js'; import { LibraryValidator } from './library-validator.js'; /** * Options for library extraction. * Passed via context.config (the transform-code MCP tool's 'options' field). */ export interface LibraryExtractionOptions { /** Library name (e.g., 'ui', 'forms'). */ libraryName: string; /** NPM scope prefix (e.g., '@myapp'). */ libraryPrefix: string; /** Library type strategy. Default: 'angular-library'. */ libraryType?: 'angular-library' | 'folder-move'; /** Target output folder. Default: 'projects/'. */ outputFolder?: string; /** File paths of artifacts to migrate. */ artifacts: string[]; /** Secondary entry point names (e.g., ['button', 'card']). */ secondaryEntryPoints?: string[]; /** Dry run — analyze only, no modifications. */ dryRun?: boolean; } /** * Library Extraction Orchestrator. * * Thin coordinator: delegates to 5 specialized collaborators, each with * a single responsibility. The coordinator is responsible only for * sequencing, error handling, and result aggregation. */ export class LibraryExtractionOrchestrator { private readonly cliGenerator = new AngularCliLibraryGenerator(); private readonly migrator = new ArtifactMigrator(); private readonly importRewriter = new ImportPathRewriter(); private readonly buildConfigUpdater = new BuildConfigUpdater(); private readonly validator = new LibraryValidator(); async run(context: TransformContext): Promise { // Options come from context.config (context.rootPath/libraryName don't exist) const options = context.config as unknown as LibraryExtractionOptions; const filePath = context.sourceFile.getFilePath(); const originalText = context.sourceFile.getFullText(); // Dry run: validate only if (options.dryRun) { return this.dryRunResult(options, filePath); } try { // Step 1: Validate pre-conditions this.validator.validatePreConditions(options, context.project); // Derive project root from source file path // (context.api.project.rootPath or fallback to ts-morph project root) const projectRoot = this.resolveProjectRoot(context); // Step 2: Create Angular library via CLI const libraryPath = await this.cliGenerator.create(projectRoot, options); // Step 3: Migrate artifacts const migrationResults = await this.migrator.migrate( options.artifacts, libraryPath, context.api, context.project, ); const successfulMigrations = migrationResults.filter((r) => r.success); const failedMigrations = migrationResults.filter((r) => !r.success); // Step 4: Rewrite imports across codebase const importResults = await this.importRewriter.rewrite( context.project, libraryPath, options, context.api, ); // Step 5: Create secondary entry points await this.cliGenerator.createSecondaryEntryPoints(libraryPath, options); // Step 6: Update build configurations await this.buildConfigUpdater.update(projectRoot, options, libraryPath); // Step 7: Post-condition validation const postChecks = this.validator.validatePostConditions( options, libraryPath, ); const postWarnings = postChecks .filter((c) => !c.passed) .map((c) => c.message); const totalImportsUpdated = importResults.reduce( (sum, r) => sum + r.importsUpdated, 0, ); return { ruleId: 'refactor:library-extraction', modified: successfulMigrations.length > 0, filePath, message: `Extracted ${successfulMigrations.length}/${options.artifacts.length} artifacts ` + `to ${options.libraryPrefix}/${options.libraryName}` + (failedMigrations.length > 0 ? ` (${failedMigrations.length} failed)` : ''), changeCount: successfulMigrations.length, metadata: { libraryPath, originalText, migratedArtifacts: successfulMigrations.length, failedArtifacts: failedMigrations.map((r) => r.sourcePath), updatedImports: totalImportsUpdated, filesWithUpdatedImports: importResults.length, postValidationWarnings: postWarnings, libraryName: options.libraryName, libraryPrefix: options.libraryPrefix, }, }; } catch (err) { // Atomic rollback: restore the original source file content context.sourceFile.replaceWithText(originalText); return { ruleId: 'refactor:library-extraction', modified: false, filePath, message: `Library extraction failed: ${err instanceof Error ? err.message : String(err)}`, changeCount: 0, metadata: { originalText, error: err instanceof Error ? err.message : String(err), }, }; } } // Helpers private dryRunResult( options: LibraryExtractionOptions, filePath: string, ): TransformResult { return { ruleId: 'refactor:library-extraction', modified: false, filePath, message: `[DRY RUN] Would extract ${options.artifacts.length} artifacts ` + `to ${options.libraryPrefix}/${options.libraryName}`, changeCount: 0, metadata: { dryRun: true, libraryName: options.libraryName, libraryPrefix: options.libraryPrefix, artifactCount: options.artifacts.length, secondaryEntryPoints: options.secondaryEntryPoints ?? [], }, }; } private resolveProjectRoot(context: TransformContext): string { // Try to get rootPath from api.project if available const api = context.api as { project?: { rootPath?: string } }; if (api.project?.rootPath) { return api.project.rootPath; } // Fall back: derive from first source file path // Walk up until we find angular.json or tsconfig.json const firstFilePath = context.sourceFile.getFilePath(); let dir = firstFilePath.includes('/') ? firstFilePath.substring(0, firstFilePath.lastIndexOf('/')) : process.cwd(); for (let i = 0; i < 10; i++) { if ( fs.existsSync(path.join(dir, 'angular.json')) || fs.existsSync(path.join(dir, 'tsconfig.json')) ) { return dir; } const parent = dir.substring(0, dir.lastIndexOf('/')); if (!parent || parent === dir) { break; } dir = parent; } return process.cwd(); } }