/** * Library Validator * * Validates pre-conditions before library extraction and post-conditions * after migration to ensure the library is correctly structured. */ import * as path from 'node:path'; import * as fs from 'node:fs'; import type { Project } from 'ts-morph'; import type { LibraryExtractionOptions } from './library-extraction.orchestrator.js'; /** * Result of a validation check. */ export interface ValidationCheck { passed: boolean; message: string; } /** * Validates library extraction pre-conditions and post-conditions. */ export class LibraryValidator { /** * Validate that extraction can safely proceed. * Throws with a descriptive message if any pre-condition fails. */ validatePreConditions( options: LibraryExtractionOptions, project: Project, ): void { const checks = this.runPreConditionChecks(options, project); const failures = checks.filter((c) => !c.passed); if (failures.length > 0) { const messages = failures.map((f) => `- ${f.message}`).join('\n'); throw new Error( `Library extraction pre-condition failures:\n${messages}`, ); } } /** * Validate library structure after migration. */ validatePostConditions( options: LibraryExtractionOptions, libraryPath: string, ): ValidationCheck[] { return this.runPostConditionChecks(options, libraryPath); } // --------------------------------------------------------------------------- // Pre-condition checks // --------------------------------------------------------------------------- private runPreConditionChecks( options: LibraryExtractionOptions, project: Project, ): ValidationCheck[] { const checks: ValidationCheck[] = []; // 1. Library name is not empty checks.push({ passed: typeof options.libraryName === 'string' && options.libraryName.length > 0, message: 'libraryName must be a non-empty string', }); // 2. Library prefix has correct format checks.push({ passed: /^@[a-z][a-z0-9-]*/.test(options.libraryPrefix), message: `libraryPrefix "${options.libraryPrefix}" must match pattern ^@[a-z][a-z0-9-]*`, }); // 3. At least one artifact to migrate checks.push({ passed: Array.isArray(options.artifacts) && options.artifacts.length > 0, message: 'artifacts list must be non-empty', }); // 4. All artifact files exist in the project if (Array.isArray(options.artifacts)) { for (const artifactPath of options.artifacts) { const exists = project.getSourceFile(artifactPath) !== undefined || fs.existsSync(artifactPath); checks.push({ passed: exists, message: `Artifact does not exist: ${artifactPath}`, }); } } // 5. No circular dependencies in the artifact set const circularResult = this.checkCircularDependencies( options.artifacts ?? [], project, ); checks.push(circularResult); return checks; } private checkCircularDependencies( artifacts: string[], project: Project, ): ValidationCheck { const artifactSet = new Set(artifacts.map((a) => path.resolve(a))); for (const artifactPath of artifacts) { const sf = project.getSourceFile(artifactPath); if (!sf) { continue; } for (const imp of sf.getImportDeclarations()) { const specifier = imp.getModuleSpecifierValue(); if (!specifier.startsWith('.')) { continue; } const resolvedPath = path.resolve(path.dirname(artifactPath), specifier) + '.ts'; if (artifactSet.has(path.resolve(resolvedPath))) { // This artifact imports another in the set — check reverse const importedSf = project.getSourceFile(resolvedPath); if (importedSf) { const reverseImports = importedSf .getImportDeclarations() .map((i) => i.getModuleSpecifierValue()); const base = path.basename(artifactPath, '.ts'); if (reverseImports.some((ri) => ri.includes(base))) { return { passed: false, message: `Circular dependency detected between artifacts: ${path.basename(artifactPath)} ↔ ${path.basename(resolvedPath)}. Resolve circular dependencies before extraction.`, }; } } } } } return { passed: true, message: 'No circular dependencies detected in artifact set', }; } // --------------------------------------------------------------------------- // Post-condition checks // --------------------------------------------------------------------------- private runPostConditionChecks( options: LibraryExtractionOptions, libraryPath: string, ): ValidationCheck[] { const checks: ValidationCheck[] = []; // 1. Library directory was created checks.push({ passed: fs.existsSync(libraryPath), message: `Library directory was created at ${libraryPath}`, }); // 2. src/index.ts exists const indexPath = path.join(libraryPath, 'src', 'index.ts'); checks.push({ passed: fs.existsSync(indexPath), message: `Library barrel file exists at ${indexPath}`, }); // 3. ng-package.json exists (for angular-library strategy) if (options.libraryType === 'angular-library') { const ngPackagePath = path.join(libraryPath, 'ng-package.json'); checks.push({ passed: fs.existsSync(ngPackagePath), message: `ng-package.json exists at ${ngPackagePath}`, }); } // 4. At least some artifacts migrated const libSrcDir = path.join(libraryPath, 'src', 'lib'); const migratedFiles = fs.existsSync(libSrcDir) ? fs.readdirSync(libSrcDir).filter((f) => f.endsWith('.ts')) : []; checks.push({ passed: migratedFiles.length > 0, message: `${migratedFiles.length} artifacts migrated to library`, }); return checks; } }