@eventra_dev/eventra-cli 0.1.33 → 0.1.34
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/dist/engine/callResolver.js +32 -131
- package/dist/engine/languageService.js +7 -38
- package/dist/engine/resolver.js +88 -77
- package/dist/engine/scanner.js +123 -201
- package/package.json +1 -1
- package/src/engine/callResolver.ts +24 -159
- package/src/engine/languageService.ts +22 -52
- package/src/engine/resolver.ts +99 -120
- package/src/engine/scanner.ts +128 -262
package/src/engine/scanner.ts
CHANGED
|
@@ -1,196 +1,157 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { resolveNodeValue } from "./resolver";
|
|
3
3
|
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
|
-
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
5
4
|
import { resolveExportedSymbol } from "./exportResolver";
|
|
6
5
|
import { EventraConfig, ScanResult } from "../types";
|
|
7
|
-
import { isExternalFile } from "./boundary";
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
function isValidEvent(v: string) {
|
|
8
|
+
return (
|
|
9
|
+
v &&
|
|
10
|
+
v.length > 1 &&
|
|
11
|
+
v.length < 120 &&
|
|
12
|
+
!v.includes(" ") &&
|
|
13
|
+
/^[a-zA-Z0-9._:-]+$/.test(v)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getCallName(expr: ts.Expression): string {
|
|
18
|
+
if (ts.isIdentifier(expr)) return expr.text;
|
|
19
|
+
if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
|
|
13
20
|
return "";
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
function getJsxTagName(tag: ts.JsxTagNameExpression): string {
|
|
17
24
|
if (ts.isIdentifier(tag)) return tag.text;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return tag.name.text;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return "";
|
|
25
|
+
if (ts.isPropertyAccessExpression(tag)) return tag.name.text;
|
|
26
|
+
return tag.getText();
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
function
|
|
27
|
-
if (ts.isIdentifier(
|
|
28
|
-
if (ts.
|
|
29
|
+
function getJsxAttrName(name: ts.JsxAttributeName): string {
|
|
30
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
31
|
+
if (ts.isJsxNamespacedName(name)) return name.name.text;
|
|
29
32
|
return "";
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
export function scanSource(
|
|
36
|
+
source: ts.SourceFile,
|
|
37
|
+
checker: ts.TypeChecker,
|
|
38
|
+
config: EventraConfig,
|
|
39
|
+
visited = new Set<string>(),
|
|
40
|
+
inheritedParamMap?: Map<string, ts.Expression>,
|
|
41
|
+
wrapperCache = new Map<string, string | null>()
|
|
42
|
+
): ScanResult {
|
|
43
|
+
const events = new Set<string>();
|
|
44
|
+
const detectedFunctionWrappers = new Set<string>();
|
|
45
|
+
const detectedComponentWrappers = new Map<string, string>();
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
const map = new Map<string, string>();
|
|
47
|
-
const firstParam = fn.parameters[0];
|
|
48
|
-
if (!firstParam) return map;
|
|
47
|
+
if (visited.has(source.fileName)) {
|
|
48
|
+
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
49
|
+
}
|
|
50
|
+
visited.add(source.fileName);
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const el of firstParam.name.elements) {
|
|
53
|
-
if (!ts.isBindingElement(el)) continue;
|
|
52
|
+
function extractTrackCall(node: ts.CallExpression, paramMap: Map<string, ts.Expression>) {
|
|
53
|
+
const expr = node.expression;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if (ts.isPropertyAccessExpression(expr) && expr.name.text === "track") {
|
|
56
|
+
const arg = node.arguments[0];
|
|
57
|
+
if (!arg) return;
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
const res = resolveNodeValue(arg, checker, paramMap);
|
|
60
|
+
for (const v of res.values) {
|
|
61
|
+
if (isValidEvent(v)) events.add(v);
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
function isWrapperFunction(fn: ts.FunctionLikeDeclaration) {
|
|
67
|
+
let found = false;
|
|
68
|
+
|
|
69
|
+
function walk(n: ts.Node) {
|
|
70
|
+
if (found) return;
|
|
71
|
+
|
|
72
|
+
if (ts.isCallExpression(n)) {
|
|
73
|
+
const expr = n.expression;
|
|
74
|
+
if (ts.isPropertyAccessExpression(expr) && expr.name.text === "track") {
|
|
75
|
+
found = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
ts.forEachChild(n, walk);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (fn.body) walk(fn.body);
|
|
83
|
+
return found;
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
function detectComponentWrapper(tag: ts.JsxTagNameExpression): string | null {
|
|
87
|
+
const tagName = getJsxTagName(tag);
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
fn: ts.FunctionLikeDeclaration,
|
|
72
|
-
checker: ts.TypeChecker
|
|
73
|
-
): string | null {
|
|
74
|
-
const propMap = getComponentPropsMap(fn);
|
|
75
|
-
let result: string | null = null;
|
|
89
|
+
if (wrapperCache.has(tagName)) return wrapperCache.get(tagName)!;
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
91
|
+
let symbol = checker.getSymbolAtLocation(tag);
|
|
92
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
93
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
94
|
+
}
|
|
95
|
+
if (!symbol) {
|
|
96
|
+
wrapperCache.set(tagName, null);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
if (!symbol) return null;
|
|
100
|
+
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
85
101
|
|
|
86
102
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
decl.initializer &&
|
|
91
|
-
ts.isPropertyAccessExpression(decl.initializer)
|
|
92
|
-
) {
|
|
93
|
-
return decl.initializer.name.text;
|
|
94
|
-
}
|
|
103
|
+
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
104
|
+
|
|
105
|
+
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
95
106
|
|
|
96
|
-
// const e = event
|
|
97
107
|
if (
|
|
98
108
|
ts.isVariableDeclaration(decl) &&
|
|
99
109
|
decl.initializer &&
|
|
100
|
-
ts.
|
|
110
|
+
(ts.isArrowFunction(decl.initializer) ||
|
|
111
|
+
ts.isFunctionExpression(decl.initializer))
|
|
101
112
|
) {
|
|
102
|
-
|
|
103
|
-
if (inner) return inner;
|
|
113
|
+
fn = decl.initializer;
|
|
104
114
|
}
|
|
105
|
-
}
|
|
106
115
|
|
|
107
|
-
|
|
108
|
-
}
|
|
116
|
+
if (!fn || !fn.body) continue;
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
if (result) return;
|
|
112
|
-
|
|
113
|
-
if (ts.isCallExpression(node)) {
|
|
114
|
-
const expr = node.expression;
|
|
115
|
-
|
|
116
|
-
const isTrack =
|
|
117
|
-
(ts.isIdentifier(expr) && expr.text === "track") ||
|
|
118
|
-
(ts.isPropertyAccessExpression(expr) &&
|
|
119
|
-
expr.name.text === "track");
|
|
120
|
-
|
|
121
|
-
if (!isTrack) {
|
|
122
|
-
ts.forEachChild(node, visit);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
118
|
+
let prop: string | null = null;
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (ts.isIdentifier(arg)) {
|
|
129
|
-
const prop = resolveIdentifierToProp(arg);
|
|
130
|
-
if (prop) {
|
|
131
|
-
result = prop;
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
120
|
+
function walk(n: ts.Node) {
|
|
121
|
+
if (prop) return;
|
|
135
122
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let current = arg;
|
|
123
|
+
if (ts.isCallExpression(n)) {
|
|
124
|
+
const expr = n.expression;
|
|
139
125
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
126
|
+
if (ts.isPropertyAccessExpression(expr) && expr.name.text === "track") {
|
|
127
|
+
const arg = n.arguments[0];
|
|
128
|
+
if (ts.isPropertyAccessExpression(arg)) {
|
|
129
|
+
prop = arg.name.text;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
143
132
|
}
|
|
144
133
|
}
|
|
134
|
+
ts.forEachChild(n, walk);
|
|
135
|
+
}
|
|
145
136
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const inner = resolveFunctionFromCall(
|
|
149
|
-
arg.expression,
|
|
150
|
-
checker
|
|
151
|
-
);
|
|
137
|
+
walk(fn.body);
|
|
138
|
+
wrapperCache.set(tagName, prop);
|
|
152
139
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
140
|
+
if (prop) {
|
|
141
|
+
detectedComponentWrappers.set(tagName, prop);
|
|
142
|
+
return prop;
|
|
158
143
|
}
|
|
159
144
|
}
|
|
160
145
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (fn.body) visit(fn.body);
|
|
165
|
-
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// MAIN SCANNER
|
|
170
|
-
export function scanSource(
|
|
171
|
-
source: ts.SourceFile,
|
|
172
|
-
checker: ts.TypeChecker,
|
|
173
|
-
config: EventraConfig,
|
|
174
|
-
visited = new Set<string>(),
|
|
175
|
-
inheritedParamMap?: Map<string, ts.Expression>,
|
|
176
|
-
wrapperCache = new Map<string, string | null>()
|
|
177
|
-
): ScanResult {
|
|
178
|
-
const events = new Set<string>();
|
|
179
|
-
const detectedFunctionWrappers = new Set<string>();
|
|
180
|
-
const detectedComponentWrappers = new Map<string, string>();
|
|
181
|
-
|
|
182
|
-
if (visited.has(source.fileName) || visited.size > 200) {
|
|
183
|
-
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
146
|
+
wrapperCache.set(tagName, null);
|
|
147
|
+
return null;
|
|
184
148
|
}
|
|
185
149
|
|
|
186
|
-
visited.add(source.fileName);
|
|
187
|
-
|
|
188
150
|
function visit(node: ts.Node) {
|
|
189
|
-
// JSX
|
|
151
|
+
// JSX
|
|
190
152
|
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
191
153
|
const tagName = getJsxTagName(node.tagName);
|
|
192
154
|
|
|
193
|
-
// skip html
|
|
194
155
|
if (tagName[0] === tagName[0].toLowerCase()) {
|
|
195
156
|
ts.forEachChild(node, visit);
|
|
196
157
|
return;
|
|
@@ -198,48 +159,9 @@ export function scanSource(
|
|
|
198
159
|
|
|
199
160
|
let wrapper = config.wrappers.find(w => w.name === tagName);
|
|
200
161
|
|
|
201
|
-
// AUTO DETECT
|
|
202
162
|
if (!wrapper) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (cached) wrapper = { name: tagName, prop: cached };
|
|
206
|
-
} else {
|
|
207
|
-
let symbol = checker.getSymbolAtLocation(node.tagName);
|
|
208
|
-
|
|
209
|
-
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
210
|
-
symbol = checker.getAliasedSymbol(symbol);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (symbol) {
|
|
214
|
-
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
215
|
-
|
|
216
|
-
for (const decl of symbol.getDeclarations() ?? []) {
|
|
217
|
-
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
218
|
-
|
|
219
|
-
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
ts.isVariableDeclaration(decl) &&
|
|
223
|
-
decl.initializer &&
|
|
224
|
-
(ts.isArrowFunction(decl.initializer) ||
|
|
225
|
-
ts.isFunctionExpression(decl.initializer))
|
|
226
|
-
) {
|
|
227
|
-
fn = decl.initializer;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!fn) continue;
|
|
231
|
-
|
|
232
|
-
const prop = findTrackedPropName(fn, checker);
|
|
233
|
-
wrapperCache.set(tagName, prop ?? null);
|
|
234
|
-
|
|
235
|
-
if (prop) {
|
|
236
|
-
wrapper = { name: tagName, prop };
|
|
237
|
-
detectedComponentWrappers.set(tagName, prop);
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
163
|
+
const prop = detectComponentWrapper(node.tagName);
|
|
164
|
+
if (prop) wrapper = { name: tagName, prop };
|
|
243
165
|
}
|
|
244
166
|
|
|
245
167
|
if (wrapper) {
|
|
@@ -252,16 +174,16 @@ export function scanSource(
|
|
|
252
174
|
if (!attr.initializer) continue;
|
|
253
175
|
|
|
254
176
|
if (ts.isStringLiteral(attr.initializer)) {
|
|
255
|
-
|
|
256
|
-
|
|
177
|
+
if (isValidEvent(attr.initializer.text)) {
|
|
178
|
+
events.add(attr.initializer.text);
|
|
179
|
+
}
|
|
257
180
|
}
|
|
258
181
|
|
|
259
182
|
if (ts.isJsxExpression(attr.initializer)) {
|
|
260
183
|
const expr = attr.initializer.expression;
|
|
261
184
|
if (!expr) continue;
|
|
262
185
|
|
|
263
|
-
const res = resolveNodeValue(expr, checker
|
|
264
|
-
|
|
186
|
+
const res = resolveNodeValue(expr, checker);
|
|
265
187
|
res.values.forEach(v => {
|
|
266
188
|
if (isValidEvent(v)) events.add(v);
|
|
267
189
|
});
|
|
@@ -273,98 +195,42 @@ export function scanSource(
|
|
|
273
195
|
return;
|
|
274
196
|
}
|
|
275
197
|
|
|
276
|
-
//
|
|
198
|
+
// CALL
|
|
277
199
|
if (ts.isCallExpression(node)) {
|
|
278
|
-
|
|
279
|
-
let isTracking = false;
|
|
280
|
-
|
|
281
|
-
const callName = getCallName(node.expression);
|
|
282
|
-
|
|
283
|
-
// direct .track()
|
|
284
|
-
if (
|
|
285
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
286
|
-
node.expression.name.text === "track"
|
|
287
|
-
) {
|
|
288
|
-
isTracking = true;
|
|
289
|
-
trackParamIndex = 0;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// config wrapper
|
|
293
|
-
const manualWrapper = config.functionWrappers.find(
|
|
294
|
-
w => w.name === callName
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
if (manualWrapper) {
|
|
298
|
-
isTracking = true;
|
|
299
|
-
trackParamIndex = 0;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// resolve function → check body
|
|
303
|
-
const resolvedFn = resolveFunctionFromCall(
|
|
304
|
-
node.expression,
|
|
305
|
-
checker
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
if (resolvedFn) {
|
|
309
|
-
const idx = findTrackParamIndex(resolvedFn);
|
|
310
|
-
|
|
311
|
-
if (idx !== null) {
|
|
312
|
-
isTracking = true;
|
|
313
|
-
trackParamIndex = idx;
|
|
314
|
-
|
|
315
|
-
if (!manualWrapper && callName !== "track") {
|
|
316
|
-
detectedFunctionWrappers.add(callName);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!isTracking) {
|
|
322
|
-
ts.forEachChild(node, visit);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
200
|
+
const resolvedFn = resolveFunctionFromCall(node.expression, checker);
|
|
325
201
|
|
|
326
202
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
327
203
|
|
|
328
|
-
if (resolvedFn
|
|
329
|
-
resolvedFn.parameters.forEach((
|
|
204
|
+
if (resolvedFn) {
|
|
205
|
+
resolvedFn.parameters.forEach((p, i) => {
|
|
330
206
|
const arg = node.arguments[i];
|
|
331
207
|
if (!arg) return;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
paramMap.set(param.name.text, arg);
|
|
208
|
+
if (ts.isIdentifier(p.name)) {
|
|
209
|
+
paramMap.set(p.name.text, arg);
|
|
335
210
|
}
|
|
336
211
|
});
|
|
337
212
|
}
|
|
338
213
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
node.arguments[trackParamIndex],
|
|
342
|
-
checker,
|
|
343
|
-
paramMap
|
|
344
|
-
);
|
|
214
|
+
// direct track
|
|
215
|
+
extractTrackCall(node, paramMap);
|
|
345
216
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
217
|
+
// wrapper
|
|
218
|
+
if (resolvedFn && isWrapperFunction(resolvedFn)) {
|
|
219
|
+
const name = getCallName(node.expression);
|
|
220
|
+
if (name && name !== "track") {
|
|
221
|
+
detectedFunctionWrappers.add(name);
|
|
222
|
+
}
|
|
223
|
+
if (resolvedFn.body) {
|
|
224
|
+
visit(resolvedFn.body);
|
|
225
|
+
}
|
|
349
226
|
}
|
|
350
227
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const sf = resolvedFn.getSourceFile();
|
|
354
|
-
|
|
355
|
-
if (!visited.has(sf.fileName)) {
|
|
356
|
-
const res = scanSource(
|
|
357
|
-
sf,
|
|
358
|
-
checker,
|
|
359
|
-
config,
|
|
360
|
-
visited,
|
|
361
|
-
paramMap,
|
|
362
|
-
wrapperCache
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
res.events.forEach(e => events.add(e));
|
|
366
|
-
}
|
|
228
|
+
for (const arg of node.arguments) {
|
|
229
|
+
visit(arg);
|
|
367
230
|
}
|
|
231
|
+
visit(node.expression);
|
|
232
|
+
|
|
233
|
+
return;
|
|
368
234
|
}
|
|
369
235
|
|
|
370
236
|
ts.forEachChild(node, visit);
|