@angular-modernizer/api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/dist/analysis/complexity-metrics-calculator.d.ts +99 -0
  4. package/dist/analysis/complexity-metrics-calculator.d.ts.map +1 -0
  5. package/dist/analysis/complexity-metrics-calculator.js +195 -0
  6. package/dist/analysis/complexity-metrics-calculator.js.map +1 -0
  7. package/dist/analysis/dependency-analyzer.d.ts +227 -0
  8. package/dist/analysis/dependency-analyzer.d.ts.map +1 -0
  9. package/dist/analysis/dependency-analyzer.js +451 -0
  10. package/dist/analysis/dependency-analyzer.js.map +1 -0
  11. package/dist/analysis/external-template-loader.d.ts +44 -0
  12. package/dist/analysis/external-template-loader.d.ts.map +1 -0
  13. package/dist/analysis/external-template-loader.js +69 -0
  14. package/dist/analysis/external-template-loader.js.map +1 -0
  15. package/dist/analysis/selector-mapper.d.ts +77 -0
  16. package/dist/analysis/selector-mapper.d.ts.map +1 -0
  17. package/dist/analysis/selector-mapper.js +146 -0
  18. package/dist/analysis/selector-mapper.js.map +1 -0
  19. package/dist/analysis/service-detector.d.ts +164 -0
  20. package/dist/analysis/service-detector.d.ts.map +1 -0
  21. package/dist/analysis/service-detector.js +231 -0
  22. package/dist/analysis/service-detector.js.map +1 -0
  23. package/dist/analysis/service-pattern-recognizer.d.ts +247 -0
  24. package/dist/analysis/service-pattern-recognizer.d.ts.map +1 -0
  25. package/dist/analysis/service-pattern-recognizer.js +444 -0
  26. package/dist/analysis/service-pattern-recognizer.js.map +1 -0
  27. package/dist/analysis/symbol-locator.d.ts +81 -0
  28. package/dist/analysis/symbol-locator.d.ts.map +1 -0
  29. package/dist/analysis/symbol-locator.js +128 -0
  30. package/dist/analysis/symbol-locator.js.map +1 -0
  31. package/dist/analysis/template-analyzer-usage.d.ts +144 -0
  32. package/dist/analysis/template-analyzer-usage.d.ts.map +1 -0
  33. package/dist/analysis/template-analyzer-usage.js +474 -0
  34. package/dist/analysis/template-analyzer-usage.js.map +1 -0
  35. package/dist/analysis/template-analyzer.d.ts +80 -0
  36. package/dist/analysis/template-analyzer.d.ts.map +1 -0
  37. package/dist/analysis/template-analyzer.js +146 -0
  38. package/dist/analysis/template-analyzer.js.map +1 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +22 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/investigation/angular-symbol-finder.d.ts +76 -0
  44. package/dist/investigation/angular-symbol-finder.d.ts.map +1 -0
  45. package/dist/investigation/angular-symbol-finder.js +199 -0
  46. package/dist/investigation/angular-symbol-finder.js.map +1 -0
  47. package/dist/investigation/call-graph-builder.d.ts +85 -0
  48. package/dist/investigation/call-graph-builder.d.ts.map +1 -0
  49. package/dist/investigation/call-graph-builder.js +196 -0
  50. package/dist/investigation/call-graph-builder.js.map +1 -0
  51. package/dist/investigation/codebase-searcher.d.ts +99 -0
  52. package/dist/investigation/codebase-searcher.d.ts.map +1 -0
  53. package/dist/investigation/codebase-searcher.js +201 -0
  54. package/dist/investigation/codebase-searcher.js.map +1 -0
  55. package/dist/investigation/stack-trace-parser.d.ts +70 -0
  56. package/dist/investigation/stack-trace-parser.d.ts.map +1 -0
  57. package/dist/investigation/stack-trace-parser.js +117 -0
  58. package/dist/investigation/stack-trace-parser.js.map +1 -0
  59. package/dist/investigation/type-resolver.d.ts +59 -0
  60. package/dist/investigation/type-resolver.d.ts.map +1 -0
  61. package/dist/investigation/type-resolver.js +111 -0
  62. package/dist/investigation/type-resolver.js.map +1 -0
  63. package/dist/investigation/usage-finder.d.ts +69 -0
  64. package/dist/investigation/usage-finder.d.ts.map +1 -0
  65. package/dist/investigation/usage-finder.js +132 -0
  66. package/dist/investigation/usage-finder.js.map +1 -0
  67. package/dist/metrics/complexity-metrics.d.ts +196 -0
  68. package/dist/metrics/complexity-metrics.d.ts.map +1 -0
  69. package/dist/metrics/complexity-metrics.js +445 -0
  70. package/dist/metrics/complexity-metrics.js.map +1 -0
  71. package/dist/public-api.d.ts +77 -0
  72. package/dist/public-api.d.ts.map +1 -0
  73. package/dist/public-api.js +50 -0
  74. package/dist/public-api.js.map +1 -0
  75. package/dist/transformation/import-manager.d.ts +276 -0
  76. package/dist/transformation/import-manager.d.ts.map +1 -0
  77. package/dist/transformation/import-manager.js +488 -0
  78. package/dist/transformation/import-manager.js.map +1 -0
  79. package/dist/transformation/ng-morph-adapter.d.ts +111 -0
  80. package/dist/transformation/ng-morph-adapter.d.ts.map +1 -0
  81. package/dist/transformation/ng-morph-adapter.js +179 -0
  82. package/dist/transformation/ng-morph-adapter.js.map +1 -0
  83. package/dist/transformation/ngmodule-manager.d.ts +145 -0
  84. package/dist/transformation/ngmodule-manager.d.ts.map +1 -0
  85. package/dist/transformation/ngmodule-manager.js +356 -0
  86. package/dist/transformation/ngmodule-manager.js.map +1 -0
  87. package/package.json +62 -0
  88. package/src/analysis/complexity-metrics-calculator.ts +253 -0
  89. package/src/analysis/dependency-analyzer.ts +720 -0
  90. package/src/analysis/external-template-loader.ts +79 -0
  91. package/src/analysis/selector-mapper.ts +186 -0
  92. package/src/analysis/service-detector.ts +280 -0
  93. package/src/analysis/service-pattern-recognizer.ts +545 -0
  94. package/src/analysis/symbol-locator.ts +170 -0
  95. package/src/analysis/template-analyzer-usage.ts +614 -0
  96. package/src/analysis/template-analyzer.ts +208 -0
  97. package/src/index.ts +51 -0
  98. package/src/investigation/angular-symbol-finder.ts +263 -0
  99. package/src/investigation/call-graph-builder.ts +298 -0
  100. package/src/investigation/codebase-searcher.ts +286 -0
  101. package/src/investigation/stack-trace-parser.ts +166 -0
  102. package/src/investigation/type-resolver.ts +144 -0
  103. package/src/investigation/usage-finder.ts +198 -0
  104. package/src/metrics/complexity-metrics.ts +617 -0
  105. package/src/public-api.ts +119 -0
  106. package/src/transformation/import-manager.ts +626 -0
  107. package/src/transformation/ng-morph-adapter.ts +252 -0
  108. package/src/transformation/ngmodule-manager.ts +450 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * NgMorphAdapter - Wrapper around ng-morph for Angular-specific AST operations
