@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,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Import Manager
|
|
3
|
+
*
|
|
4
|
+
* High-level API for managing imports in TypeScript files.
|
|
5
|
+
* Handles adding, removing, and organizing imports safely.
|
|
6
|
+
*
|
|
7
|
+
* ## HYBRID ng-morph + ts-morph APPROACH
|
|
8
|
+
*
|
|
9
|
+
* ### What ng-morph HANDLES (Component/Module Metadata):
|
|
10
|
+
* - Component imports: [] array (e.g., @Component({ imports: [CommonModule] }))
|
|
11
|
+
* - Module imports: [] array (e.g., @NgModule({ imports: [CommonModule] }))
|
|
12
|
+
* - These are decorator metadata, NOT ES6 import statements
|
|
13
|
+
*
|
|
14
|
+
* ### What ts-morph HANDLES (Everything Else):
|
|
15
|
+
* - ES6 import statements (e.g., import { CommonModule } from '@angular/common')
|
|
16
|
+
* - Import organization and merging
|
|
17
|
+
* - Removal of imports
|
|
18
|
+
* - Aliases and complex imports
|
|
19
|
+
* - Non-Angular imports
|
|
20
|
+
*
|
|
21
|
+
* ### Code Impact:
|
|
22
|
+
* - ng-morph provides value for ~5-10% of import operations (metadata only)
|
|
23
|
+
* - ts-morph still handles 90-95% of imports (ES6 statements)
|
|
24
|
+
* - Maintains 100% backward compatibility
|
|
25
|
+
*
|
|
26
|
+
* ### Why This Hybrid?
|
|
27
|
+
* ng-morph provides cleaner API for adding to decorator metadata arrays.
|
|
28
|
+
* For ES6 import statements, ng-morph is just a thin wrapper around ts-morph with no advantage.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { SourceFile } from 'ts-morph';
|
|
32
|
+
import type { NgMorphAdapter } from './ng-morph-adapter.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Import information.
|
|
36
|
+
*/
|
|
37
|
+
export interface ImportInfo {
|
|
38
|
+
/**
|
|
39
|
+
* Module specifier (e.g., '@angular/core').
|
|
40
|
+
*/
|
|
41
|
+
moduleSpecifier: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Named imports (e.g., ['Component', 'OnInit']).
|
|
45
|
+
*/
|
|
46
|
+
namedImports?: string[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default import (e.g., 'React').
|
|
50
|
+
*/
|
|
51
|
+
defaultImport?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Namespace import (e.g., '* as fs').
|
|
55
|
+
*/
|
|
56
|
+
namespaceImport?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Import Manager - manages imports in source files using hybrid ng-morph + ts-morph approach.
|
|
61
|
+
*
|
|
62
|
+
* ## HYBRID STRATEGY (See file header for full explanation)
|
|
63
|
+
*
|
|
64
|
+
* **ng-morph (5-10% of operations):**
|
|
65
|
+
* - ONLY for adding to component/module imports: [] metadata arrays
|
|
66
|
+
* - Example: @Component({ imports: [CommonModule] }) ← ng-morph adds here
|
|
67
|
+
*
|
|
68
|
+
* **ts-morph (90-95% of operations):**
|
|
69
|
+
* - ES6 import statements: import { X } from 'y'
|
|
70
|
+
* - Import removal, organization, aliases
|
|
71
|
+
* - All non-metadata import operations
|
|
72
|
+
*
|
|
73
|
+
* **Result:** Cleaner API for metadata, minimal overall code reduction.
|
|
74
|
+
*/
|
|
75
|
+
export class ImportManager {
|
|
76
|
+
private readonly ngMorphAdapter?: NgMorphAdapter;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Constructor for ImportManager.
|
|
80
|
+
*
|
|
81
|
+
* @param ngMorphAdapter - Optional ng-morph adapter for Angular-specific operations
|
|
82
|
+
*
|
|
83
|
+
* BACKWARD COMPATIBILITY: Constructor is optional - if not provided, falls back to pure ts-morph.
|
|
84
|
+
*/
|
|
85
|
+
constructor(ngMorphAdapter?: NgMorphAdapter) {
|
|
86
|
+
this.ngMorphAdapter = ngMorphAdapter;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Add a named import to a source file.
|
|
90
|
+
* Handles merging with existing imports from the same module.
|
|
91
|
+
*
|
|
92
|
+
* HYBRID APPROACH:
|
|
93
|
+
* - Angular imports (@angular/*): Try ng-morph for components/modules
|
|
94
|
+
* - Non-Angular/complex: Fall back to ts-morph
|
|
95
|
+
*
|
|
96
|
+
* @param sourceFile - The source file to modify
|
|
97
|
+
* @param moduleSpecifier - The module to import from (e.g., '@angular/common')
|
|
98
|
+
* @param namedImport - The named import to add (e.g., 'CommonModule')
|
|
99
|
+
* @param alias - Optional alias for the import (e.g., 'NgComponent')
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* importManager.addNamedImport(sourceFile, '@angular/common', 'CommonModule');
|
|
104
|
+
* // Result: import { CommonModule } from '@angular/common';
|
|
105
|
+
*
|
|
106
|
+
* importManager.addNamedImport(sourceFile, '@angular/core', 'Component', 'NgComponent');
|
|
107
|
+
* // Result: import { Component as NgComponent } from '@angular/core';
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* BEFORE (ts-morph): 30 lines with manual checking
|
|
111
|
+
* AFTER (hybrid): ~20 lines with ng-morph for Angular imports
|
|
112
|
+
*/
|
|
113
|
+
addNamedImport(
|
|
114
|
+
sourceFile: SourceFile,
|
|
115
|
+
moduleSpecifier: string,
|
|
116
|
+
namedImport: string,
|
|
117
|
+
alias?: string,
|
|
118
|
+
): void {
|
|
119
|
+
// Try ng-morph for Angular imports
|
|
120
|
+
if (this.ngMorphAdapter && this.isAngularImport(moduleSpecifier)) {
|
|
121
|
+
if (
|
|
122
|
+
this.tryAddAngularImport(
|
|
123
|
+
sourceFile,
|
|
124
|
+
moduleSpecifier,
|
|
125
|
+
namedImport,
|
|
126
|
+
alias,
|
|
127
|
+
)
|
|
128
|
+
) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fall back to ts-morph for non-Angular or complex cases
|
|
134
|
+
this.addImportUsingTsMorph(sourceFile, moduleSpecifier, namedImport, alias);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if a module specifier is an Angular import.
|
|
139
|
+
*
|
|
140
|
+
* @param moduleSpecifier - The module specifier to check
|
|
141
|
+
* @returns True if it's an Angular module
|
|
142
|
+
*/
|
|
143
|
+
private isAngularImport(moduleSpecifier: string): boolean {
|
|
144
|
+
return moduleSpecifier.startsWith('@angular/');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Try to add an Angular import using ng-morph.
|
|
149
|
+
*
|
|
150
|
+
* ng-morph can add imports to component/module metadata (imports: [] array).
|
|
151
|
+
* This is useful for adding Angular modules like CommonModule, FormsModule, etc.
|
|
152
|
+
*
|
|
153
|
+
* @param sourceFile - The source file
|
|
154
|
+
* @param moduleSpecifier - The Angular module specifier
|
|
155
|
+
* @param namedImport - The import name (e.g., 'CommonModule')
|
|
156
|
+
* @param alias - Optional alias (not supported for metadata array imports)
|
|
157
|
+
* @returns True if successfully added via ng-morph, false to fall back
|
|
158
|
+
*/
|
|
159
|
+
private tryAddAngularImport(
|
|
160
|
+
sourceFile: SourceFile,
|
|
161
|
+
_moduleSpecifier: string,
|
|
162
|
+
namedImport: string,
|
|
163
|
+
alias?: string,
|
|
164
|
+
): boolean {
|
|
165
|
+
// ng-morph doesn't support aliases for metadata imports
|
|
166
|
+
if (alias) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const filePath = sourceFile.getFilePath();
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// Check if this is a component file
|
|
174
|
+
const component = this.ngMorphAdapter?.getComponent(filePath);
|
|
175
|
+
if (component) {
|
|
176
|
+
// Add to component's imports array
|
|
177
|
+
this.ngMorphAdapter?.addImportToComponent(component, namedImport, {
|
|
178
|
+
unique: true,
|
|
179
|
+
});
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check if this is a module file
|
|
184
|
+
const module = this.ngMorphAdapter?.getModule(filePath);
|
|
185
|
+
if (module) {
|
|
186
|
+
// Add to module's imports array
|
|
187
|
+
this.ngMorphAdapter?.addImportToModule(module, namedImport, {
|
|
188
|
+
unique: true,
|
|
189
|
+
});
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Not a component or module - fall back to ts-morph
|
|
194
|
+
return false;
|
|
195
|
+
} catch {
|
|
196
|
+
// ng-morph failed - fall back to ts-morph
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Add import using ts-morph (fallback for complex cases).
|
|
203
|
+
*
|
|
204
|
+
* @param sourceFile - The source file
|
|
205
|
+
* @param moduleSpecifier - The module specifier
|
|
206
|
+
* @param namedImport - The import name
|
|
207
|
+
* @param alias - Optional alias
|
|
208
|
+
*/
|
|
209
|
+
private addImportUsingTsMorph(
|
|
210
|
+
sourceFile: SourceFile,
|
|
211
|
+
moduleSpecifier: string,
|
|
212
|
+
namedImport: string,
|
|
213
|
+
alias?: string,
|
|
214
|
+
): void {
|
|
215
|
+
const existingImport = sourceFile.getImportDeclaration(moduleSpecifier);
|
|
216
|
+
|
|
217
|
+
if (!existingImport) {
|
|
218
|
+
// No import from this module exists, create a new one
|
|
219
|
+
const namedImports = alias
|
|
220
|
+
? [{ name: namedImport, alias }]
|
|
221
|
+
: [namedImport];
|
|
222
|
+
sourceFile.addImportDeclaration({
|
|
223
|
+
moduleSpecifier,
|
|
224
|
+
namedImports,
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Import from this module exists, check if the named import is already there
|
|
230
|
+
const alreadyImported = existingImport
|
|
231
|
+
.getNamedImports()
|
|
232
|
+
.some((ni) => ni.getName() === namedImport);
|
|
233
|
+
|
|
234
|
+
if (alreadyImported) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Add the named import to the existing import declaration
|
|
239
|
+
const importSpec = alias ? { name: namedImport, alias } : namedImport;
|
|
240
|
+
existingImport.addNamedImport(importSpec);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Remove a named import from a source file.
|
|
245
|
+
* Cleans up empty import declarations after removal.
|
|
246
|
+
*
|
|
247
|
+
* HYBRID APPROACH:
|
|
248
|
+
* - Always uses ts-morph for removal (simpler than ng-morph)
|
|
249
|
+
* - ng-morph doesn't provide significant benefits for removal operations
|
|
250
|
+
*
|
|
251
|
+
* @param sourceFile - The source file
|
|
252
|
+
* @param namedImport - The named import to remove
|
|
253
|
+
* @param moduleSpecifier - Optional module specifier to target specific imports
|
|
254
|
+
*
|
|
255
|
+
* Handles:
|
|
256
|
+
* - Removing specific named imports from import declarations
|
|
257
|
+
* - Cleaning up empty import declarations
|
|
258
|
+
* - Preserving other imports from the same module
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* // Before: import { Foo, Bar } from './module';
|
|
263
|
+
* importManager.removeNamedImport(sourceFile, 'Bar', './module');
|
|
264
|
+
* // After: import { Foo } from './module';
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
removeNamedImport(
|
|
268
|
+
sourceFile: SourceFile,
|
|
269
|
+
namedImport: string,
|
|
270
|
+
moduleSpecifier?: string,
|
|
271
|
+
): void {
|
|
272
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
273
|
+
|
|
274
|
+
// Helper to check if import declaration is empty after removal
|
|
275
|
+
const isEmptyImportDecl = (
|
|
276
|
+
decl: (typeof importDeclarations)[0],
|
|
277
|
+
): boolean => {
|
|
278
|
+
return (
|
|
279
|
+
decl.getNamedImports().length === 0 &&
|
|
280
|
+
!decl.getDefaultImport() &&
|
|
281
|
+
!decl.getNamespaceImport()
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Find and remove the import from the first matching declaration
|
|
286
|
+
importDeclarations.some((importDecl) => {
|
|
287
|
+
// Skip if module specifier doesn't match (when specified)
|
|
288
|
+
if (
|
|
289
|
+
moduleSpecifier &&
|
|
290
|
+
importDecl.getModuleSpecifierValue() !== moduleSpecifier
|
|
291
|
+
) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Find the named import to remove
|
|
296
|
+
const importToRemove = importDecl
|
|
297
|
+
.getNamedImports()
|
|
298
|
+
.find((ni) => ni.getName() === namedImport);
|
|
299
|
+
|
|
300
|
+
if (!importToRemove) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Remove the named import
|
|
305
|
+
importToRemove.remove();
|
|
306
|
+
|
|
307
|
+
// Remove entire declaration if empty
|
|
308
|
+
if (isEmptyImportDecl(importDecl)) {
|
|
309
|
+
importDecl.remove();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return true; // Stop after first match
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get all imports from a source file.
|
|
318
|
+
*
|
|
319
|
+
* @param sourceFile - The source file
|
|
320
|
+
* @returns Array of import information
|
|
321
|
+
*
|
|
322
|
+
* Extracts:
|
|
323
|
+
* - All named imports with their module specifiers
|
|
324
|
+
* - Default imports and namespace imports
|
|
325
|
+
* - Import declaration positions for organization
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```typescript
|
|
329
|
+
* const imports = importManager.getImports(sourceFile);
|
|
330
|
+
* // Returns:
|
|
331
|
+
* // [
|
|
332
|
+
* // { moduleSpecifier: '@angular/core', namedImports: ['Component', 'OnInit'] },
|
|
333
|
+
* // { moduleSpecifier: 'rxjs', namedImports: ['Observable'], defaultImport: 'Rx' }
|
|
334
|
+
* // ]
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
getImports(sourceFile: SourceFile): ImportInfo[] {
|
|
338
|
+
const imports: ImportInfo[] = [];
|
|
339
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
340
|
+
|
|
341
|
+
for (const importDecl of importDeclarations) {
|
|
342
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
343
|
+
const importInfo: ImportInfo = {
|
|
344
|
+
moduleSpecifier,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Extract named imports
|
|
348
|
+
const namedImports = importDecl.getNamedImports();
|
|
349
|
+
if (namedImports.length > 0) {
|
|
350
|
+
importInfo.namedImports = namedImports.map((ni) => ni.getName());
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Extract default import
|
|
354
|
+
const defaultImport = importDecl.getDefaultImport();
|
|
355
|
+
if (defaultImport) {
|
|
356
|
+
importInfo.defaultImport = defaultImport.getText();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Extract namespace import (e.g., * as fs)
|
|
360
|
+
const namespaceImport = importDecl.getNamespaceImport();
|
|
361
|
+
if (namespaceImport) {
|
|
362
|
+
importInfo.namespaceImport = namespaceImport.getText();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
imports.push(importInfo);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return imports;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Organize imports (sort and remove duplicates).
|
|
373
|
+
*
|
|
374
|
+
* @param sourceFile - The source file
|
|
375
|
+
*
|
|
376
|
+
* Handles:
|
|
377
|
+
* - Sorting imports by module specifier (external libraries first, then internal)
|
|
378
|
+
* - Removing duplicate imports
|
|
379
|
+
* - Grouping related imports
|
|
380
|
+
* - Maintaining proper spacing and formatting
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```typescript
|
|
384
|
+
* // Before:
|
|
385
|
+
* // import { Component } from '@angular/core';
|
|
386
|
+
* // import { MyService } from './my-service';
|
|
387
|
+
* // import { Injectable } from '@angular/core';
|
|
388
|
+
*
|
|
389
|
+
* importManager.organizeImports(sourceFile);
|
|
390
|
+
*
|
|
391
|
+
* // After:
|
|
392
|
+
* // import { Component, Injectable } from '@angular/core';
|
|
393
|
+
* // import { MyService } from './my-service';
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
organizeImports(sourceFile: SourceFile): void {
|
|
397
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
398
|
+
|
|
399
|
+
if (importDeclarations.length === 0) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Group imports by module specifier, preserving aliases
|
|
404
|
+
interface NamedImportEntry {
|
|
405
|
+
name: string;
|
|
406
|
+
alias?: string;
|
|
407
|
+
}
|
|
408
|
+
const importMap = new Map<
|
|
409
|
+
string,
|
|
410
|
+
{
|
|
411
|
+
namedImports: Map<string, NamedImportEntry>; // key: "name|alias" for dedup
|
|
412
|
+
defaultImport?: string;
|
|
413
|
+
namespaceImport?: string;
|
|
414
|
+
}
|
|
415
|
+
>();
|
|
416
|
+
|
|
417
|
+
// Collect all imports grouped by module specifier
|
|
418
|
+
importDeclarations.forEach((importDecl) => {
|
|
419
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
420
|
+
|
|
421
|
+
if (!importMap.has(moduleSpecifier)) {
|
|
422
|
+
importMap.set(moduleSpecifier, {
|
|
423
|
+
namedImports: new Map(),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const importData = importMap.get(moduleSpecifier)!;
|
|
428
|
+
|
|
429
|
+
// Collect named imports, preserving aliases (e.g. UserModel as DomainUser)
|
|
430
|
+
importDecl.getNamedImports().forEach((ni) => {
|
|
431
|
+
const name = ni.getName();
|
|
432
|
+
const alias = ni.getAliasNode()?.getText();
|
|
433
|
+
const key = alias ? `${name}|${alias}` : name;
|
|
434
|
+
importData.namedImports.set(key, { name, alias });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Collect default import (take the first one if multiple)
|
|
438
|
+
const defaultImport = importDecl.getDefaultImport();
|
|
439
|
+
if (defaultImport && !importData.defaultImport) {
|
|
440
|
+
importData.defaultImport = defaultImport.getText();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Collect namespace import (take the first one if multiple)
|
|
444
|
+
const namespaceImport = importDecl.getNamespaceImport();
|
|
445
|
+
if (namespaceImport && !importData.namespaceImport) {
|
|
446
|
+
importData.namespaceImport = namespaceImport.getText();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Sort module specifiers: external first, then internal
|
|
451
|
+
const sortedModules = Array.from(importMap.keys()).sort((a, b) => {
|
|
452
|
+
const aIsExternal = !a.startsWith('.');
|
|
453
|
+
const bIsExternal = !b.startsWith('.');
|
|
454
|
+
|
|
455
|
+
if (aIsExternal !== bIsExternal) {
|
|
456
|
+
return aIsExternal ? -1 : 1;
|
|
457
|
+
}
|
|
458
|
+
return a.localeCompare(b);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Remove all existing import declarations and add organized imports back
|
|
462
|
+
importDeclarations.forEach((decl) => decl.remove());
|
|
463
|
+
|
|
464
|
+
sortedModules.forEach((moduleSpecifier) => {
|
|
465
|
+
const importData = importMap.get(moduleSpecifier)!;
|
|
466
|
+
// Sort by name (and alias if present) for stable output
|
|
467
|
+
const namedImports = Array.from(importData.namedImports.values()).sort(
|
|
468
|
+
(a, b) =>
|
|
469
|
+
a.name.localeCompare(b.name) ||
|
|
470
|
+
(a.alias ?? '').localeCompare(b.alias ?? ''),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
sourceFile.addImportDeclaration({
|
|
474
|
+
moduleSpecifier,
|
|
475
|
+
namedImports: namedImports.length > 0 ? namedImports : undefined,
|
|
476
|
+
defaultImport: importData.defaultImport,
|
|
477
|
+
namespaceImport: importData.namespaceImport,
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Merge duplicate import declarations.
|
|
484
|
+
*
|
|
485
|
+
* Combines multiple import statements from the same module into one.
|
|
486
|
+
*
|
|
487
|
+
* @param sourceFile - The source file
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* // Before:
|
|
492
|
+
* // import { Component } from '@angular/core';
|
|
493
|
+
* // import { Injectable } from '@angular/core';
|
|
494
|
+
*
|
|
495
|
+
* importManager.mergeImports(sourceFile);
|
|
496
|
+
*
|
|
497
|
+
* // After:
|
|
498
|
+
* // import { Component, Injectable } from '@angular/core';
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
mergeImports(sourceFile: SourceFile): void {
|
|
502
|
+
this.organizeImports(sourceFile);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Remove import declarations that have no imports.
|
|
507
|
+
*
|
|
508
|
+
* Cleans up empty import statements that may result from refactoring.
|
|
509
|
+
*
|
|
510
|
+
* @param sourceFile - The source file
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* ```typescript
|
|
514
|
+
* // Before: import {} from '@angular/core';
|
|
515
|
+
*
|
|
516
|
+
* importManager.cleanupEmptyImports(sourceFile);
|
|
517
|
+
*
|
|
518
|
+
* // After: (empty import removed)
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
cleanupEmptyImports(sourceFile: SourceFile): void {
|
|
522
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
523
|
+
|
|
524
|
+
for (const importDecl of importDeclarations) {
|
|
525
|
+
const namedImports = importDecl.getNamedImports();
|
|
526
|
+
const hasDefaultImport = !!importDecl.getDefaultImport();
|
|
527
|
+
const hasNamespaceImport = !!importDecl.getNamespaceImport();
|
|
528
|
+
|
|
529
|
+
// Remove if no imports at all
|
|
530
|
+
if (
|
|
531
|
+
namedImports.length === 0 &&
|
|
532
|
+
!hasDefaultImport &&
|
|
533
|
+
!hasNamespaceImport
|
|
534
|
+
) {
|
|
535
|
+
importDecl.remove();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Check if a named import exists in the source file.
|
|
542
|
+
*
|
|
543
|
+
* @param sourceFile - The source file
|
|
544
|
+
* @param symbol - The symbol name to check
|
|
545
|
+
* @param module - Optional module specifier to check specifically
|
|
546
|
+
* @returns True if the import exists
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* ```typescript
|
|
550
|
+
* const exists = importManager.hasImport(sourceFile, 'Component', '@angular/core');
|
|
551
|
+
* // Returns true if 'import { Component } from '@angular/core';' exists
|
|
552
|
+
* ```
|
|
553
|
+
*/
|
|
554
|
+
hasImport(sourceFile: SourceFile, symbol: string, module?: string): boolean {
|
|
555
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
556
|
+
|
|
557
|
+
for (const importDecl of importDeclarations) {
|
|
558
|
+
if (module && importDecl.getModuleSpecifierValue() !== module) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const namedImports = importDecl.getNamedImports();
|
|
563
|
+
const hasNamedImport = namedImports.some((ni) => ni.getName() === symbol);
|
|
564
|
+
|
|
565
|
+
if (hasNamedImport) {
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check default import
|
|
570
|
+
const defaultImport = importDecl.getDefaultImport();
|
|
571
|
+
if (defaultImport && defaultImport.getText() === symbol) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check namespace import
|
|
576
|
+
const namespaceImport = importDecl.getNamespaceImport();
|
|
577
|
+
if (namespaceImport && namespaceImport.getText() === symbol) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Get the alias for a named import, if it exists.
|
|
587
|
+
*
|
|
588
|
+
* @param sourceFile - The source file
|
|
589
|
+
* @param symbol - The symbol name
|
|
590
|
+
* @param module - Optional module specifier
|
|
591
|
+
* @returns The alias if found, undefined otherwise
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```typescript
|
|
595
|
+
* // import { Component as NgComponent } from '@angular/core';
|
|
596
|
+
* const alias = importManager.getImportAlias(sourceFile, 'Component', '@angular/core');
|
|
597
|
+
* // Returns 'NgComponent'
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
getImportAlias(
|
|
601
|
+
sourceFile: SourceFile,
|
|
602
|
+
symbol: string,
|
|
603
|
+
module?: string,
|
|
604
|
+
): string | undefined {
|
|
605
|
+
const importDeclarations = sourceFile.getImportDeclarations();
|
|
606
|
+
|
|
607
|
+
// Find the first matching import with the symbol
|
|
608
|
+
for (const importDecl of importDeclarations) {
|
|
609
|
+
// Skip if module doesn't match (when specified)
|
|
610
|
+
if (module && importDecl.getModuleSpecifierValue() !== module) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Find the named import with matching symbol
|
|
615
|
+
const matchingImport = importDecl
|
|
616
|
+
.getNamedImports()
|
|
617
|
+
.find((ni) => ni.getName() === symbol);
|
|
618
|
+
|
|
619
|
+
if (matchingImport) {
|
|
620
|
+
return matchingImport.getAliasNode()?.getText();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
}
|