/** * @angular-modernizer/plugin-angular - Facade Pattern Orchestrator * * The central "brain" of the facade pattern plugin. * Orchestrates the extraction of complex component logic into facade services. * * Philosophy: "API-Driven Orchestration" * - Receives all capabilities via TransformContext * - Uses PublicApi for all AST operations * - Stateless - no constructor dependencies * - Pure business logic, no low-level manipulation */ import type { TransformContext } from '@angular-modernizer/plugin-system'; import type { PublicApi } from '@angular-modernizer/api'; import type { SourceFile, ClassDeclaration, MethodDeclaration, PropertyAccessExpression, } from 'ts-morph'; import { SyntaxKind } from 'ts-morph'; /** * Configuration options for facade pattern extraction. */ export interface FacadePatternConfig { /** * Minimum number of methods to trigger facade extraction. * Default: 8 */ minMethodsForFacade?: number; /** * Minimum complexity score to trigger facade extraction. * Default: 50 */ minComplexityForFacade?: number; /** * Whether to create facade services. * Default: true */ createFacadeServices?: boolean; /** * Whether to update component to use facade services. * Default: true */ updateComponentUsage?: boolean; /** * Whether to add Injectable decorator to facade services. * Default: true */ addInjectableDecorator?: boolean; } /** * Facade Pattern Orchestrator * * @remarks * The central orchestrator for extracting complex component logic into facade services. * This class analyzes Angular components and extracts business logic into dedicated * facade services for better separation of concerns and maintainability. * * ## Architecture Pattern * * This orchestrator follows three core principles: * 1. **Stateless Design** - No constructor dependencies * 2. **Context-Driven** - All capabilities injected via {@link TransformContext} * 3. **API-First** - Uses {@link PublicApi} for all AST operations * * ## Facade Extraction Workflow * * The orchestrator performs the following transformations: * 1. Analyze components for complexity (method count, complexity score) * 2. Identify business logic methods to extract * 3. Create facade service with extracted methods * 4. Update component to inject and use facade service * 5. Update imports and dependencies * * ## Example Usage * * ```typescript * const orchestrator = new FacadePatternOrchestrator(); * const context = ContextFactory.createTransformContext({ * sourceFile, * project, * api, * config: { minMethodsForFacade: 10 } * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * @see {@link PublicApi} for available API tools * @see {@link FacadePatternConfig} for configuration options * * @public */ export class FacadePatternOrchestrator { /** * Creates a new FacadePatternOrchestrator instance. * * @remarks * The constructor is intentionally stateless. All dependencies are provided * through the {@link TransformContext} passed to the {@link run} method. * This enables testing, composition, and prevents tight coupling. * * @public */ constructor() { // Intentionally empty - follows the new architecture pattern } /** * Executes the facade pattern extraction on a single source file. * * @remarks * This method is the main entry point for the orchestrator. It receives all * necessary dependencies (project, API, config) through the context parameter, * analyzes components for complexity, and extracts facade services as needed. * * The method operates on one source file at a time and is safe to call * multiple times. It will: * - Analyze component complexity * - Extract business logic into facade services * - Update component dependencies and usage * * ## Facade Extraction Behavior * * **Complexity Analysis**: * - Counts methods in component * - Calculates complexity score based on method bodies * - Identifies business logic vs. lifecycle methods * * **Facade Service Creation**: * - Creates new service file with extracted methods * - Adds @Injectable decorator * - Moves complex business logic from component * * **Component Updates**: * - Injects facade service in constructor * - Updates method calls to use facade service * - Removes extracted methods from component * * @param context - The transformation context containing: * - `sourceFile`: The TypeScript source file to analyze * - `project`: The ts-morph Project for cross-file operations * - `api`: The PublicApi with analysis and transformation tools * - `config`: User-provided configuration options * * @example * ```typescript * // Basic usage * orchestrator.run(context); * * // With custom configuration * const context = ContextFactory.createTransformContext({ * sourceFile, * project, * api, * config: { * minMethodsForFacade: 5, // Lower threshold * createFacadeServices: true, * } * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * @see {@link FacadePatternConfig} for configuration options * * @public */ run(context: TransformContext): void { const { sourceFile, config } = context; // Get plugin-specific configuration const pluginConfig = config['@angular-modernizer/plugin-angular'] as | Record | undefined; const facadeConfig = this.getFacadeConfig(pluginConfig ?? {}); // Find all classes in the file const classes = sourceFile.getClasses(); for (const classDecl of classes) { // Check if this class needs facade extraction if (this.needsFacadeExtraction(classDecl, facadeConfig)) { this.extractFacadeForComponent( classDecl, sourceFile, context, facadeConfig, ); } } } /** * Check if a component needs facade extraction. */ private needsFacadeExtraction( classDecl: ClassDeclaration, config: FacadePatternConfig, ): boolean { // Only process Angular components if (!this.isAngularComponent(classDecl)) { return false; } const methods = classDecl.getMethods(); const businessMethods = this.getBusinessMethods(methods); // Check method count threshold if (businessMethods.length >= config.minMethodsForFacade!) { return true; } // Check complexity score const complexityScore = this.calculateComplexityScore(businessMethods); if (complexityScore >= config.minComplexityForFacade!) { return true; } return false; } /** * Extract facade service for a complex component. */ private extractFacadeForComponent( classDecl: ClassDeclaration, sourceFile: SourceFile, context: TransformContext, config: FacadePatternConfig, ): void { const className = classDecl.getName(); if (!className) { return; } // Identify methods to extract const methods = classDecl.getMethods(); const businessMethods = this.getBusinessMethods(methods); if (businessMethods.length === 0) { return; } // Collect method information before any modifications const methodInfos = businessMethods .map((method) => ({ name: method.getName(), text: method.getText(), })) .filter((info) => info.name !== undefined); // Analyze service dependencies used by extracted methods const serviceDependencies = this.analyzeServiceDependencies( businessMethods, classDecl, ); // Create facade service if (config.createFacadeServices) { this.createFacadeService( classDecl, businessMethods, serviceDependencies, context, config, ); } // Update component to use facade if (config.updateComponentUsage) { this.updateComponentToUseFacade( className, sourceFile, methodInfos, serviceDependencies, config, ); } } /** * Analyze service dependencies used by extracted methods. */ private analyzeServiceDependencies( methods: MethodDeclaration[], classDecl: ClassDeclaration, ): { name: string; type: string; propertyName: string }[] { const constructor = classDecl.getConstructors()[0]; if (!constructor) { return []; } const constructorParams = constructor.getParameters(); const serviceDependencies: { name: string; type: string; propertyName: string; }[] = []; // Get all property access expressions in the methods that reference 'this' const usedServices = new Set(); for (const method of methods) { const propertyAccesses = method.getDescendantsOfKind( SyntaxKind.PropertyAccessExpression, ); for (const propAccess of propertyAccesses) { if (propAccess.getExpression().getText() === 'this') { const propertyName = propAccess.getName(); usedServices.add(propertyName); } } } // Check which constructor parameters correspond to the used services for (const param of constructorParams) { const paramName = param.getName(); if (usedServices.has(paramName)) { const paramType = param.getType().getText(); serviceDependencies.push({ name: paramName, type: paramType, propertyName: paramName, }); } } return serviceDependencies; } /** * Check if a class is an Angular component. */ private isAngularComponent(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((decorator) => decorator.getName() === 'Component'); } /** * Get business logic methods (excluding lifecycle hooks). */ private getBusinessMethods( methods: MethodDeclaration[], ): MethodDeclaration[] { const lifecycleHooks = [ 'ngOnInit', 'ngOnChanges', 'ngOnDestroy', 'ngAfterViewInit', 'ngAfterViewChecked', 'ngAfterContentInit', 'ngAfterContentChecked', 'ngDoCheck', ]; return methods.filter((method) => { const methodName = method.getName(); return methodName && !lifecycleHooks.includes(methodName); }); } /** * Calculate complexity score for methods. */ private calculateComplexityScore(methods: MethodDeclaration[]): number { let totalScore = 0; for (const method of methods) { const body = method.getBody(); if (!body) { continue; } const bodyText = body.getFullText(); // Simple complexity metrics const linesOfCode = bodyText.split('\n').length; const conditionalStatements = ( bodyText.match(/\b(if|else|switch|case|for|while|do)\b/g) ?? [] ).length; const methodCalls = (bodyText.match(/\w+\(/g) ?? []).length; // Calculate method complexity const methodScore = linesOfCode + conditionalStatements * 2 + methodCalls * 1; totalScore += methodScore; } return totalScore; } /** * Create a facade service with extracted methods. */ private createFacadeService( classDecl: ClassDeclaration, methods: MethodDeclaration[], serviceDependencies: { name: string; type: string; propertyName: string }[], context: TransformContext, config: FacadePatternConfig, ): void { const className = classDecl.getName(); if (!className) { return; } // Generate facade service name const facadeName = `${className.replace('Component', '')}FacadeService`; // Create facade service content const facadeContent = this.generateFacadeServiceContent( facadeName, methods, serviceDependencies, classDecl, config, ); // Create new source file for the facade service // For now, we'll add the facade service to the same file // In a real implementation, we'd create a separate file this.addFacadeServiceToFile(context.sourceFile, facadeContent); } /** * Generate facade service content. */ private generateFacadeServiceContent( facadeName: string, methods: MethodDeclaration[], serviceDependencies: { name: string; type: string; propertyName: string }[], _classDecl: ClassDeclaration, config: FacadePatternConfig, ): string { const injectableDecorator = config.addInjectableDecorator ? "@Injectable({ providedIn: 'root' })\n" : ''; const methodSignatures = methods .map((method) => { const methodName = method.getName(); const parameters = method.getParameters(); const returnType = method.getReturnType().getText(); const paramList = parameters .map((p) => `${p.getName()}: ${p.getType().getText()}`) .join(', '); return ` ${methodName}(${paramList}): ${returnType} {\n // TODO: Implement ${methodName}\n }`; }) .join('\n\n'); // Generate constructor if there are service dependencies let constructorText = ''; if (serviceDependencies.length > 0) { const constructorParams = serviceDependencies .map((dep) => `private ${dep.name}: ${dep.type}`) .join(', '); constructorText = ` constructor(${constructorParams}) {}\n\n`; } return ` ${injectableDecorator}export class ${facadeName} { ${constructorText}${methodSignatures} } `; } /** * Add facade service to the source file. */ private addFacadeServiceToFile( sourceFile: SourceFile, facadeContent: string, ): void { // Add the facade service at the end of the file const fullText = sourceFile.getFullText(); sourceFile.replaceWithText(fullText + '\n\n' + facadeContent); } /** * Update component to use facade service. */ private updateComponentToUseFacade( className: string, sourceFile: SourceFile, methodInfos: { name: string; text: string }[], serviceDependencies: { name: string; type: string; propertyName: string }[], _config: FacadePatternConfig, ): void { // Find the class again (in case it was modified) const classDecl = sourceFile.getClass(className); if (!classDecl) { return; } const facadeName = `${className.replace('Component', '')}FacadeService`; // Remove migrated service dependencies from component constructor this.removeMigratedServicesFromComponent(classDecl, serviceDependencies); // Add facade service injection to constructor this.addFacadeInjection(classDecl, facadeName); // Collect method names const methodNames = methodInfos.map((info) => info.name); // Update method calls to use facade for (const methodName of methodNames) { // Replace this.methodName() calls with this.facade.methodName() this.replaceMethodCalls(classDecl, methodName, facadeName); } // Remove extracted methods from component for (const methodInfo of methodInfos) { // Find and remove the method by name const method = classDecl.getMethod(methodInfo.name); if (method) { method.remove(); } } } /** * Remove migrated services from component constructor. */ private removeMigratedServicesFromComponent( classDecl: ClassDeclaration, serviceDependencies: { name: string; type: string; propertyName: string }[], ): void { const constructor = classDecl.getConstructors()[0]; if (!constructor) { return; } const parameters = constructor.getParameters(); const serviceNames = new Set(serviceDependencies.map((dep) => dep.name)); // Remove parameters that correspond to migrated services for (const param of [...parameters]) { // Create a copy to avoid modification issues if (serviceNames.has(param.getName())) { param.remove(); } } // Clean up constructor if it becomes empty const remainingParams = constructor.getParameters(); if (remainingParams.length === 0) { // Remove empty constructor - it will be recreated if needed when adding facade injection constructor.remove(); } } /** * Add facade service injection to constructor. */ private addFacadeInjection( classDecl: ClassDeclaration, facadeName: string, ): void { const constructor = classDecl.getConstructors()[0]; if (!constructor) { // Create constructor if it doesn't exist const constructorText = ` constructor(private ${this.toCamelCase(facadeName)}: ${facadeName}) {}`; classDecl.addMember(constructorText); return; } // Add parameter to existing constructor const parameters = constructor.getParameters(); if (parameters.length > 0) { // Add parameter after the last existing parameter const lastParam = parameters.at(-1); if (lastParam) { const paramText = `, private ${this.toCamelCase(facadeName)}: ${facadeName}`; lastParam.replaceWithText(lastParam.getText() + paramText); } } else { // For empty constructor, replace the whole thing const constructorText = `constructor(private ${this.toCamelCase(facadeName)}: ${facadeName}) {}`; constructor.replaceWithText(constructorText); } } /** * Replace method calls to use facade service. */ private replaceMethodCalls( classDecl: ClassDeclaration, methodName: string, facadeName: string, ): void { const facadePropertyName = this.toCamelCase(facadeName); // Find all method calls in the class const methodCalls = classDecl.getDescendantsOfKind( SyntaxKind.CallExpression, ); for (const callExpr of methodCalls) { const expression = callExpr.getExpression(); if (expression.getKind() === SyntaxKind.PropertyAccessExpression) { const propAccess = expression as PropertyAccessExpression; if ( propAccess.getExpression().getText() === 'this' && propAccess.getName() === methodName ) { // Replace this.methodName with this.facade.methodName propAccess.replaceWithText( `this.${facadePropertyName}.${methodName}`, ); } } } } /** * Convert PascalCase to camelCase. */ private toCamelCase(str: string): string { return str.charAt(0).toLowerCase() + str.slice(1); } /** * Get facade configuration with defaults. */ private getFacadeConfig( config: Record, ): FacadePatternConfig { return { minMethodsForFacade: (config['minMethodsForFacade'] as number) || 8, minComplexityForFacade: (config['minComplexityForFacade'] as number) || 50, createFacadeServices: config['createFacadeServices'] !== false, updateComponentUsage: config['updateComponentUsage'] !== false, addInjectableDecorator: config['addInjectableDecorator'] !== false, }; } }