@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,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
+ }