@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,614 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* @angular-modernizer/api - Template Usage Analyzer
|
|
8
|
+
*
|
|
9
|
+
* Intelligent template analysis for import filtering.
|
|
10
|
+
* Three-tier strategy: Fast path → Angular Compiler → Conservative fallback
|
|
11
|
+
*
|
|
12
|
+
* Philosophy: "Only Import What You Use"
|
|
13
|
+
* - Tier 1 (80%): String analysis for simple templates (~2ms)
|
|
14
|
+
* - Tier 2 (15%): Angular Compiler for complex templates (~15ms)
|
|
15
|
+
* - Tier 3 (5%): Conservative fallback (include all imports)
|
|
16
|
+
*
|
|
17
|
+
* Average overhead: ~15% (vs 30% single-tier approach)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { parseTemplate, InterpolationConfig } from '@angular/compiler';
|
|
21
|
+
import { LRUCache } from 'lru-cache';
|
|
22
|
+
import type { ClassDeclaration, Project } from 'ts-morph';
|
|
23
|
+
import { SelectorMapper } from './selector-mapper.js';
|
|
24
|
+
import { ExternalTemplateLoader } from './external-template-loader.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parsed template result from Angular Compiler.
|
|
28
|
+
* Using 'any' type as @angular/compiler types are complex and internal.
|
|
29
|
+
*/
|
|
30
|
+
type ParsedTemplate = any;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Standard HTML5 elements that should be ignored.
|
|
34
|
+
* These are not Angular components/directives.
|
|
35
|
+
*/
|
|
36
|
+
const STANDARD_HTML_ELEMENTS = new Set([
|
|
37
|
+
'div',
|
|
38
|
+
'span',
|
|
39
|
+
'p',
|
|
40
|
+
'a',
|
|
41
|
+
'button',
|
|
42
|
+
'input',
|
|
43
|
+
'select',
|
|
44
|
+
'textarea',
|
|
45
|
+
'label',
|
|
46
|
+
'form',
|
|
47
|
+
'table',
|
|
48
|
+
'tr',
|
|
49
|
+
'td',
|
|
50
|
+
'th',
|
|
51
|
+
'thead',
|
|
52
|
+
'tbody',
|
|
53
|
+
'ul',
|
|
54
|
+
'ol',
|
|
55
|
+
'li',
|
|
56
|
+
'h1',
|
|
57
|
+
'h2',
|
|
58
|
+
'h3',
|
|
59
|
+
'h4',
|
|
60
|
+
'h5',
|
|
61
|
+
'h6',
|
|
62
|
+
'img',
|
|
63
|
+
'video',
|
|
64
|
+
'audio',
|
|
65
|
+
'canvas',
|
|
66
|
+
'svg',
|
|
67
|
+
'path',
|
|
68
|
+
'section',
|
|
69
|
+
'article',
|
|
70
|
+
'header',
|
|
71
|
+
'footer',
|
|
72
|
+
'nav',
|
|
73
|
+
'main',
|
|
74
|
+
'aside',
|
|
75
|
+
'figure',
|
|
76
|
+
'figcaption',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* CommonModule pipes that are always available.
|
|
81
|
+
*/
|
|
82
|
+
const COMMON_MODULE_PIPES = new Set([
|
|
83
|
+
'AsyncPipe',
|
|
84
|
+
'CurrencyPipe',
|
|
85
|
+
'DatePipe',
|
|
86
|
+
'DecimalPipe',
|
|
87
|
+
'I18nPluralPipe',
|
|
88
|
+
'I18nSelectPipe',
|
|
89
|
+
'JsonPipe',
|
|
90
|
+
'KeyValuePipe',
|
|
91
|
+
'LowerCasePipe',
|
|
92
|
+
'PercentPipe',
|
|
93
|
+
'SlicePipe',
|
|
94
|
+
'TitleCasePipe',
|
|
95
|
+
'UpperCasePipe',
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Template Usage Analyzer with Three-Tier Strategy.
|
|
100
|
+
*
|
|
101
|
+
* Analyzes Angular templates to determine which imports are actually used.
|
|
102
|
+
* Uses a progressive enhancement approach for optimal performance.
|
|
103
|
+
*/
|
|
104
|
+
export class TemplateUsageAnalyzer {
|
|
105
|
+
private readonly templateParseCache = new LRUCache<string, ParsedTemplate>({
|
|
106
|
+
max: 500, // Cache 500 templates
|
|
107
|
+
ttl: 1000 * 60 * 5, // 5 minutes
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
private readonly selectorMapper = new SelectorMapper();
|
|
111
|
+
private readonly templateLoader = new ExternalTemplateLoader();
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Analyze template and return ONLY what's actually used.
|
|
115
|
+
*
|
|
116
|
+
* Uses three-tier strategy for optimal performance:
|
|
117
|
+
* - Tier 1: Fast path for simple templates (80% of cases, ~2ms)
|
|
118
|
+
* - Tier 2: Angular Compiler for complex templates (15% of cases, ~15ms)
|
|
119
|
+
* - Tier 3: Conservative fallback (5% of cases, include all imports)
|
|
120
|
+
*
|
|
121
|
+
* @param component - Component class declaration
|
|
122
|
+
* @param templateContent - Template HTML content
|
|
123
|
+
* @param availableImports - All imports from parent NgModule
|
|
124
|
+
* @param project - ts-morph Project (for selector mapping)
|
|
125
|
+
* @returns Array of actually-used import names
|
|
126
|
+
*/
|
|
127
|
+
analyzeTemplateUsage(
|
|
128
|
+
component: ClassDeclaration,
|
|
129
|
+
templateContent: string,
|
|
130
|
+
availableImports: string[],
|
|
131
|
+
project: Project,
|
|
132
|
+
): string[] {
|
|
133
|
+
// TIER 1: Fast path for simple templates (80% of cases)
|
|
134
|
+
if (this.isSimpleTemplate(templateContent)) {
|
|
135
|
+
console.info(` Using fast path analysis (simple template)`);
|
|
136
|
+
return this.fastPathAnalysis(templateContent, availableImports, project);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// TIER 2: Angular compiler for complex templates (15% of cases)
|
|
140
|
+
try {
|
|
141
|
+
console.info(` Using Angular compiler analysis (complex template)`);
|
|
142
|
+
|
|
143
|
+
// Check cache first
|
|
144
|
+
const cacheKey = this.getCacheKey(component, templateContent);
|
|
145
|
+
const cached = this.templateParseCache.get(cacheKey);
|
|
146
|
+
if (cached) {
|
|
147
|
+
console.info(` Cache hit for ${component.getName()}`);
|
|
148
|
+
return this.extractImportsFromParsed(cached, availableImports, project);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Parse with Angular compiler
|
|
152
|
+
const parsed = parseTemplate(templateContent, 'component.html', {
|
|
153
|
+
preserveWhitespaces: false,
|
|
154
|
+
interpolationConfig: InterpolationConfig.fromArray(['{{', '}}']),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Cache result
|
|
158
|
+
this.templateParseCache.set(cacheKey, parsed);
|
|
159
|
+
|
|
160
|
+
// Extract imports from AST
|
|
161
|
+
return this.extractImportsFromParsed(parsed, availableImports, project);
|
|
162
|
+
} catch (error: unknown) {
|
|
163
|
+
// TIER 3: Conservative fallback (5% of cases)
|
|
164
|
+
const errorMessage =
|
|
165
|
+
error instanceof Error ? error.message : String(error);
|
|
166
|
+
console.warn(
|
|
167
|
+
` Template parsing failed for ${component.getName()}, using conservative strategy`,
|
|
168
|
+
);
|
|
169
|
+
console.warn(` Error: ${errorMessage}`);
|
|
170
|
+
return this.conservativeFallback(availableImports);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Analyze external template (from templateUrl) and return ONLY what's used.
|
|
176
|
+
*
|
|
177
|
+
* Loads the external template file and analyzes it using the three-tier strategy.
|
|
178
|
+
*
|
|
179
|
+
* @param component - Component class declaration
|
|
180
|
+
* @param templateUrl - Template URL from `` metadata (e.g., './user.component.html')
|
|
181
|
+
* @param componentPath - Absolute path to the component file
|
|
182
|
+
* @param availableImports - All imports from parent NgModule
|
|
183
|
+
* @param project - ts-morph Project (for selector mapping)
|
|
184
|
+
* @returns Array of actually-used import names
|
|
185
|
+
*/
|
|
186
|
+
async analyzeExternalTemplate(
|
|
187
|
+
component: ClassDeclaration,
|
|
188
|
+
templateUrl: string,
|
|
189
|
+
componentPath: string,
|
|
190
|
+
availableImports: string[],
|
|
191
|
+
project: Project,
|
|
192
|
+
): Promise<string[]> {
|
|
193
|
+
console.info(` Loading external template: ${templateUrl}`);
|
|
194
|
+
|
|
195
|
+
const componentFilePath =
|
|
196
|
+
component.getSourceFile().getFilePath() || componentPath;
|
|
197
|
+
|
|
198
|
+
// Load external template
|
|
199
|
+
const templateContent = await this.templateLoader.loadExternalTemplate(
|
|
200
|
+
templateUrl,
|
|
201
|
+
componentFilePath,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// If loading failed, use conservative fallback
|
|
205
|
+
if (!templateContent) {
|
|
206
|
+
console.warn(
|
|
207
|
+
` External template empty or failed to load, using conservative strategy`,
|
|
208
|
+
);
|
|
209
|
+
return this.conservativeFallback(availableImports);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Analyze the loaded template
|
|
213
|
+
return this.analyzeTemplateUsage(
|
|
214
|
+
component,
|
|
215
|
+
templateContent,
|
|
216
|
+
availableImports,
|
|
217
|
+
project,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Determine if template is simple (can use fast path).
|
|
223
|
+
*
|
|
224
|
+
* Simple templates:
|
|
225
|
+
* - No interpolation: {{ }}
|
|
226
|
+
* - No structural directives: *ngIf, *ngFor, *ngSwitch
|
|
227
|
+
* - No property/event bindings: [], ()
|
|
228
|
+
* - Short content (< 500 chars)
|
|
229
|
+
*/
|
|
230
|
+
private isSimpleTemplate(content: string): boolean {
|
|
231
|
+
return (
|
|
232
|
+
!content.includes('{{') &&
|
|
233
|
+
!content.includes('*ng') &&
|
|
234
|
+
!content.includes('[') &&
|
|
235
|
+
!content.includes('(') &&
|
|
236
|
+
content.length < 500
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Fast path: String-based analysis for simple templates.
|
|
242
|
+
*
|
|
243
|
+
* Uses regex to extract element selectors only.
|
|
244
|
+
* ~2ms per template (very fast).
|
|
245
|
+
*/
|
|
246
|
+
private fastPathAnalysis(
|
|
247
|
+
templateContent: string,
|
|
248
|
+
availableImports: string[],
|
|
249
|
+
project: Project,
|
|
250
|
+
): string[] {
|
|
251
|
+
const requiredImports = new Set<string>();
|
|
252
|
+
|
|
253
|
+
// Always include CommonModule for basic directives
|
|
254
|
+
requiredImports.add('CommonModule');
|
|
255
|
+
|
|
256
|
+
// Extract element selectors: <app-user></app-user>
|
|
257
|
+
const elementRegex = /<([a-z][a-z0-9-]*)/gi;
|
|
258
|
+
const matches = templateContent.matchAll(elementRegex);
|
|
259
|
+
|
|
260
|
+
for (const match of matches) {
|
|
261
|
+
const selector = match[1];
|
|
262
|
+
|
|
263
|
+
// Skip if selector is undefined or empty
|
|
264
|
+
if (!selector) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Skip standard HTML elements
|
|
269
|
+
if (this.isStandardHtmlElement(selector)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Map selector → class name via Phase 3 selector map
|
|
274
|
+
const className = this.mapSelectorToClassName(
|
|
275
|
+
selector,
|
|
276
|
+
availableImports,
|
|
277
|
+
project,
|
|
278
|
+
);
|
|
279
|
+
if (className) {
|
|
280
|
+
requiredImports.add(className);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return Array.from(requiredImports);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Angular compiler-based analysis for complex templates.
|
|
289
|
+
*
|
|
290
|
+
* Parses template AST and extracts:
|
|
291
|
+
* - Element nodes (components)
|
|
292
|
+
* - Pipe nodes (pipes)
|
|
293
|
+
* - Attribute directives
|
|
294
|
+
* - Structural directives
|
|
295
|
+
*
|
|
296
|
+
* ~15ms per template (slower but accurate).
|
|
297
|
+
*/
|
|
298
|
+
private extractImportsFromParsed(
|
|
299
|
+
parsed: ParsedTemplate,
|
|
300
|
+
availableImports: string[],
|
|
301
|
+
project: Project,
|
|
302
|
+
): string[] {
|
|
303
|
+
const requiredImports = new Set<string>();
|
|
304
|
+
|
|
305
|
+
// Always include CommonModule
|
|
306
|
+
requiredImports.add('CommonModule');
|
|
307
|
+
|
|
308
|
+
// Extract from AST nodes
|
|
309
|
+
const elements = this.extractElementsFromAST(parsed.nodes);
|
|
310
|
+
const pipes = this.extractPipesFromAST(parsed.nodes);
|
|
311
|
+
const directives = this.extractDirectivesFromAST(parsed.nodes);
|
|
312
|
+
|
|
313
|
+
// Map selectors to class names
|
|
314
|
+
for (const selector of elements) {
|
|
315
|
+
const className = this.mapSelectorToClassName(
|
|
316
|
+
selector,
|
|
317
|
+
availableImports,
|
|
318
|
+
project,
|
|
319
|
+
);
|
|
320
|
+
if (className) {
|
|
321
|
+
requiredImports.add(className);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Map pipes to class names
|
|
326
|
+
for (const pipeName of pipes) {
|
|
327
|
+
const className = this.mapPipeToClassName(pipeName, availableImports);
|
|
328
|
+
if (className) {
|
|
329
|
+
requiredImports.add(className);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Map directives to class names
|
|
334
|
+
for (const directiveName of directives) {
|
|
335
|
+
const className = this.mapDirectiveToClassName(
|
|
336
|
+
directiveName,
|
|
337
|
+
availableImports,
|
|
338
|
+
);
|
|
339
|
+
if (className) {
|
|
340
|
+
requiredImports.add(className);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return Array.from(requiredImports);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* ⭐ CRITICAL: Selector-to-Class mapping using Phase 3 selector map.
|
|
349
|
+
*
|
|
350
|
+
* Reuses Phase 3's cached selector map for O(1) lookup.
|
|
351
|
+
*
|
|
352
|
+
* Example: 'app-user-profile' → 'UserProfileComponent'
|
|
353
|
+
*/
|
|
354
|
+
private mapSelectorToClassName(
|
|
355
|
+
selector: string,
|
|
356
|
+
availableImports: string[],
|
|
357
|
+
project: Project,
|
|
358
|
+
): string | undefined {
|
|
359
|
+
// Reuse Phase 3's selector map caching (O(1) lookup)
|
|
360
|
+
const className = this.selectorMapper.findComponentBySelector(
|
|
361
|
+
project,
|
|
362
|
+
selector,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (!className) {
|
|
366
|
+
console.warn(` ⚠️ Unknown selector: ${selector}`);
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Verify it's in availableImports
|
|
371
|
+
if (!availableImports.includes(className)) {
|
|
372
|
+
console.info(
|
|
373
|
+
` Selector ${selector} → ${className} not in available imports (external component)`,
|
|
374
|
+
);
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return className;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Map pipe name to class name.
|
|
383
|
+
*
|
|
384
|
+
* Examples:
|
|
385
|
+
* - 'ngxMask' → 'NgxMaskPipe'
|
|
386
|
+
* - 'currency' → 'CurrencyPipe' (CommonModule)
|
|
387
|
+
* - 'uppercase' → 'UpperCasePipe' (CommonModule)
|
|
388
|
+
*/
|
|
389
|
+
private mapPipeToClassName(
|
|
390
|
+
pipeName: string,
|
|
391
|
+
availableImports: string[],
|
|
392
|
+
): string | undefined {
|
|
393
|
+
// Try exact match first
|
|
394
|
+
if (availableImports.includes(pipeName)) {
|
|
395
|
+
return pipeName;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Try with 'Pipe' suffix
|
|
399
|
+
const withSuffix = `${pipeName.charAt(0).toUpperCase()}${pipeName.slice(1)}Pipe`;
|
|
400
|
+
if (availableImports.includes(withSuffix)) {
|
|
401
|
+
return withSuffix;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// CommonModule pipes (always available)
|
|
405
|
+
if (COMMON_MODULE_PIPES.has(withSuffix)) {
|
|
406
|
+
return 'CommonModule'; // Pipe comes from CommonModule
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
console.warn(` Unknown pipe: ${pipeName}`);
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Map directive name to class name.
|
|
415
|
+
*
|
|
416
|
+
* Examples:
|
|
417
|
+
* - 'ngModel' → 'NgModel'
|
|
418
|
+
* - 'ngIf' → 'NgIf' (CommonModule)
|
|
419
|
+
* - 'myDirective' → 'MyDirectiveDirective'
|
|
420
|
+
*/
|
|
421
|
+
private mapDirectiveToClassName(
|
|
422
|
+
directiveName: string,
|
|
423
|
+
availableImports: string[],
|
|
424
|
+
): string | undefined {
|
|
425
|
+
// Try exact match first
|
|
426
|
+
if (availableImports.includes(directiveName)) {
|
|
427
|
+
return directiveName;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Try with 'Directive' suffix
|
|
431
|
+
const withSuffix = `${directiveName.charAt(0).toUpperCase()}${directiveName.slice(1)}Directive`;
|
|
432
|
+
if (availableImports.includes(withSuffix)) {
|
|
433
|
+
return withSuffix;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// CommonModule directives (structural)
|
|
437
|
+
const commonDirectives = [
|
|
438
|
+
'NgIf',
|
|
439
|
+
'NgFor',
|
|
440
|
+
'NgSwitch',
|
|
441
|
+
'NgSwitchCase',
|
|
442
|
+
'NgSwitchDefault',
|
|
443
|
+
];
|
|
444
|
+
if (commonDirectives.includes(withSuffix.replace('Directive', ''))) {
|
|
445
|
+
return 'CommonModule';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.warn(` Unknown directive: ${directiveName}`);
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Conservative fallback: Include all imports (safe).
|
|
454
|
+
*
|
|
455
|
+
* Used when:
|
|
456
|
+
* - Template parsing fails
|
|
457
|
+
* - Template is invalid
|
|
458
|
+
* - Compilation errors
|
|
459
|
+
*
|
|
460
|
+
* Better to include too much than break the application.
|
|
461
|
+
*/
|
|
462
|
+
private conservativeFallback(availableImports: string[]): string[] {
|
|
463
|
+
console.info(
|
|
464
|
+
` Conservative fallback: including all ${availableImports.length} imports`,
|
|
465
|
+
);
|
|
466
|
+
return availableImports;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Check if element is a standard HTML element.
|
|
471
|
+
*/
|
|
472
|
+
private isStandardHtmlElement(tagName: string): boolean {
|
|
473
|
+
return STANDARD_HTML_ELEMENTS.has(tagName.toLowerCase());
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Generate cache key for template parsing.
|
|
478
|
+
*/
|
|
479
|
+
private getCacheKey(
|
|
480
|
+
component: ClassDeclaration,
|
|
481
|
+
templateContent: string,
|
|
482
|
+
): string {
|
|
483
|
+
const componentName = component.getName() ?? 'unknown';
|
|
484
|
+
const contentHash = this.simpleHash(templateContent);
|
|
485
|
+
return `${componentName}-${contentHash}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Simple hash function for cache keys.
|
|
490
|
+
*/
|
|
491
|
+
private simpleHash(str: string): string {
|
|
492
|
+
let hash = 0;
|
|
493
|
+
for (let i = 0; i < str.length; i++) {
|
|
494
|
+
const char = str.codePointAt(i)!;
|
|
495
|
+
hash = (hash << 5) - hash + char;
|
|
496
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
497
|
+
}
|
|
498
|
+
return hash.toString(36);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Extract element selectors from Angular Compiler AST.
|
|
503
|
+
*/
|
|
504
|
+
private extractElementsFromAST(nodes: unknown[]): string[] {
|
|
505
|
+
const elements: string[] = [];
|
|
506
|
+
|
|
507
|
+
const traverse = (node: any): void => {
|
|
508
|
+
if (!node) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Element node
|
|
513
|
+
if (node.name && typeof node.name === 'string') {
|
|
514
|
+
if (!this.isStandardHtmlElement(node.name)) {
|
|
515
|
+
elements.push(node.name);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Traverse children
|
|
520
|
+
if (Array.isArray(node.children)) {
|
|
521
|
+
node.children.forEach(traverse);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
if (Array.isArray(nodes)) {
|
|
526
|
+
nodes.forEach(traverse);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return elements;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Extract pipe names from Angular Compiler AST.
|
|
534
|
+
*/
|
|
535
|
+
private extractPipesFromAST(nodes: unknown[]): string[] {
|
|
536
|
+
const pipes: string[] = [];
|
|
537
|
+
|
|
538
|
+
const traverse = (node: any): void => {
|
|
539
|
+
if (!node) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Pipe expression node
|
|
544
|
+
if (node.name && node.exp && typeof node.name === 'string') {
|
|
545
|
+
pipes.push(node.name);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Traverse children and expressions
|
|
549
|
+
if (Array.isArray(node.children)) {
|
|
550
|
+
node.children.forEach(traverse);
|
|
551
|
+
}
|
|
552
|
+
if (node.exp) {
|
|
553
|
+
traverse(node.exp);
|
|
554
|
+
}
|
|
555
|
+
if (node.args) {
|
|
556
|
+
node.args.forEach(traverse);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
if (Array.isArray(nodes)) {
|
|
561
|
+
nodes.forEach(traverse);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return pipes;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Extract directive names from Angular Compiler AST.
|
|
569
|
+
*/
|
|
570
|
+
private extractDirectivesFromAST(nodes: unknown[]): string[] {
|
|
571
|
+
const directives: string[] = [];
|
|
572
|
+
|
|
573
|
+
const traverse = (node: any): void => {
|
|
574
|
+
if (!node) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Structural directive (*ngIf, *ngFor, etc.)
|
|
579
|
+
if (node.templateAttrs && Array.isArray(node.templateAttrs)) {
|
|
580
|
+
node.templateAttrs.forEach((attr: any) => {
|
|
581
|
+
if (attr.name && typeof attr.name === 'string') {
|
|
582
|
+
directives.push(attr.name);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Attribute directive
|
|
588
|
+
if (node.attributes && Array.isArray(node.attributes)) {
|
|
589
|
+
node.attributes.forEach((attr: any) => {
|
|
590
|
+
if (attr.name && typeof attr.name === 'string') {
|
|
591
|
+
// Skip standard HTML attributes
|
|
592
|
+
if (
|
|
593
|
+
!this.isStandardHtmlElement(attr.name) &&
|
|
594
|
+
attr.name.includes('[')
|
|
595
|
+
) {
|
|
596
|
+
directives.push(attr.name.replaceAll(/[[\]]/g, ''));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Traverse children
|
|
603
|
+
if (Array.isArray(node.children)) {
|
|
604
|
+
node.children.forEach(traverse);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
if (Array.isArray(nodes)) {
|
|
609
|
+
nodes.forEach(traverse);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return directives;
|
|
613
|
+
}
|
|
614
|
+
}
|