/** * @angular-modernizer/plugin-angular - Dependency Injection Migration Orchestrator * * The central "brain" of the dependency injection migration transformation plugin. * Orchestrates the modernization of Angular dependency injection patterns. * * 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, PropertyDeclaration, CallExpression, ArrayLiteralExpression, ObjectLiteralElementLike, } from 'ts-morph'; import { SyntaxKind } from 'ts-morph'; /** * Dependency Injection Migration Orchestrator * * @remarks * The central orchestrator for modernizing Angular dependency injection patterns. * This class demonstrates the "API-Driven Plugin" pattern where all capabilities * are received via context objects rather than constructor injection. * * ## 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 * * ## Transformation Workflow * * The orchestrator performs the following steps: * 1. Detect deprecated DI patterns in the source file * 2. Transform string-based injection tokens to proper InjectionToken usage * 3. Update deprecated injector creation patterns * 4. Modernize provider configurations where applicable * 5. Ensure proper imports are in place * * ## Example Usage * * ```typescript * const orchestrator = new DependencyInjectionMigrationOrchestrator(); * const context = ContextFactory.createTransformContext({ * sourceFile, * project, * api, * config: {} * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * @see {@link PublicApi} for available API tools * * @public */ export class DependencyInjectionMigrationOrchestrator { /** * Creates a new DependencyInjectionMigrationOrchestrator 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 dependency injection migration transformation 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, * scans the file for deprecated DI patterns, and modernizes them. * * The method operates on one source file at a time and is safe to call * multiple times. It will: * - Find string-based @Inject() decorators and convert them to inject() calls * - Update deprecated ReflectiveInjector usage * - Modernize provider configurations * - Ensure proper imports are in place * * ## Transformation Behavior * * **String-based @Inject() Decorators**: * ```typescript * // Before * @Inject('API_URL') private apiUrl: string; * * // After * private apiUrl = inject(API_URL_TOKEN); * ``` * * **ReflectiveInjector Usage**: * ```typescript * // Before * const injector = ReflectiveInjector.resolveAndCreate([SomeService]); * * // After * const injector = Injector.create({ providers: [SomeService] }); * ``` * * @param context - The transformation context containing: * - `sourceFile`: The TypeScript source file to transform * - `project`: The ts-morph Project for cross-file analysis * - `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: {} * }); * orchestrator.run(context); * ``` * * @see {@link TransformContext} for context structure * * @public */ run(context: TransformContext): void { const { sourceFile, api } = context; // Transform @Inject() decorators with string tokens this.transformInjectDecorators(sourceFile, api); // Transform ReflectiveInjector usage this.transformReflectiveInjectorUsage(sourceFile, api); // Transform provider configurations this.transformProviderConfigurations(sourceFile, api); } /** * Transform @Inject() decorators with string tokens to inject() calls. * * @param sourceFile - The source file to transform * @param api - The PublicApi with transformation tools */ private transformInjectDecorators( sourceFile: SourceFile, api: PublicApi, ): void { // Find all properties with @Inject() decorators const properties = sourceFile.getDescendantsOfKind( SyntaxKind.PropertyDeclaration, ); for (const property of properties) { const injectDecorator = property.getDecorator('Inject'); if (!injectDecorator) { continue; } // Check if it's using a string token const decoratorArgs = injectDecorator.getArguments(); if (decoratorArgs.length !== 1) { continue; } const firstArg = decoratorArgs[0]; if (!firstArg?.isKind(SyntaxKind.StringLiteral)) { continue; } const tokenValue = firstArg.getLiteralValue(); if (typeof tokenValue !== 'string') { continue; } // Convert to inject() call this.convertInjectDecoratorToInjectCall(property, tokenValue, api); // Remove the @Inject decorator injectDecorator.remove(); } } /** * Convert an @Inject() decorator to an inject() call. * * @param property - The property declaration to transform * @param tokenValue - The string token value * @param api - The PublicApi with transformation tools */ private convertInjectDecoratorToInjectCall( property: PropertyDeclaration, tokenValue: string, api: PublicApi, ): void { // Create the inject() call const injectCall = `inject(${this.getTokenName(tokenValue)})`; // Replace the property with an initializer property.removeInitializer(); property.setInitializer(injectCall); // Ensure inject import is present this.ensureInjectImport(property.getSourceFile(), api); } /** * Get the token name for a string token value. * * @param tokenValue - The string token value * @returns The token variable name */ private getTokenName(tokenValue: string): string { // Convert string tokens to UPPER_SNAKE_CASE token names return ( tokenValue .replaceAll(/[^a-zA-Z0-9]/g, '_') .replaceAll(/_{2,}/g, '_') .toUpperCase() .replaceAll(/^_+|_+$/g, '') + '_TOKEN' ); } /** * Transform ReflectiveInjector usage to modern Injector.create(). * * @param sourceFile - The source file to transform * @param api - The PublicApi with transformation tools */ private transformReflectiveInjectorUsage( sourceFile: SourceFile, api: PublicApi, ): void { // Find ReflectiveInjector calls const reflectiveInjectorCalls = sourceFile.getDescendantsOfKind( SyntaxKind.CallExpression, ); for (const call of reflectiveInjectorCalls) { const expression = call.getExpression(); if (!expression.isKind(SyntaxKind.PropertyAccessExpression)) { continue; } const propertyAccess = expression.asKind( SyntaxKind.PropertyAccessExpression, ); if (!propertyAccess) { continue; } const objectName = propertyAccess.getExpression().getText(); const methodName = propertyAccess.getName(); if ( objectName === 'ReflectiveInjector' && methodName === 'resolveAndCreate' ) { this.convertReflectiveInjectorCall(call, api); } } } /** * Convert ReflectiveInjector.resolveAndCreate() to Injector.create(). * * @param call - The call expression to transform * @param api - The PublicApi with transformation tools */ private convertReflectiveInjectorCall( call: CallExpression, api: PublicApi, ): void { const args = call.getArguments(); if (args.length !== 1) { return; } const providersArg = args[0]; if (!providersArg) { return; } // Replace ReflectiveInjector.resolveAndCreate(providers) with Injector.create({ providers }) call.replaceWithText( `Injector.create({ providers: ${providersArg.getText()} })`, ); // Ensure Injector import is present this.ensureInjectorImport(call.getSourceFile(), api); } /** * Transform provider configurations to modern syntax. * * @param sourceFile - The source file to transform * @param api - The PublicApi with transformation tools */ private transformProviderConfigurations( sourceFile: SourceFile, api: PublicApi, ): void { // Find provider array literals and object literals const arrayLiterals = sourceFile.getDescendantsOfKind( SyntaxKind.ArrayLiteralExpression, ); for (const arrayLiteral of arrayLiterals) { // Check if this looks like a providers array if (this.isProvidersArray(arrayLiteral)) { this.modernizeProvidersArray(arrayLiteral, api); } } } /** * Check if an array literal looks like a providers array. * * @param arrayLiteral - The array literal to check * @returns True if it looks like a providers array */ private isProvidersArray(arrayLiteral: ArrayLiteralExpression): boolean { try { const elements = arrayLiteral.getElements(); // Look for provider-like objects or class references for (const element of elements) { if (element.isKind(SyntaxKind.ObjectLiteralExpression)) { const properties = element.getProperties(); // Check for provider properties like 'provide', 'useClass', 'useValue', etc. const hasProviderProps = properties.some( (prop: ObjectLiteralElementLike) => { if (prop.isKind?.(SyntaxKind.PropertyAssignment)) { const name = prop.getName(); return [ 'provide', 'useClass', 'useValue', 'useFactory', 'useExisting', ].includes(name); } return false; }, ); if (hasProviderProps) { return true; } } } } catch { // If anything fails, assume it's not a providers array } return false; } /** * Modernize a providers array to use modern syntax. * * @param arrayLiteral - The providers array to modernize * @param api - The PublicApi with transformation tools */ private modernizeProvidersArray( arrayLiteral: ArrayLiteralExpression, api: PublicApi, ): void { //TODO: For now, just ensure proper imports. More complex transformations can be added later. const sourceFile = arrayLiteral.getSourceFile(); // Ensure modern provider imports are available this.ensureModernProviderImports(sourceFile, api); } /** * Ensure the inject function is imported from @angular/core. * * @param sourceFile - The source file to add the import to * @param api - The PublicApi with import management tools */ private ensureInjectImport(sourceFile: SourceFile, api: PublicApi): void { const importManager = api.transformation.importManager; // Check if inject is already imported const existingImports = sourceFile.getImportDeclarations(); for (const importDecl of existingImports) { if (importDecl.getModuleSpecifierValue() === '@angular/core') { const namedImports = importDecl.getNamedImports(); const hasInject = namedImports.some((ni) => ni.getName() === 'inject'); if (hasInject) { return; // Already imported } // Add inject to existing @angular/core import importDecl.addNamedImport('inject'); return; } } // Add new import for inject importManager.addNamedImport(sourceFile, '@angular/core', 'inject'); } /** * Ensure the Injector is imported from @angular/core. * * @param sourceFile - The source file to add the import to * @param api - The PublicApi with import management tools */ private ensureInjectorImport(sourceFile: SourceFile, api: PublicApi): void { const importManager = api.transformation.importManager; // Check if Injector is already imported const existingImports = sourceFile.getImportDeclarations(); for (const importDecl of existingImports) { if (importDecl.getModuleSpecifierValue() === '@angular/core') { const namedImports = importDecl.getNamedImports(); const hasInjector = namedImports.some( (ni) => ni.getName() === 'Injector', ); if (hasInjector) { return; // Already imported } // Add Injector to existing @angular/core import importDecl.addNamedImport('Injector'); return; } } // Add new import for Injector importManager.addNamedImport(sourceFile, '@angular/core', 'Injector'); } /** * Ensure modern provider-related imports are available. * * @param sourceFile - The source file to add imports to * @param api - The PublicApi with import management tools */ private ensureModernProviderImports( sourceFile: SourceFile, api: PublicApi, ): void { // TODO: For now, this method is a placeholder for future enhancements. // Currently, the basic provider array detection doesn't require additional imports // beyond what's already handled by ensureInjectImport and ensureInjectorImport. // Future versions may add InjectionToken imports when token-based providers are detected. // Keep parameters for future implementation void sourceFile; void api; } }