/** * @angular-modernizer/plugin-angular - Core and Shared Modules Orchestrator * * Orchestrator that analyzes and organizes Angular applications into proper * Core and Shared module architecture. * * Philosophy: "Thin Rule, Thick Orchestrator" * - The orchestrator contains all business logic * - The rule is a minimal protocol wrapper * - Complex analysis and AST manipulation happens here */ import type { TransformContext } from '@angular-modernizer/plugin-system'; import type { PublicApi } from '@angular-modernizer/api'; import type { ClassDeclaration } from 'ts-morph'; import { type Project, SyntaxKind, Node, type ObjectLiteralExpression, } from 'ts-morph'; /** * Represents a singleton service that should be in CoreModule. */ interface SingletonService { name: string; filePath: string; className: string; providedIn?: string; } /** * Represents a detected shared module. */ interface SharedModule { name: string; filePath: string; className: string; exportsReusableComponents: boolean; exportsCommonModules: boolean; importCount: number; isProperSharedModule: boolean; } /** * Represents a reusable artifact (component/directive/pipe) for SharedModule. */ interface ReusableArtifact { type: 'component' | 'directive' | 'pipe'; name: string; filePath: string; className: string; usageCount: number; } /** * Analysis report of the codebase organization. */ interface OrganizationReport { singletonServices: SingletonService[]; reusableArtifacts: ReusableArtifact[]; sharedModules: SharedModule[]; hasCoreModule: boolean; hasSharedModule: boolean; recommendations: string[]; } /** * Orchestrator for analyzing and organizing Core/Shared module architecture. * * @remarks * This orchestrator handles the complex logic for: * - Scanning the project for singleton services * - Identifying reusable components, directives, and pipes * - Analyzing module organization * - Creating or updating CoreModule * - Creating or updating SharedModule * - Generating organization reports and recommendations * * The orchestrator uses ts-morph for AST manipulation and Project-wide analysis. * * @public */ export class CoreSharedModulesOrchestrator { /** * Singleton service patterns (services that should be in Core). */ private readonly singletonServicePatterns = [ 'AuthService', 'AuthenticationService', 'AuthorizationService', 'LoggerService', 'LoggingService', 'HttpInterceptor', 'ErrorHandler', 'StateService', 'StoreService', 'CacheService', 'ConfigService', 'SettingsService', // Authentication & Security 'JwtService', 'TokenService', 'SessionService', 'PermissionsService', 'RoleService', 'SecurityService', 'GuardService', // Infrastructure & Communication 'ApiService', 'HttpService', 'WebSocketService', 'NotificationService', 'BroadcastService', 'EventService', 'MessageService', // State Management 'AppStateService', 'GlobalStateService', 'UserStateService', 'ThemeService', 'LanguageService', 'LocalizationService', 'TranslationService', // Configuration & Environment 'EnvironmentService', 'FeatureFlagService', 'AppConfigService', 'ConstantsService', 'EnvironmentConfigService', // Monitoring & Analytics 'AnalyticsService', 'TrackingService', 'MetricsService', 'PerformanceService', 'MonitoringService', // Utilities & Helpers 'DateTimeService', 'FormatService', 'ValidationService', 'UtilityService', 'HelperService', ]; /** * Common reusable component patterns (components that should be in Shared). */ private readonly sharedComponentPatterns = [ 'LoadingSpinnerComponent', 'ModalComponent', 'DialogComponent', 'AlertComponent', 'ToastComponent', 'ButtonComponent', 'CardComponent', 'HeaderComponent', 'FooterComponent', 'SidebarComponent', 'NavbarComponent', 'PaginationComponent', 'TabsComponent', 'AccordionComponent', // Form Components 'InputComponent', 'SelectComponent', 'CheckboxComponent', 'RadioComponent', 'TextareaComponent', 'FormFieldComponent', 'DatePickerComponent', 'TimePickerComponent', 'FileUploadComponent', 'SearchComponent', // Data Display Components 'TableComponent', 'ListComponent', 'GridComponent', 'ChartComponent', 'ProgressBarComponent', 'BadgeComponent', 'ChipComponent', 'AvatarComponent', 'IconComponent', 'ImageComponent', // Navigation Components 'BreadcrumbComponent', 'MenuComponent', 'DropdownComponent', 'StepperComponent', 'WizardComponent', // Layout Components 'ContainerComponent', 'RowComponent', 'ColumnComponent', 'GridLayoutComponent', 'FlexLayoutComponent', 'PanelComponent', 'SplitterComponent', // Feedback Components 'SpinnerComponent', 'SkeletonComponent', 'PlaceholderComponent', 'EmptyStateComponent', 'ErrorStateComponent', 'SuccessStateComponent', // Interactive Components 'TooltipComponent', 'PopoverComponent', 'CarouselComponent', 'SliderComponent', 'ToggleComponent', 'SwitchComponent', ]; /** * Run the Core/Shared modules organization on the given context. * * @param context - Transform context with source file and project access * @returns Organization report as string * * @public */ run(context: TransformContext): string { const { project, config } = context; // Get configuration const pluginConfig = (config['@angular-modernizer/plugin-angular'] ?? {}) as Record; const autoCreate = pluginConfig['autoCreateModules'] !== false; // Analyze the entire project const report = this.analyzeProject(project); // Generate report text const reportText = this.generateReport(report); // Optionally create/update modules if (autoCreate && report.recommendations.length > 0) { this.createOrUpdateModules(project, report); } return reportText; } /** * Analyze the entire project for Core/Shared organization issues. * * @param project - The ts-morph Project instance * @returns Organization report * * @private */ private analyzeProject(project: Project): OrganizationReport { const singletonServices: SingletonService[] = []; const reusableArtifacts: ReusableArtifact[] = []; const sharedModules: SharedModule[] = []; let hasCoreModule = false; let hasSharedModule = false; // Get all source files const sourceFiles = project.getSourceFiles(); for (const sourceFile of sourceFiles) { const filePath = sourceFile.getFilePath(); // Skip node_modules and test files if (filePath.includes('node_modules') || filePath.includes('.spec.ts')) { continue; } // Analyze classes const classes = sourceFile.getClasses(); for (const classDecl of classes) { // Check for NgModules if (this.isNgModule(classDecl)) { const moduleInfo = this.analyzeNgModule(classDecl, filePath, project); if (moduleInfo) { sharedModules.push(moduleInfo); // Update flags based on module characteristics if (moduleInfo.isProperSharedModule) { hasSharedModule = true; } if (filePath.includes('core.module')) { hasCoreModule = true; } } } // Check for services if (this.isService(classDecl)) { const service = this.analyzeService(classDecl, filePath); if (service && this.isSingletonService(service)) { singletonServices.push(service); } } // Check for components if (this.isComponent(classDecl)) { const artifact = this.analyzeComponent(classDecl, filePath, project); if (artifact && artifact.usageCount > 1) { reusableArtifacts.push(artifact); } } // Check for directives if (this.isDirective(classDecl)) { const artifact = this.analyzeDirective(classDecl, filePath, project); if (artifact && artifact.usageCount > 1) { reusableArtifacts.push(artifact); } } // Check for pipes if (this.isPipe(classDecl)) { const artifact = this.analyzePipe(classDecl, filePath, project); if (artifact && artifact.usageCount > 1) { reusableArtifacts.push(artifact); } } } } // Generate recommendations const recommendations = this.generateRecommendations( singletonServices, reusableArtifacts, sharedModules, hasCoreModule, hasSharedModule, ); return { singletonServices, reusableArtifacts, sharedModules, hasCoreModule, hasSharedModule, recommendations, }; } /** * Check if a class is a service. * * @param classDecl - The class declaration * @returns true if the class is a service * * @private */ private isService(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((d) => d.getName() === 'Injectable'); } /** * Check if a class is a component. * * @param classDecl - The class declaration * @returns true if the class is a component * * @private */ private isComponent(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((d) => d.getName() === 'Component'); } /** * Check if a class is a directive. * * @param classDecl - The class declaration * @returns true if the class is a directive * * @private */ private isDirective(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((d) => d.getName() === 'Directive'); } /** * Check if a class is a pipe. * * @param classDecl - The class declaration * @returns true if the class is a pipe * * @private */ private isPipe(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((d) => d.getName() === 'Pipe'); } /** * Check if a class is an NgModule. * * @param classDecl - The class declaration * @returns true if the class is an NgModule * * @private */ private isNgModule(classDecl: ClassDeclaration): boolean { const decorators = classDecl.getDecorators(); return decorators.some((d) => d.getName() === 'NgModule'); } /** * Analyze an NgModule to determine if it's a shared module. * * @param classDecl - The NgModule class declaration * @param filePath - Path to the file * @param project - The project instance * @returns Shared module information * * @private */ private analyzeNgModule( classDecl: ClassDeclaration, filePath: string, project: Project, ): SharedModule | null { const className = classDecl.getName(); if (!className) { return null; } // Get NgModule decorator metadata const decorator = classDecl.getDecorator('NgModule'); if (!decorator) { return null; } const args = decorator.getArguments(); if (args.length === 0) { return null; } const metadata = args[0]; if ( !metadata || metadata.getKind() !== SyntaxKind.ObjectLiteralExpression ) { return null; } const objLiteral = metadata as ObjectLiteralExpression; // Analyze exports const exportsProperty = objLiteral.getProperty('exports'); let exportsReusableComponents = false; let exportsCommonModules = false; if (exportsProperty && Node.isPropertyAssignment(exportsProperty)) { const exportsArray = exportsProperty.getInitializerIfKind( SyntaxKind.ArrayLiteralExpression, ); if (exportsArray) { const exportsText = exportsArray.getText(); // Check for reusable components/directives/pipes exportsReusableComponents = /\b(Component|Directive|Pipe)\b/.test( exportsText, ); // Check for common Angular modules exportsCommonModules = /\b(CommonModule|FormsModule|ReactiveFormsModule)\b/.test( exportsText, ); } } // Count how many times this module is imported const importCount = this.countModuleUsages(className, project); // Determine if this is a proper shared module // A shared module typically: // - Exports reusable UI components/directives/pipes // - Exports CommonModule/FormsModule // - Is imported by multiple feature modules (not just AppModule) const isProperSharedModule = exportsReusableComponents || exportsCommonModules || importCount > 1; return { name: className, filePath, className, exportsReusableComponents, exportsCommonModules, importCount, isProperSharedModule, }; } /** * Analyze a service to extract information. * * @param classDecl - The service class declaration * @param filePath - Path to the file * @returns Service information * * @private */ private analyzeService( classDecl: ClassDeclaration, filePath: string, ): SingletonService | null { const className = classDecl.getName(); if (!className) { return null; } // Get providedIn metadata const decorator = classDecl.getDecorator('Injectable'); let providedIn: string | undefined; if (decorator) { const args = decorator.getArguments(); if (args.length > 0 && args[0]) { const arg = args[0]; const text = arg.getText(); const match = /providedIn:\s*['"]([^'"]+)['"]/.exec(text); if (match) { providedIn = match[1]; } } } return { name: className, filePath, className, providedIn, }; } /** * Check if a service should be a singleton (in CoreModule). * * @param service - The service to check * @returns true if the service should be a singleton * * @private */ private isSingletonService(service: SingletonService): boolean { // Check if it matches singleton patterns const matchesPattern = this.singletonServicePatterns.some((pattern) => service.name.includes(pattern), ); // Check if it's outside the core folder const isOutsideCore = !service.filePath.includes('/core/'); // Only flag services that match the pattern AND are outside core return matchesPattern && isOutsideCore; } /** * Analyze a component to extract information and usage count. * * @param classDecl - The component class declaration * @param filePath - Path to the file * @param project - The project instance * @returns Component information * * @private */ private analyzeComponent( classDecl: ClassDeclaration, filePath: string, project: Project, ): ReusableArtifact | null { const className = classDecl.getName(); if (!className) { return null; } // Check if it matches common reusable component patterns const matchesPattern = this.sharedComponentPatterns.some((pattern) => className.includes(pattern.replace('Component', '')), ); // Check if it's outside the shared folder const isOutsideShared = !filePath.includes('/shared/'); if (!matchesPattern && !isOutsideShared) { return null; } // Count usage (simple heuristic: count imports of this component) const usageCount = this.countUsages(className, project); return { type: 'component', name: className, filePath, className, usageCount, }; } /** * Analyze a directive to extract information. * * @param classDecl - The directive class declaration * @param filePath - Path to the file * @param project - The project instance * @returns Directive information * * @private */ private analyzeDirective( classDecl: ClassDeclaration, filePath: string, project: Project, ): ReusableArtifact | null { const className = classDecl.getName(); if (!className) { return null; } const usageCount = this.countUsages(className, project); return { type: 'directive', name: className, filePath, className, usageCount, }; } /** * Analyze a pipe to extract information. * * @param classDecl - The pipe class declaration * @param filePath - Path to the file * @param project - The project instance * @returns Pipe information * * @private */ private analyzePipe( classDecl: ClassDeclaration, filePath: string, project: Project, ): ReusableArtifact | null { const className = classDecl.getName(); if (!className) { return null; } const usageCount = this.countUsages(className, project); return { type: 'pipe', name: className, filePath, className, usageCount, }; } /** * Count how many times a class is used across the project. * * @param className - The class name to search for * @param project - The project instance * @returns Usage count * * @private */ private countUsages(className: string, project: Project): number { let count = 0; const sourceFiles = project.getSourceFiles(); for (const sourceFile of sourceFiles) { const filePath = sourceFile.getFilePath(); // Skip node_modules and test files if (filePath.includes('node_modules') || filePath.includes('.spec.ts')) { continue; } // Count imports of this class const imports = sourceFile.getImportDeclarations(); for (const importDecl of imports) { const namedImports = importDecl.getNamedImports(); for (const namedImport of namedImports) { if (namedImport.getName() === className) { count++; } } } } return count; } /** * Count how many times a module is imported across the project. * * @param moduleClassName - The module class name to search for * @param project - The project instance * @returns Import count * * @private */ private countModuleUsages(moduleClassName: string, project: Project): number { let count = 0; const sourceFiles = project.getSourceFiles(); for (const sourceFile of sourceFiles) { const filePath = sourceFile.getFilePath(); // Skip node_modules and test files if (filePath.includes('node_modules') || filePath.includes('.spec.ts')) { continue; } // Look for NgModule imports arrays that contain this module const classes = sourceFile.getClasses(); for (const classDecl of classes) { if (this.isNgModule(classDecl)) { const decorator = classDecl.getDecorator('NgModule'); if (decorator) { const args = decorator.getArguments(); if (args.length > 0) { const metadata = args[0]; if ( metadata && metadata.getKind() === SyntaxKind.ObjectLiteralExpression ) { const objLiteral = metadata as ObjectLiteralExpression; const importsProperty = objLiteral.getProperty('imports'); if ( importsProperty && Node.isPropertyAssignment(importsProperty) ) { const importsArray = importsProperty.getInitializerIfKind( SyntaxKind.ArrayLiteralExpression, ); if (importsArray) { const importsText = importsArray.getText(); if (importsText.includes(moduleClassName)) { count++; } } } } } } } } } return count; } /** * Generate recommendations based on analysis. * * @param singletonServices - Singleton services found * @param reusableArtifacts - Reusable artifacts found * @param sharedModules - Detected shared modules * @param hasCoreModule - Whether CoreModule exists * @param hasSharedModule - Whether SharedModule exists * @returns Array of recommendations * * @private */ private generateRecommendations( singletonServices: SingletonService[], reusableArtifacts: ReusableArtifact[], sharedModules: SharedModule[], hasCoreModule: boolean, hasSharedModule: boolean, ): string[] { const recommendations: string[] = []; // Shared Module analysis const properSharedModules = sharedModules.filter( (m) => m.isProperSharedModule, ); const improperSharedModules = sharedModules.filter( (m) => !m.isProperSharedModule && m.importCount > 0, ); if (properSharedModules.length > 0) { recommendations.push( `FOUND ${properSharedModules.length} proper shared module(s):`, ); for (const mod of properSharedModules.slice(0, 3)) { recommendations.push(` - ${mod.name} (${mod.importCount} imports)`); } if (properSharedModules.length > 3) { recommendations.push( ` ... and ${properSharedModules.length - 3} more`, ); } } if (improperSharedModules.length > 0) { recommendations.push( `FOUND ${improperSharedModules.length} modules that may be shared but need review:`, ); for (const mod of improperSharedModules.slice(0, 3)) { recommendations.push( ` - ${mod.name} (${mod.importCount} imports, exports: ${mod.exportsReusableComponents ? 'components' : 'none'})`, ); } } // Core Module recommendations if (singletonServices.length > 0 && !hasCoreModule) { recommendations.push('CREATE CoreModule in src/app/core/core.module.ts'); recommendations.push( 'ADD import guard to CoreModule to prevent multiple imports', ); } if (singletonServices.length > 0) { recommendations.push( `MOVE ${singletonServices.length} singleton service(s) to src/app/core/services/`, ); for (const service of singletonServices.slice(0, 5)) { recommendations.push(` - ${service.name} from ${service.filePath}`); } if (singletonServices.length > 5) { recommendations.push(` ... and ${singletonServices.length - 5} more`); } } // Shared Module recommendations if ( reusableArtifacts.length > 0 && !hasSharedModule && properSharedModules.length === 0 ) { recommendations.push( 'CREATE SharedModule in src/app/shared/shared.module.ts', ); } if (reusableArtifacts.length > 0) { const components = reusableArtifacts.filter( (a) => a.type === 'component', ); const directives = reusableArtifacts.filter( (a) => a.type === 'directive', ); const pipes = reusableArtifacts.filter((a) => a.type === 'pipe'); if (components.length > 0) { recommendations.push( `MOVE ${components.length} reusable component(s) to src/app/shared/components/`, ); } if (directives.length > 0) { recommendations.push( `MOVE ${directives.length} reusable directive(s) to src/app/shared/directives/`, ); } if (pipes.length > 0) { recommendations.push( `MOVE ${pipes.length} reusable pipe(s) to src/app/shared/pipes/`, ); } } return recommendations; } /** * Generate a text report from the analysis. * * @param report - The organization report * @returns Formatted report text * * @private */ private generateReport(report: OrganizationReport): string { const lines: string[] = []; lines.push('=== Core/Shared Modules Analysis ===\n'); // Current state lines.push('Current State:'); lines.push(` CoreModule exists: ${report.hasCoreModule ? 'Yes' : 'No'}`); lines.push( ` SharedModule exists: ${report.hasSharedModule ? 'Yes' : 'No'}`, ); lines.push(` Total NgModules found: ${report.sharedModules.length}`); lines.push(''); // Shared modules analysis if (report.sharedModules.length > 0) { const properShared = report.sharedModules.filter( (m) => m.isProperSharedModule, ); const potentialShared = report.sharedModules.filter( (m) => !m.isProperSharedModule && m.importCount > 1, ); lines.push('Shared Modules Analysis:'); lines.push(` Proper shared modules: ${properShared.length}`); lines.push(` Potential shared modules: ${potentialShared.length}`); lines.push(''); } // Findings lines.push('Findings:'); lines.push( ` Singleton services to organize: ${report.singletonServices.length}`, ); lines.push( ` Reusable artifacts to organize: ${report.reusableArtifacts.length}`, ); lines.push(''); // Recommendations if (report.recommendations.length > 0) { lines.push('Recommendations:'); for (const rec of report.recommendations) { lines.push(` ${rec}`); } } else { lines.push('No recommendations - Core/Shared architecture looks good!'); } return lines.join('\n'); } /** * Create or update CoreModule and SharedModule. * * @param project - The project instance * @param report - The organization report * * @private */ //TODO: Implement module creation/updating logic and standalone implementation private createOrUpdateModules( _project: Project, _report: OrganizationReport, ): void { // Note: This is a placeholder for future implementation // Creating modules requires determining the correct file paths and // ensuring we don't conflict with existing files // For now, the rule primarily generates reports and recommendations // Future implementation would: // 1. Create src/app/core/core.module.ts if needed // 2. Create src/app/shared/shared.module.ts if needed // 3. Add import guards // 4. Update module declarations and exports } }