@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.
- package/LICENSE +21 -0
- package/README.md +507 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts +99 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts.map +1 -0
- package/dist/analysis/complexity-metrics-calculator.js +195 -0
- package/dist/analysis/complexity-metrics-calculator.js.map +1 -0
- package/dist/analysis/dependency-analyzer.d.ts +227 -0
- package/dist/analysis/dependency-analyzer.d.ts.map +1 -0
- package/dist/analysis/dependency-analyzer.js +451 -0
- package/dist/analysis/dependency-analyzer.js.map +1 -0
- package/dist/analysis/external-template-loader.d.ts +44 -0
- package/dist/analysis/external-template-loader.d.ts.map +1 -0
- package/dist/analysis/external-template-loader.js +69 -0
- package/dist/analysis/external-template-loader.js.map +1 -0
- package/dist/analysis/selector-mapper.d.ts +77 -0
- package/dist/analysis/selector-mapper.d.ts.map +1 -0
- package/dist/analysis/selector-mapper.js +146 -0
- package/dist/analysis/selector-mapper.js.map +1 -0
- package/dist/analysis/service-detector.d.ts +164 -0
- package/dist/analysis/service-detector.d.ts.map +1 -0
- package/dist/analysis/service-detector.js +231 -0
- package/dist/analysis/service-detector.js.map +1 -0
- package/dist/analysis/service-pattern-recognizer.d.ts +247 -0
- package/dist/analysis/service-pattern-recognizer.d.ts.map +1 -0
- package/dist/analysis/service-pattern-recognizer.js +444 -0
- package/dist/analysis/service-pattern-recognizer.js.map +1 -0
- package/dist/analysis/symbol-locator.d.ts +81 -0
- package/dist/analysis/symbol-locator.d.ts.map +1 -0
- package/dist/analysis/symbol-locator.js +128 -0
- package/dist/analysis/symbol-locator.js.map +1 -0
- package/dist/analysis/template-analyzer-usage.d.ts +144 -0
- package/dist/analysis/template-analyzer-usage.d.ts.map +1 -0
- package/dist/analysis/template-analyzer-usage.js +474 -0
- package/dist/analysis/template-analyzer-usage.js.map +1 -0
- package/dist/analysis/template-analyzer.d.ts +80 -0
- package/dist/analysis/template-analyzer.d.ts.map +1 -0
- package/dist/analysis/template-analyzer.js +146 -0
- package/dist/analysis/template-analyzer.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/investigation/angular-symbol-finder.d.ts +76 -0
- package/dist/investigation/angular-symbol-finder.d.ts.map +1 -0
- package/dist/investigation/angular-symbol-finder.js +199 -0
- package/dist/investigation/angular-symbol-finder.js.map +1 -0
- package/dist/investigation/call-graph-builder.d.ts +85 -0
- package/dist/investigation/call-graph-builder.d.ts.map +1 -0
- package/dist/investigation/call-graph-builder.js +196 -0
- package/dist/investigation/call-graph-builder.js.map +1 -0
- package/dist/investigation/codebase-searcher.d.ts +99 -0
- package/dist/investigation/codebase-searcher.d.ts.map +1 -0
- package/dist/investigation/codebase-searcher.js +201 -0
- package/dist/investigation/codebase-searcher.js.map +1 -0
- package/dist/investigation/stack-trace-parser.d.ts +70 -0
- package/dist/investigation/stack-trace-parser.d.ts.map +1 -0
- package/dist/investigation/stack-trace-parser.js +117 -0
- package/dist/investigation/stack-trace-parser.js.map +1 -0
- package/dist/investigation/type-resolver.d.ts +59 -0
- package/dist/investigation/type-resolver.d.ts.map +1 -0
- package/dist/investigation/type-resolver.js +111 -0
- package/dist/investigation/type-resolver.js.map +1 -0
- package/dist/investigation/usage-finder.d.ts +69 -0
- package/dist/investigation/usage-finder.d.ts.map +1 -0
- package/dist/investigation/usage-finder.js +132 -0
- package/dist/investigation/usage-finder.js.map +1 -0
- package/dist/metrics/complexity-metrics.d.ts +196 -0
- package/dist/metrics/complexity-metrics.d.ts.map +1 -0
- package/dist/metrics/complexity-metrics.js +445 -0
- package/dist/metrics/complexity-metrics.js.map +1 -0
- package/dist/public-api.d.ts +77 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +50 -0
- package/dist/public-api.js.map +1 -0
- package/dist/transformation/import-manager.d.ts +276 -0
- package/dist/transformation/import-manager.d.ts.map +1 -0
- package/dist/transformation/import-manager.js +488 -0
- package/dist/transformation/import-manager.js.map +1 -0
- package/dist/transformation/ng-morph-adapter.d.ts +111 -0
- package/dist/transformation/ng-morph-adapter.d.ts.map +1 -0
- package/dist/transformation/ng-morph-adapter.js +179 -0
- package/dist/transformation/ng-morph-adapter.js.map +1 -0
- package/dist/transformation/ngmodule-manager.d.ts +145 -0
- package/dist/transformation/ngmodule-manager.d.ts.map +1 -0
- package/dist/transformation/ngmodule-manager.js +356 -0
- package/dist/transformation/ngmodule-manager.js.map +1 -0
- package/package.json +62 -0
- package/src/analysis/complexity-metrics-calculator.ts +253 -0
- package/src/analysis/dependency-analyzer.ts +720 -0
- package/src/analysis/external-template-loader.ts +79 -0
- package/src/analysis/selector-mapper.ts +186 -0
- package/src/analysis/service-detector.ts +280 -0
- package/src/analysis/service-pattern-recognizer.ts +545 -0
- package/src/analysis/symbol-locator.ts +170 -0
- package/src/analysis/template-analyzer-usage.ts +614 -0
- package/src/analysis/template-analyzer.ts +208 -0
- package/src/index.ts +51 -0
- package/src/investigation/angular-symbol-finder.ts +263 -0
- package/src/investigation/call-graph-builder.ts +298 -0
- package/src/investigation/codebase-searcher.ts +286 -0
- package/src/investigation/stack-trace-parser.ts +166 -0
- package/src/investigation/type-resolver.ts +144 -0
- package/src/investigation/usage-finder.ts +198 -0
- package/src/metrics/complexity-metrics.ts +617 -0
- package/src/public-api.ts +119 -0
- package/src/transformation/import-manager.ts +626 -0
- package/src/transformation/ng-morph-adapter.ts +252 -0
- 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
|
+
}
|