/** * @angular-modernizer/plugin-angular - Container and Presentational Components Rule * * Separates Angular components with mixed responsibilities into cleaner * presentational components by removing service dependencies and using * @Input/@Output decorators for data flow. * * Philosophy: "Thin Rule, Thick Orchestrator" * - This rule is a minimal protocol wrapper * - ContainerPresentationalOrchestrator contains all business logic */ import type { TransformRule, TransformContext, TransformResult, } from '@angular-modernizer/plugin-system'; import type { PublicApi } from '@angular-modernizer/api'; import { ContainerPresentationalOrchestrator } from '../orchestrators/container-presentational-orchestrator.js'; /** * Rule for transforming components with mixed responsibilities into presentational components. * * @remarks * This rule identifies Angular components that have service dependencies and business logic * mixed with presentation logic. It transforms them into presentational components by: * - Removing service dependencies from the constructor * - Adding @Input properties for data * - Adding @Output EventEmitters for user interactions * - Converting service-calling methods to event-emitting methods * - Updating imports (add Input/Output/EventEmitter, remove unused services) * * The goal is to create components that are purely responsible for presentation, * receiving all data via @Input and communicating changes only through @Output events. * * ## Example * * **Before:** * ```typescript * @Component({ * selector: 'app-user-list', * template: '...' * }) * export class UserListComponent { * users: User[] = []; * * constructor(private userService: UserService) {} * * ngOnInit() { * this.userService.getUsers().subscribe(users => this.users = users); * } * * deleteUser(id: string) { * this.userService.deleteUser(id).subscribe(); * } * } * ``` * * **After:** * ```typescript * @Component({ * selector: 'app-user-list', * template: '...' * }) * export class UserListComponent { * @Input() users: User[] = []; * @Output() userDelete = new EventEmitter(); * * deleteUser(id: string) { * this.userDelete.emit(id); * } * } * ``` * * @public */ export class ContainerPresentationalRule implements TransformRule { /** * Unique rule identifier. */ public readonly id = 'angular:container-presentational'; /** * Human-readable rule name. */ public readonly name = 'Container and Presentational Components'; /** * Detailed rule description. */ public readonly description = 'Separates components with mixed responsibilities by converting service-dependent components into presentational components with @Input/@Output decorators'; /** * Rule category. */ public readonly category = 'modernization'; /** * Rule tags for filtering and organization. */ public readonly tags = [ 'angular', 'component', 'architecture', 'separation-of-concerns', 'presentational', 'container', 'best-practices', ]; /** * Orchestrator instance (stateless). */ private readonly orchestrator = new ContainerPresentationalOrchestrator(); /** * Transform a component with mixed responsibilities into a presentational component. * * @param context - Transform context with source file and configuration * * @returns Transform result indicating success and modifications * * @public */ async transform( context: TransformContext, ): Promise { const { sourceFile, filePath } = context; // Check configuration to determine if this transformation should run const pluginConfig = context.config[ '@angular-modernizer/plugin-angular' ] as Record | undefined; if (pluginConfig?.['transformationType'] !== 'container-presentational') { return { ruleId: this.id, modified: false, message: 'Transformation type not applicable', filePath, }; } // Capture initial state to detect modifications const initialText = sourceFile.getFullText(); // Run the orchestrator this.orchestrator.run(context); // Detect if any modifications were made const modified = sourceFile.getFullText() !== initialText; return { ruleId: this.id, modified, message: modified ? 'Converted component to presentational with @Input/@Output decorators' : 'No transformations needed', filePath, }; } }