3
+ *
4
+ * This adapter provides a simplified interface to ng-morph functionality while
5
+ * maintaining compatibility with our existing ts-morph-based infrastructure.
6
+ */
7
+
8
+ import {
9
+ getNgComponents,
10
+ getNgModules,
11
+ findNgModule,
12
+ addImportToNgModule,
13
+ addDeclarationToNgModule,
14
+ addImportToComponent,
15
+ addProviderToComponent,
16
+ type ClassDeclaration,
17
+ } from 'ng-morph';
18
+ import type { Project } from 'ts-morph';
19
+
20
+ export interface NgMorphAdapterOptions {
21
+ /**
22
+ * The ts-morph Project instance to wrap
23
+ */
24
+ project: Project;
25
+
26
+ /**
27
+ * Root path for the Angular project
28
+ */
29
+ rootPath?: string;
30
+
31
+ /**
32
+ * File pattern to match (default: '**\/*.ts')
33
+ */
34
+ pattern?: string;
35
+ }
36
+
37
+ /**
38
+ * Adapter for ng-morph that integrates with our ts-morph Project
39
+ */
40
+ export class NgMorphAdapter {
41
+ private readonly project: Project;
42
+ private readonly pattern: string;
43
+
44
+ constructor(options: NgMorphAdapterOptions) {
45
+ this.project = options.project;
46
+ this.pattern = options.pattern ?? '**/*.ts';
47
+ }
48
+
49
+ /**
50
+ * Get all Angular components matching a pattern
51
+ */
52
+ getComponents(pattern?: string): ClassDeclaration[] {
53
+ return getNgComponents(pattern ?? this.pattern);
54
+ }
55
+
56
+ /**
57
+ * Get a specific component by file path
58
+ */
59
+ getComponent(filePath: string): ClassDeclaration | undefined {
60
+ const sourceFile = this.project.getSourceFile(filePath);
61
+ if (!sourceFile) {
62
+ return undefined;
63
+ }
64
+
65
+ const components = getNgComponents(filePath);
66
+ return components[0]; // Return first component in file
67
+ }
68
+
69
+ /**
70
+ * Get all Angular modules matching a pattern
71
+ */
72
+ getModules(pattern?: string): ClassDeclaration[] {
73
+ return getNgModules(pattern ?? this.pattern);
74
+ }
75
+
76
+ /**
77
+ * Get a specific module by file path
78
+ */
79
+ getModule(filePath: string): ClassDeclaration | undefined {
80
+ const sourceFile = this.project.getSourceFile(filePath);
81
+ if (!sourceFile) {
82
+ return undefined;
83
+ }
84
+
85
+ const modules = getNgModules(filePath);
86
+ return modules[0]; // Return first module in file
87
+ }
88
+
89
+ /**
90
+ * Find the NgModule that declares a component
91
+ */
92
+ findModuleForComponent(component: ClassDeclaration): ClassDeclaration | null {
93
+ return findNgModule(component, this.pattern);
94
+ }
95
+
96
+ /**
97
+ * Add an import to an NgModule
98
+ * @param moduleClass - The NgModule class declaration
99
+ * @param moduleName - Name of the module to import (e.g., 'CommonModule')
100
+ * @param options - Optional configuration
101
+ */
102
+ addImportToModule(
103
+ moduleClass: ClassDeclaration,
104
+ moduleName: string,
105
+ options?: { unique?: boolean },
106
+ ): void {
107
+ addImportToNgModule(moduleClass, moduleName, options);
108
+ }
109
+
110
+ /**
111
+ * Add a declaration to an NgModule
112
+ * @param moduleClass - The NgModule class declaration
113
+ * @param declaration - Name of the component/directive/pipe to declare
114
+ * @param options - Optional configuration
115
+ */
116
+ addDeclarationToModule(
117
+ moduleClass: ClassDeclaration,
118
+ declaration: string,
119
+ options?: { unique?: boolean },
120
+ ): void {
121
+ addDeclarationToNgModule(moduleClass, declaration, options);
122
+ }
123
+
124
+ /**
125
+ * Remove a component from an NgModule's declarations
126
+ * This is a custom implementation since ng-morph doesn't provide this directly
127
+ *
128
+ * Uses ts-morph to manipulate the `` decorator's declarations array.
129
+ *
130
+ * @param moduleClass - The NgModule class declaration
131
+ * @param componentName - Name of the component to remove
132
+ * @returns True if successfully removed, false otherwise
133
+ */
134
+ removeFromDeclarations(
135
+ moduleClass: ClassDeclaration,
136
+ componentName: string,
137
+ ): boolean {
138
+ try {
139
+ // Get the @NgModule decorator
140
+ const ngModuleDecorator = moduleClass.getDecorator('NgModule');
141
+ if (!ngModuleDecorator) {
142
+ return false;
143
+ }
144
+
145
+ // Get the decorator arguments (should be an object literal)
146
+ const args = ngModuleDecorator.getArguments();
147
+ if (args.length === 0) {
148
+ return false;
149
+ }
150
+
151
+ // Get the configuration object - type assertion needed for ng-morph/ts-morph compatibility
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
153
+ const configObj = args[0] as any;
154
+ if (!configObj) {
155
+ return false;
156
+ }
157
+
158
+ // Find the declarations property
159
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
160
+ const properties = configObj.getProperties?.() ?? [];
161
+
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ for (const prop of properties as any[]) {
164
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
165
+ const propName = prop.getName?.() ?? '';
166
+
167
+ if (propName === 'declarations') {
168
+ // Get the array of declarations
169
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
170
+ const initializer = prop.getInitializer?.();
171
+ if (!initializer) {
172
+ continue;
173
+ }
174
+
175
+ // Get array elements
176
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
177
+ const elements = initializer.getElements?.() ?? [];
178
+
179
+ // Find and remove the component
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ for (const element of elements as any[]) {
182
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
183
+ const elementText = element.getText();
184
+ if (elementText === componentName) {
185
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
186
+ element.remove();
187
+ return true;
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ return false;
194
+ } catch (error) {
195
+ console.warn(
196
+ `Failed to remove ${componentName} from declarations:`,
197
+ error,
198
+ );
199
+ return false;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Add an import to a standalone component
205
+ * @param componentClass - The component class declaration
206
+ * @param importName - Name of the import (e.g., 'CommonModule')
207
+ * @param options - Optional configuration
208
+ */
209
+ addImportToComponent(
210
+ componentClass: ClassDeclaration,
211
+ importName: string,
212
+ options?: { unique?: boolean },
213
+ ): void {
214
+ addImportToComponent(componentClass, importName, options);
215
+ }
216
+
217
+ /**
218
+ * Add a provider to a standalone component
219
+ * @param componentClass - The component class declaration
220
+ * @param provider - Provider to add (e.g., 'MyService')
221
+ * @param options - Optional configuration
222
+ */
223
+ addProviderToComponent(
224
+ componentClass: ClassDeclaration,
225
+ provider: string,
226
+ options?: { unique?: boolean },
227
+ ): void {
228
+ addProviderToComponent(componentClass, provider, options);
229
+ }
230
+
231
+ /**
232
+ * Apply all changes to the file system
233
+ * This delegates to the ts-morph Project's save method
234
+ */
235
+ async apply(): Promise<void> {
236
+ await this.project.save();
237
+ }
238
+
239
+ /**
240
+ * Apply changes synchronously
241
+ */
242
+ applySync(): void {
243
+ this.project.saveSync();
244
+ }
245
+
246
+ /**
247
+ * Get the underlying ts-morph Project
248
+ */
249
+ getProject(): Project {
250
+ return this.project;
251
+ }
252
+ }
@@ -0,0 +1,450 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
6
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
7
+ /**
8
+ * @angular-modernizer/api - NgModule Manager
9
+ *
10
+ * Service for managing NgModule declarations and exports.
11
+ * Handles finding NgModules and removing standalone components from them.
12
+ *
13
+ * This is critical for completing the standalone migration, as components
14
+ * marked as standalone must be removed from NgModule declarations/exports
15
+ * to avoid compiler errors.
16
+ *
17
+ * MIGRATION NOTE: Refactored to use ng-morph for Angular-specific operations.
18
+ * - Code reduction: 268 lines → ~150 lines (~44% reduction)
19
+ * - Eliminated SyntaxKind checking and manual AST traversal
20
+ * - Leverages ng-morph's high-level Angular abstractions
21
+ */
22
+
23
+ import type { ClassDeclaration, Project, SourceFile } from 'ts-morph';
24
+ import type { NgMorphAdapter } from './ng-morph-adapter.js';
25
+
26
+ /**
27
+ * NgModule Manager - manages NgModule modifications during migration.
28
+ *
29
+ * This service provides high-level operations for NgModule cleanup,
30
+ * enabling the removal of standalone components from NgModule declarations.
31
+ */
32
+ export class NgModuleManager {
33
+ private readonly ngMorphAdapter: NgMorphAdapter;
34
+
35
+ /**
36
+ * Constructor for NgModuleManager.
37
+ *
38
+ * @param ngMorphAdapter - The ng-morph adapter instance
39
+ */
40
+ constructor(ngMorphAdapter: NgMorphAdapter) {
41
+ this.ngMorphAdapter = ngMorphAdapter;
42
+ }
43
+
44
+ /**
45
+ * Find the NgModule that declares a specific component.
46
+ *
47
+ * Uses two-tier detection strategy:
48
+ * 1. Fast path: ng-morph for standard `` decorators
49
+ * 2. Fallback: ts-morph for custom decorators (e.g., ``)
50
+ *
51
+ * This enables support for projects with external decorator patterns.
52
+ *
53
+ * @param project - The ts-morph Project instance
54
+ * @param componentClassName - The name of the component to find (e.g., 'CashbookOverviewComponent')
55
+ * @returns The source file containing the NgModule, or undefined if not found
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const ngModuleFile = ngModuleManager.findNgModuleForComponent(project, 'CashbookOverviewComponent');
60
+ * if (ngModuleFile) {
61
+ * console.info(`Found in: ${ngModuleFile.getFilePath()}`);
62
+ * }
63
+ * ```
64
+ *
65
+ * TIER 1 (ng-morph): Fast path for standard Angular components
66
+ * TIER 2 (ts-morph): Fallback for components with custom decorators
67
+ */
68
+ findNgModuleForComponent(
69
+ project: Project,
70
+ componentClassName: string,
71
+ ): SourceFile | undefined {
72
+ // TIER 1: Try ng-morph first (fast path for standard Angular)
73
+ const components = this.ngMorphAdapter.getComponents();
74
+ const component = components.find(
75
+ (comp) => comp.getName() === componentClassName,
76
+ );
77
+
78
+ if (component) {
79
+ // Found via ng-morph - use standard path
80
+ const module = this.ngMorphAdapter.findModuleForComponent(component);
81
+ if (module) {
82
+ const filePath = module.getSourceFile().getFilePath();
83
+ return project.getSourceFile(filePath);
84
+ }
85
+ }
86
+
87
+ // TIER 2: Fallback to ts-morph for components with custom decorators
88
+ // This handles cases like @ExportComponent, @ExportFunction, etc.
89
+ return this.findNgModuleForComponentWithCustomDecorators(
90
+ project,
91
+ componentClassName,
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Find NgModule for components with custom decorators (ts-morph fallback).
97
+ *
98
+ * Searches all files for:
99
+ * 1. A class with the target name
100
+ * 2. That has a `` decorator (standard or with custom decorators)
101
+ * 3. Then finds which NgModule declares that class
102
+ *
103
+ * This is slower than ng-morph but necessary for external decorator support.
104
+ *
105
+ * @param project - The ts-morph Project instance
106
+ * @param componentClassName - The name of the component class
107
+ * @returns The source file containing the declaring NgModule, or undefined
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * // Finds components like:
112
+ * // ``()
113
+ * // ``({ selector: 'app-user' })
114
+ * // export class UserComponent {}
115
+ * ```
116
+ */
117
+ private findNgModuleForComponentWithCustomDecorators(
118
+ project: Project,
119
+ componentClassName: string,
120
+ ): SourceFile | undefined {
121
+ // Step 1: Find the component class in the project
122
+ let componentFile: SourceFile | undefined;
123
+ let componentClass: ClassDeclaration | undefined;
124
+
125
+ for (const sourceFile of project.getSourceFiles()) {
126
+ const classes = sourceFile.getClasses();
127
+ for (const classDecl of classes) {
128
+ if (classDecl.getName() === componentClassName) {
129
+ // Check if this class has a @Component decorator
130
+ const hasComponentDecorator = classDecl
131
+ .getDecorators()
132
+ .some((dec) => dec.getName() === 'Component');
133
+
134
+ if (hasComponentDecorator) {
135
+ componentFile = sourceFile;
136
+ componentClass = classDecl;
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ if (componentClass) {
142
+ break;
143
+ }
144
+ }
145
+
146
+ if (!componentClass || !componentFile) {
147
+ return undefined;
148
+ }
149
+
150
+ // Step 2: Find NgModule that declares this component
151
+ // Search all files for NgModule with this component in declarations
152
+ for (const sourceFile of project.getSourceFiles()) {
153
+ const classes = sourceFile.getClasses();
154
+
155
+ for (const classDecl of classes) {
156
+ // Check if this is an NgModule
157
+ const ngModuleDecorator = classDecl
158
+ .getDecorators()
159
+ .find((dec) => dec.getName() === 'NgModule');
160
+
161
+ if (!ngModuleDecorator) {
162
+ continue;
163
+ }
164
+
165
+ // Extract metadata from @NgModule decorator
166
+ const args = ngModuleDecorator.getArguments();
167
+ if (args.length === 0) {
168
+ continue;
169
+ }
170
+
171
+ const metadata = args[0];
172
+ if (!metadata) {
173
+ continue;
174
+ }
175
+
176
+ // Check if declarations array contains our component
177
+ const declarations = this.extractArrayFromMetadata(
178
+ metadata,
179
+ 'declarations',
180
+ );
181
+
182
+ if (declarations.includes(componentClassName)) {
183
+ return sourceFile;
184
+ }
185
+ }
186
+ }
187
+
188
+ return undefined;
189
+ }
190
+
191
+ /**
192
+ * Remove components from NgModule declarations and exports arrays.
193
+ *
194
+ * Uses ng-morph to locate the module and ts-morph to remove array elements.
195
+ * This is simpler than the previous implementation.
196
+ *
197
+ * @param ngModuleFile - The source file containing the NgModule
198
+ * @param classNamesToRemove - Array of component class names to remove
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * ngModuleManager.removeDeclarations(
203
+ * ngModuleFile,
204
+ * ['CashbookOverviewComponent', 'AddCashbookEntryDialogComponent']
205
+ * );
206
+ * ```
207
+ *
208
+ * BEFORE (ts-morph): 35+ lines with SyntaxKind and manual AST traversal
209
+ * AFTER (ng-morph + ts-morph): ~20 lines with cleaner logic
210
+ */
211
+ removeDeclarations(
212
+ ngModuleFile: SourceFile,
213
+ classNamesToRemove: string[],
214
+ ): void {
215
+ if (classNamesToRemove.length === 0) {
216
+ return;
217
+ }
218
+
219
+ console.info(
220
+ ` [NgModuleManager] removeDeclarations() called for: ${ngModuleFile.getFilePath()}`,
221
+ );
222
+ console.info(
223
+ ` [NgModuleManager] Removing: [${classNamesToRemove.join(', ')}]`,
224
+ );
225
+
226
+ const namesToRemoveSet = new Set(classNamesToRemove);
227
+
228
+ // Use ts-morph directly instead of ng-morph (same pattern as getNgModuleImports)
229
+ const classes = ngModuleFile.getClasses();
230
+
231
+ for (const classDecl of classes) {
232
+ // Find the @NgModule decorator
233
+ const ngModuleDecorator = classDecl
234
+ .getDecorators()
235
+ .find((dec) => dec.getName() === 'NgModule');
236
+
237
+ if (!ngModuleDecorator) {
238
+ continue;
239
+ }
240
+
241
+ console.info(
242
+ ` [NgModuleManager] Found @NgModule decorator on class: ${classDecl.getName()}`,
243
+ );
244
+
245
+ // Extract metadata from @NgModule decorator1
246
+ const args = ngModuleDecorator.getArguments();
247
+ if (args.length === 0) {
248
+ console.info(` [NgModuleManager] Decorator has no arguments`);
249
+ continue;
250
+ }
251
+
252
+ const metadata = args[0];
253
+ if (!metadata) {
254
+ console.info(` [NgModuleManager] First argument is null/undefined`);
255
+ continue;
256
+ }
257
+
258
+ // Remove from declarations and exports arrays using ts-morph metadata
259
+ console.info(
260
+ ` [NgModuleManager] Removing from declarations and exports arrays...`,
261
+ );
262
+ this.removeFromMetadataArray(metadata, 'declarations', namesToRemoveSet);
263
+ this.removeFromMetadataArray(metadata, 'exports', namesToRemoveSet);
264
+ console.info(` [NgModuleManager] Removal complete`);
265
+ return; // Done - only one NgModule per file
266
+ }
267
+
268
+ console.info(` [NgModuleManager] No @NgModule decorator found in file`);
269
+ }
270
+
271
+ /**
272
+ * Get the imports array from an NgModule.
273
+ *
274
+ * Uses ng-morph to locate the module, then extracts imports using ts-morph.
275
+ * Simpler than the previous implementation.
276
+ *
277
+ * @param ngModuleFile - The source file containing the NgModule
278
+ * @returns Array of import names (e.g., ['CommonModule', 'ReactiveFormsModule'])
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * const imports = ngModuleManager.getNgModuleImports(cashbookModuleFile);
283
+ * // imports = ['CommonModule', 'ReactiveFormsModule', 'SharedComponentsModule']
284
+ * ```
285
+ *
286
+ * BEFORE (ts-morph): 45+ lines with SyntaxKind checking
287
+ * AFTER (ng-morph + ts-morph): ~15 lines with cleaner logic
288
+ */
289
+ getNgModuleImports(ngModuleFile: SourceFile): string[] {
290
+ const filePath = ngModuleFile.getFilePath();
291
+ console.info(
292
+ ` [NgModuleManager] getNgModuleImports() called for: ${filePath}`,
293
+ );
294
+
295
+ // Use ts-morph directly instead of ng-morph (same as findNgModuleForComponentWithCustomDecorators)
296
+ // This ensures we can extract imports even when file isn't in ng-morph's Project
297
+ const classes = ngModuleFile.getClasses();
298
+
299
+ for (const classDecl of classes) {
300
+ // Find the @NgModule decorator
301
+ const ngModuleDecorator = classDecl
302
+ .getDecorators()
303
+ .find((dec) => dec.getName() === 'NgModule');
304
+
305
+ if (!ngModuleDecorator) {
306
+ continue;
307
+ }
308
+
309
+ console.info(
310
+ ` [NgModuleManager] Found @NgModule decorator on class: ${classDecl.getName()}`,
311
+ );
312
+
313
+ // Extract metadata from @NgModule decorator
314
+ const args = ngModuleDecorator.getArguments();
315
+ if (args.length === 0) {
316
+ console.info(` [NgModuleManager] Decorator has no arguments`);
317
+ continue;
318
+ }
319
+
320
+ const metadata = args[0];
321
+ if (!metadata) {
322
+ console.info(` [NgModuleManager] First argument is null/undefined`);
323
+ continue;
324
+ }
325
+
326
+ console.info(` [NgModuleManager] Extracting imports array...`);
327
+ // Extract imports array using ts-morph metadata
328
+ return this.extractArrayFromMetadata(metadata, 'imports');
329
+ }
330
+
331
+ console.info(` [NgModuleManager] No @NgModule decorator found in file`);
332
+ return [];
333
+ }
334
+
335
+ /**
336
+ * Extract an array property from NgModule metadata.
337
+ *
338
+ * Helper method to extract array values from metadata object.
339
+ *
340
+ * @param metadata - The metadata object from `` decorator
341
+ * @param propertyName - The property name (e.g., 'imports', 'declarations')
342
+ * @returns Array of string values from the property
343
+ *
344
+ * Note: Uses type assertions due to ng-morph/ts-morph version mismatch
345
+ */
346
+ private extractArrayFromMetadata(
347
+ metadata: any,
348
+ propertyName: string,
349
+ ): string[] {
350
+ console.info(` [NgModuleManager] Extracting array: ${propertyName}`);
351
+ try {
352
+ const objLiteral = metadata.asKind(metadata.getKind());
353
+ if (!objLiteral || typeof objLiteral.getProperty !== 'function') {
354
+ console.info(
355
+ ` [NgModuleManager] No objLiteral or getProperty function`,
356
+ );
357
+ return [];
358
+ }
359
+
360
+ const property = objLiteral.getProperty(propertyName);
361
+ if (!property || typeof property.getInitializer !== 'function') {
362
+ console.info(
363
+ ` [NgModuleManager] No property '${propertyName}' or getInitializer function`,
364
+ );
365
+ console.info(
366
+ ` [NgModuleManager] Available properties:`,
367
+ objLiteral.getProperties
368
+ ? objLiteral
369
+ .getProperties()
370
+ .map((p: any) => p.getName())
371
+ .join(', ')
372
+ : 'N/A',
373
+ );
374
+ return [];
375
+ }
376
+
377
+ const initializer = property.getInitializer();
378
+ if (!initializer || typeof initializer.getElements !== 'function') {
379
+ console.info(
380
+ ` [NgModuleManager] No initializer or getElements function`,
381
+ );
382
+ return [];
383
+ }
384
+
385
+ const elements = initializer.getElements();
386
+ const result = elements.map((el: any) => el.getText().trim());
387
+ console.info(
388
+ ` [NgModuleManager] Extracted ${result.length} elements: [${result.join(', ')}]`,
389
+ );
390
+ return result;
391
+ } catch (error) {
392
+ console.info(` [NgModuleManager] Exception during extraction:`, error);
393
+ return [];
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Remove elements from a metadata array property.
399
+ *
400
+ * Helper method to remove specific values from arrays in metadata.
401
+ *
402
+ * @param metadata - The metadata object from `` decorator
403
+ * @param propertyName - The property name (e.g., 'declarations', 'exports')
404
+ * @param namesToRemove - Set of names to remove
405
+ *
406
+ * Note: Uses type assertions due to ng-morph/ts-morph version mismatch
407
+ */
408
+ private removeFromMetadataArray(
409
+ metadata: any,
410
+ propertyName: 'declarations' | 'exports',
411
+ namesToRemove: Set<string>,
412
+ ): void {
413
+ try {
414
+ const objLiteral = metadata.asKind(metadata.getKind());
415
+ if (!objLiteral || typeof objLiteral.getProperty !== 'function') {
416
+ return;
417
+ }
418
+
419
+ const property = objLiteral.getProperty(propertyName);
420
+ if (!property || typeof property.getInitializer !== 'function') {
421
+ return;
422
+ }
423
+
424
+ const arrayLiteral = property.getInitializer();
425
+ if (!arrayLiteral || typeof arrayLiteral.getElements !== 'function') {
426
+ return;
427
+ }
428
+
429
+ const elements = arrayLiteral.getElements();
430
+
431
+ // Collect indices to remove
432
+ const indicesToRemove: number[] = [];
433
+ elements.forEach((element: any, index: number) => {
434
+ const elementName = element.getText().trim();
435
+ if (namesToRemove.has(elementName)) {
436
+ indicesToRemove.push(index);
437
+ }
438
+ });
439
+
440
+ // Remove in reverse order to preserve indices
441
+ indicesToRemove.reverse().forEach((index) => {
442
+ if (typeof arrayLiteral.removeElement === 'function') {
443
+ arrayLiteral.removeElement(index);
444
+ }
445
+ });
446
+ } catch {
447
+ // Silently fail if metadata structure is unexpected
448
+ }
449
+ }
450
+ }