@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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Template Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Dumb Collector - extracts ALL elements from Angular templates.
|
|
5
|
+
* Zero filtering, zero intelligence. Based on the "Trust the Map" principle.
|
|
6
|
+
*
|
|
7
|
+
* Philosophy: "Collect Everything, Filter Later"
|
|
8
|
+
* - No business logic in collectors
|
|
9
|
+
* - Plugins decide what matters
|
|
10
|
+
* - Raw data is more valuable than pre-filtered data
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents a single template usage (element, directive, pipe, etc.).
|
|
15
|
+
*/
|
|
16
|
+
export interface TemplateUsage {
|
|
17
|
+
/**
|
|
18
|
+
* Type of usage.
|
|
19
|
+
*/
|
|
20
|
+
type:
|
|
21
|
+
| 'element'
|
|
22
|
+
| 'directive'
|
|
23
|
+
| 'pipe'
|
|
24
|
+
| 'component'
|
|
25
|
+
| 'structural-directive'
|
|
26
|
+
| 'control-flow';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Name/selector of the element.
|
|
30
|
+
*/
|
|
31
|
+
name: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Position in template (line number).
|
|
35
|
+
*/
|
|
36
|
+
line?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Position in template (column number).
|
|
40
|
+
*/
|
|
41
|
+
column?: number;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Raw HTML/template string of this usage.
|
|
45
|
+
*/
|
|
46
|
+
raw?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Attributes/inputs on this element.
|
|
50
|
+
*/
|
|
51
|
+
attributes?: Record<string, string>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Additional metadata.
|
|
55
|
+
*/
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Template Analyzer - extracts all usages from Angular templates.
|
|
61
|
+
*
|
|
62
|
+
* This is a "Dumb Collector" that doesn't filter or interpret.
|
|
63
|
+
* It just collects everything and lets plugins decide what's important.
|
|
64
|
+
*/
|
|
65
|
+
export class TemplateAnalyzer {
|
|
66
|
+
/**
|
|
67
|
+
* Analyze a template string and extract all usages.
|
|
68
|
+
*
|
|
69
|
+
* @param template - The template string to analyze
|
|
70
|
+
* @returns Array of template usages
|
|
71
|
+
*
|
|
72
|
+
* Philosophy: "Collect Everything, Filter Later"
|
|
73
|
+
* - Extracts ALL elements, attributes, pipes, directives
|
|
74
|
+
* - No filtering, no business logic
|
|
75
|
+
* - Plugins decide what's important using the SymbolMapper
|
|
76
|
+
*/
|
|
77
|
+
analyze(template: string): TemplateUsage[] {
|
|
78
|
+
const usages: TemplateUsage[] = [];
|
|
79
|
+
const lines = template.split('\n');
|
|
80
|
+
|
|
81
|
+
// Extract element tags (e.g., <fixture-child>, <div>, <custom-component>)
|
|
82
|
+
const elementRegex = /<([a-zA-Z][\w-]*)/g;
|
|
83
|
+
|
|
84
|
+
// Extract pipes (e.g., {{ value | pipeName }}, {{ value | pipeName: arg }})
|
|
85
|
+
const pipeRegex = /\|\s*([a-zA-Z]\w*)/g;
|
|
86
|
+
|
|
87
|
+
lines.forEach((line, lineIndex) => {
|
|
88
|
+
// Extract elements
|
|
89
|
+
let match: RegExpExecArray | null;
|
|
90
|
+
while ((match = elementRegex.exec(line)) !== null) {
|
|
91
|
+
const tagName = match[1];
|
|
92
|
+
if (tagName) {
|
|
93
|
+
usages.push({
|
|
94
|
+
type: 'element',
|
|
95
|
+
name: tagName,
|
|
96
|
+
line: lineIndex + 1,
|
|
97
|
+
column: match.index,
|
|
98
|
+
raw: match[0],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Extract attributes - use multiple patterns for better coverage
|
|
104
|
+
|
|
105
|
+
// Pattern 1: Structural directives (*ngIf, *ngFor)
|
|
106
|
+
const structuralDirectiveRegex = /\*([a-zA-Z][\w-]*)/g;
|
|
107
|
+
while ((match = structuralDirectiveRegex.exec(line)) !== null) {
|
|
108
|
+
usages.push({
|
|
109
|
+
type: 'structural-directive',
|
|
110
|
+
name: `*${match[1]}`,
|
|
111
|
+
line: lineIndex + 1,
|
|
112
|
+
column: match.index,
|
|
113
|
+
raw: match[0],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Pattern 2: Angular 17+ control flow (@if, @for, @switch, @empty, @let)
|
|
118
|
+
const controlFlowRegex =
|
|
119
|
+
/@(if|else if|for|switch|case|default|empty|let)\b/g;
|
|
120
|
+
while ((match = controlFlowRegex.exec(line)) !== null) {
|
|
121
|
+
usages.push({
|
|
122
|
+
type: 'control-flow',
|
|
123
|
+
name: match[1]!,
|
|
124
|
+
line: lineIndex + 1,
|
|
125
|
+
column: match.index,
|
|
126
|
+
raw: match[0],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Pattern 3: Property bindings ([propName]="value")
|
|
131
|
+
const propertyBindingRegex = /\[([a-zA-Z][\w-]*)\]/g;
|
|
132
|
+
while ((match = propertyBindingRegex.exec(line)) !== null) {
|
|
133
|
+
usages.push({
|
|
134
|
+
type: 'directive',
|
|
135
|
+
name: `[${match[1]}]`,
|
|
136
|
+
line: lineIndex + 1,
|
|
137
|
+
column: match.index,
|
|
138
|
+
raw: match[0],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Pattern 3: Bare attribute selectors (fixtureAttribute, ngModel)
|
|
143
|
+
// Look for attribute names that are NOT standard HTML attributes
|
|
144
|
+
const bareAttributeRegex =
|
|
145
|
+
/\s+([a-z][\w-]*(?:[A-Z][\w-]*)+)\s*(?:=|>|\s|$)/g;
|
|
146
|
+
while ((match = bareAttributeRegex.exec(line)) !== null) {
|
|
147
|
+
const attrName = match[1];
|
|
148
|
+
// Only add if it looks like a directive (has camelCase or kebab-case)
|
|
149
|
+
if (attrName && (attrName.includes('-') || /[A-Z]/.test(attrName))) {
|
|
150
|
+
usages.push({
|
|
151
|
+
type: 'directive',
|
|
152
|
+
name: attrName,
|
|
153
|
+
line: lineIndex + 1,
|
|
154
|
+
column: match.index,
|
|
155
|
+
raw: match[0].trim(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Extract pipes
|
|
161
|
+
const pipeMatches = line.matchAll(pipeRegex);
|
|
162
|
+
for (const pipeMatch of pipeMatches) {
|
|
163
|
+
const pipeName = pipeMatch[1];
|
|
164
|
+
if (pipeName) {
|
|
165
|
+
usages.push({
|
|
166
|
+
type: 'pipe',
|
|
167
|
+
name: pipeName,
|
|
168
|
+
line: lineIndex + 1,
|
|
169
|
+
column: pipeMatch.index,
|
|
170
|
+
raw: pipeMatch[0],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return usages;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract inline template from component decorator.
|
|
181
|
+
*
|
|
182
|
+
* @param decoratorText - The `` decorator text
|
|
183
|
+
* @returns The template string or undefined
|
|
184
|
+
*/
|
|
185
|
+
extractInlineTemplate(decoratorText: string): string | undefined {
|
|
186
|
+
// Match template: `...` or template: '...' (including multiline)
|
|
187
|
+
const templateRegex = /template\s*:\s*(['`])([\s\S]*?)\1/;
|
|
188
|
+
const match = templateRegex.exec(decoratorText);
|
|
189
|
+
|
|
190
|
+
if (match) {
|
|
191
|
+
return match[2];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if a template uses a specific selector.
|
|
199
|
+
*
|
|
200
|
+
* @param template - The template string
|
|
201
|
+
* @param selector - The selector to search for
|
|
202
|
+
* @returns True if selector is used
|
|
203
|
+
*/
|
|
204
|
+
usesSelector(template: string, selector: string): boolean {
|
|
205
|
+
const usages = this.analyze(template);
|
|
206
|
+
return usages.some((usage) => usage.name === selector);
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for @angular-modernizer/api
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './public-api.js';
|
|
6
|
+
export * from './transformation/ng-morph-adapter.js';
|
|
7
|
+
export * from './transformation/ngmodule-manager.js';
|
|
8
|
+
export * from './transformation/import-manager.js';
|
|
9
|
+
export * from './analysis/service-pattern-recognizer.js';
|
|
10
|
+
export * from './analysis/service-detector.js';
|
|
11
|
+
export type { ServiceDetectionResult } from './analysis/service-pattern-recognizer.js';
|
|
12
|
+
export * from './analysis/dependency-analyzer.js';
|
|
13
|
+
export * from './analysis/template-analyzer.js';
|
|
14
|
+
export * from './analysis/template-analyzer-usage.js';
|
|
15
|
+
export * from './analysis/selector-mapper.js';
|
|
16
|
+
export * from './analysis/external-template-loader.js';
|
|
17
|
+
export {
|
|
18
|
+
SymbolLocator,
|
|
19
|
+
type SymbolInfo as ProjectSymbolInfo,
|
|
20
|
+
} from './analysis/symbol-locator.js';
|
|
21
|
+
export {
|
|
22
|
+
StackTraceParser,
|
|
23
|
+
type StackFrame,
|
|
24
|
+
} from './investigation/stack-trace-parser.js';
|
|
25
|
+
export {
|
|
26
|
+
UsageFinder,
|
|
27
|
+
type SymbolUsage,
|
|
28
|
+
type UsageType,
|
|
29
|
+
type FindUsagesOptions,
|
|
30
|
+
} from './investigation/usage-finder.js';
|
|
31
|
+
export {
|
|
32
|
+
CallGraphBuilder,
|
|
33
|
+
type CallNode,
|
|
34
|
+
type CallGraphOptions,
|
|
35
|
+
type CallGraphResult,
|
|
36
|
+
} from './investigation/call-graph-builder.js';
|
|
37
|
+
export {
|
|
38
|
+
TypeResolver,
|
|
39
|
+
type ResolvedTypeInfo,
|
|
40
|
+
} from './investigation/type-resolver.js';
|
|
41
|
+
export {
|
|
42
|
+
AngularSymbolFinder,
|
|
43
|
+
type AngularSymbolResult,
|
|
44
|
+
type AngularSymbolType,
|
|
45
|
+
} from './investigation/angular-symbol-finder.js';
|
|
46
|
+
export {
|
|
47
|
+
CodebaseSearcher,
|
|
48
|
+
type SearchResult,
|
|
49
|
+
type SearchOptions,
|
|
50
|
+
type SymbolKind,
|
|
51
|
+
} from './investigation/codebase-searcher.js';
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Angular Symbol Finder
|
|
3
|
+
*
|
|
4
|
+
* Finds Angular symbols (components, directives, pipes, services, guards,
|
|
5
|
+
* interceptors, resolvers, modules, tokens) by selector, template name, class
|
|
6
|
+
* name, or token name. Supports standard Angular decorators as well as
|
|
7
|
+
* ngUWI-specific external decorators (@ExportComponent, @ExportFunction).
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The project must already have the relevant source files added before calling
|
|
11
|
+
* `findSymbol`. Queries are matched case-sensitively for class names, and
|
|
12
|
+
* substring-matched for selectors/pipe names (e.g. searching "app-list" also
|
|
13
|
+
* returns "app-list-item" selectors).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const finder = new AngularSymbolFinder(project);
|
|
18
|
+
* const results = finder.findSymbol('app-user');
|
|
19
|
+
* // results[0] → { file, className, angularType: 'component', selector: 'app-user', metadata: {...} }
|
|
20
|
+
*
|
|
21
|
+
* const pipes = finder.findSymbol('dateFormat', 'pipe');
|
|
22
|
+
* // pipes[0] → { file, className: 'DateFormatPipe', angularType: 'pipe', pipeName: 'dateFormat', metadata: {...} }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { Project, ClassDeclaration, Decorator } from 'ts-morph';
|
|
27
|
+
|
|
28
|
+
/** The Angular symbol type classification. */
|
|
29
|
+
export type AngularSymbolType =
|
|
30
|
+
| 'component'
|
|
31
|
+
| 'directive'
|
|
32
|
+
| 'pipe'
|
|
33
|
+
| 'service'
|
|
34
|
+
| 'guard'
|
|
35
|
+
| 'interceptor'
|
|
36
|
+
| 'resolver'
|
|
37
|
+
| 'module'
|
|
38
|
+
| 'token';
|
|
39
|
+
|
|
40
|
+
/** A resolved Angular symbol. */
|
|
41
|
+
export interface AngularSymbolResult {
|
|
42
|
+
/** Absolute file path containing the class. */
|
|
43
|
+
file: string;
|
|
44
|
+
|
|
45
|
+
/** Class name (e.g. `UserComponent`). */
|
|
46
|
+
className: string;
|
|
47
|
+
|
|
48
|
+
/** Angular symbol type. */
|
|
49
|
+
angularType: AngularSymbolType;
|
|
50
|
+
|
|
51
|
+
/** CSS selector for components and directives (stripped of brackets). */
|
|
52
|
+
selector?: string;
|
|
53
|
+
|
|
54
|
+
/** Template name for pipes (value of `@Pipe({ name: '...' })`). */
|
|
55
|
+
pipeName?: string;
|
|
56
|
+
|
|
57
|
+
/** Raw decorator metadata (key-value pairs extracted from the decorator). */
|
|
58
|
+
metadata: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Decorator names that map to Angular symbol types. */
|
|
62
|
+
const DECORATOR_TYPE_MAP: Record<string, AngularSymbolType> = {
|
|
63
|
+
Component: 'component',
|
|
64
|
+
ExportComponent: 'component',
|
|
65
|
+
Directive: 'directive',
|
|
66
|
+
Pipe: 'pipe',
|
|
67
|
+
Injectable: 'service',
|
|
68
|
+
ExportFunction: 'service',
|
|
69
|
+
NgModule: 'module',
|
|
70
|
+
Guard: 'guard',
|
|
71
|
+
Interceptor: 'interceptor',
|
|
72
|
+
Resolver: 'resolver',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extracts the string value of a named property from a decorator's argument
|
|
77
|
+
* object literal (e.g. `selector: 'app-foo'` → `'app-foo'`).
|
|
78
|
+
*/
|
|
79
|
+
function extractDecoratorProperty(
|
|
80
|
+
decorator: Decorator,
|
|
81
|
+
property: string,
|
|
82
|
+
): string | undefined {
|
|
83
|
+
try {
|
|
84
|
+
const args = decorator.getArguments();
|
|
85
|
+
if (args.length === 0) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const arg = args[0];
|
|
89
|
+
if (arg === undefined) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const text = arg.getText();
|
|
93
|
+
const match = new RegExp(`${property}\\s*:\\s*['"\`]([^'"\`]+)['"\`]`).exec(
|
|
94
|
+
text,
|
|
95
|
+
);
|
|
96
|
+
return match?.[1];
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extracts all string-valued properties from a decorator's first argument
|
|
104
|
+
* as a flat `Record<string, string>`.
|
|
105
|
+
*/
|
|
106
|
+
function extractDecoratorMetadata(
|
|
107
|
+
decorator: Decorator,
|
|
108
|
+
): Record<string, string> {
|
|
109
|
+
const metadata: Record<string, string> = {};
|
|
110
|
+
try {
|
|
111
|
+
const args = decorator.getArguments();
|
|
112
|
+
const firstArg = args[0];
|
|
113
|
+
if (args.length === 0 || firstArg === undefined) {
|
|
114
|
+
return metadata;
|
|
115
|
+
}
|
|
116
|
+
const text = firstArg.getText();
|
|
117
|
+
const pattern = /(\w+)\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
118
|
+
let match: RegExpExecArray | null;
|
|
119
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
120
|
+
const key = match[1];
|
|
121
|
+
const value = match[2];
|
|
122
|
+
if (key !== undefined && value !== undefined) {
|
|
123
|
+
metadata[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
return metadata;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Strips attribute-selector brackets from a selector string.
|
|
134
|
+
* `[appHighlight]` → `appHighlight`; `app-user` → `app-user`.
|
|
135
|
+
*/
|
|
136
|
+
function normalizeSelector(raw: string): string {
|
|
137
|
+
return raw.replace(/^\[(.+)\]$/, '$1');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns `true` when the query matches the given selector (substring) or
|
|
142
|
+
* class name (exact).
|
|
143
|
+
*/
|
|
144
|
+
function matchesQuery(
|
|
145
|
+
query: string,
|
|
146
|
+
className: string,
|
|
147
|
+
selector: string | undefined,
|
|
148
|
+
pipeName: string | undefined,
|
|
149
|
+
): boolean {
|
|
150
|
+
if (className === query) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (selector?.includes(query)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (pipeName !== undefined && pipeName === query) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Finds Angular symbols across all source files in a ts-morph `Project`.
|
|
164
|
+
*/
|
|
165
|
+
export class AngularSymbolFinder {
|
|
166
|
+
constructor(private readonly project: Project) {}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Find Angular symbols matching `query`.
|
|
170
|
+
*
|
|
171
|
+
* @param query - Selector (e.g. `app-user`, `appHighlight`), pipe name
|
|
172
|
+
* (e.g. `dateFormat`), class name (e.g. `UserComponent`), or partial
|
|
173
|
+
* selector substring.
|
|
174
|
+
* @param type - Optional filter; when provided only results of the given type
|
|
175
|
+
* are returned.
|
|
176
|
+
* @returns Array of matching `AngularSymbolResult` objects, empty when none
|
|
177
|
+
* are found.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* // By selector
|
|
182
|
+
* finder.findSymbol('app-user');
|
|
183
|
+
*
|
|
184
|
+
* // By class name with type filter
|
|
185
|
+
* finder.findSymbol('UserService', 'service');
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
findSymbol(query: string, type?: AngularSymbolType): AngularSymbolResult[] {
|
|
189
|
+
const results: AngularSymbolResult[] = [];
|
|
190
|
+
|
|
191
|
+
for (const sourceFile of this.project.getSourceFiles()) {
|
|
192
|
+
for (const cls of sourceFile.getClasses()) {
|
|
193
|
+
const result = this.classifyClass(cls);
|
|
194
|
+
if (result === null) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (type !== undefined && result.angularType !== type) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
matchesQuery(
|
|
202
|
+
query,
|
|
203
|
+
result.className,
|
|
204
|
+
result.selector,
|
|
205
|
+
result.pipeName,
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
results.push(result);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return results;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Attempts to classify a class declaration as an Angular symbol.
|
|
218
|
+
* Returns `null` when the class carries no recognised Angular decorator.
|
|
219
|
+
*/
|
|
220
|
+
private classifyClass(cls: ClassDeclaration): AngularSymbolResult | null {
|
|
221
|
+
for (const decorator of cls.getDecorators()) {
|
|
222
|
+
const decoratorName = decorator.getName();
|
|
223
|
+
const angularType = DECORATOR_TYPE_MAP[decoratorName];
|
|
224
|
+
if (angularType === undefined) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const className = cls.getName() ?? '(anonymous)';
|
|
229
|
+
const file = cls.getSourceFile().getFilePath();
|
|
230
|
+
const metadata = extractDecoratorMetadata(decorator);
|
|
231
|
+
|
|
232
|
+
let selector: string | undefined;
|
|
233
|
+
let pipeName: string | undefined;
|
|
234
|
+
|
|
235
|
+
if (angularType === 'component' || angularType === 'directive') {
|
|
236
|
+
const rawSelector = extractDecoratorProperty(decorator, 'selector');
|
|
237
|
+
if (rawSelector !== undefined) {
|
|
238
|
+
selector = normalizeSelector(rawSelector);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (angularType === 'pipe') {
|
|
243
|
+
pipeName = extractDecoratorProperty(decorator, 'name');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result: AngularSymbolResult = {
|
|
247
|
+
file,
|
|
248
|
+
className,
|
|
249
|
+
angularType,
|
|
250
|
+
metadata,
|
|
251
|
+
};
|
|
252
|
+
if (selector !== undefined) {
|
|
253
|
+
result.selector = selector;
|
|
254
|
+
}
|
|
255
|
+
if (pipeName !== undefined) {
|
|
256
|
+
result.pipeName = pipeName;
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|