@eventra_dev/eventra-cli 0.1.30 → 0.1.32
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/README.md +0 -1
- package/dist/engine/callResolver.js +56 -25
- package/dist/engine/engine.js +7 -4
- package/dist/engine/languageService.js +36 -11
- package/dist/engine/scanner.js +224 -27
- package/dist/utils/config.js +0 -1
- package/dist/utils/processFile.js +3 -3
- package/dist/utils/universal.js +121 -53
- package/package.json +1 -1
- package/src/engine/callResolver.ts +61 -28
- package/src/engine/engine.ts +26 -7
- package/src/engine/languageService.ts +50 -16
- package/src/engine/scanner.ts +285 -42
- package/src/types.ts +6 -1
- package/src/utils/config.ts +0 -1
- package/src/utils/processFile.ts +3 -3
- package/src/utils/universal.ts +149 -77
- package/tests/run.ts +0 -1
- package/dist/engine/trackingDetector.js +0 -17
- package/src/engine/trackingDetector.ts +0 -17
package/src/engine/scanner.ts
CHANGED
|
@@ -2,9 +2,27 @@ import ts from "typescript";
|
|
|
2
2
|
import { resolveNodeValue } from "./resolver";
|
|
3
3
|
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
4
|
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveExportedSymbol } from "./exportResolver";
|
|
6
|
+
import { EventraConfig, ScanResult } from "../types";
|
|
6
7
|
import { isExternalFile } from "./boundary";
|
|
7
8
|
|
|
9
|
+
// UTILS
|
|
10
|
+
function getJsxAttrName(name: ts.JsxAttributeName): string {
|
|
11
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
12
|
+
if (ts.isJsxNamespacedName(name)) return name.name.text;
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getJsxTagName(tag: ts.JsxTagNameExpression): string {
|
|
17
|
+
if (ts.isIdentifier(tag)) return tag.text;
|
|
18
|
+
|
|
19
|
+
if (ts.isPropertyAccessExpression(tag)) {
|
|
20
|
+
return tag.name.text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
function getCallName(expr: ts.Expression): string {
|
|
9
27
|
if (ts.isIdentifier(expr)) return expr.text;
|
|
10
28
|
if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
|
|
@@ -21,59 +39,290 @@ function isValidEvent(v: string) {
|
|
|
21
39
|
);
|
|
22
40
|
}
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
// COMPONENT ANALYSIS
|
|
43
|
+
function getComponentPropsMap(
|
|
44
|
+
fn: ts.FunctionLikeDeclaration
|
|
45
|
+
): Map<string, string> {
|
|
46
|
+
const map = new Map<string, string>();
|
|
47
|
+
const firstParam = fn.parameters[0];
|
|
48
|
+
if (!firstParam) return map;
|
|
49
|
+
|
|
50
|
+
// ({ event }) => ...
|
|
51
|
+
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
52
|
+
for (const el of firstParam.name.elements) {
|
|
53
|
+
if (!ts.isBindingElement(el)) continue;
|
|
54
|
+
|
|
55
|
+
const propName =
|
|
56
|
+
el.propertyName?.getText() || el.name.getText();
|
|
57
|
+
|
|
58
|
+
map.set(el.name.getText(), propName);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// (props) => props.event
|
|
63
|
+
if (ts.isIdentifier(firstParam.name)) {
|
|
64
|
+
map.set(firstParam.name.text, "*");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findTrackedPropName(
|
|
71
|
+
fn: ts.FunctionLikeDeclaration,
|
|
72
|
+
checker: ts.TypeChecker
|
|
73
|
+
): string | null {
|
|
74
|
+
const propMap = getComponentPropsMap(fn);
|
|
75
|
+
let result: string | null = null;
|
|
76
|
+
|
|
77
|
+
function resolveIdentifierToProp(
|
|
78
|
+
node: ts.Identifier
|
|
79
|
+
): string | null {
|
|
80
|
+
const mapped = propMap.get(node.text);
|
|
81
|
+
if (mapped && mapped !== "*") return mapped;
|
|
82
|
+
|
|
83
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
84
|
+
if (!symbol) return null;
|
|
85
|
+
|
|
86
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
87
|
+
// const e = props.event
|
|
88
|
+
if (
|
|
89
|
+
ts.isVariableDeclaration(decl) &&
|
|
90
|
+
decl.initializer &&
|
|
91
|
+
ts.isPropertyAccessExpression(decl.initializer)
|
|
92
|
+
) {
|
|
93
|
+
return decl.initializer.name.text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// const e = event
|
|
97
|
+
if (
|
|
98
|
+
ts.isVariableDeclaration(decl) &&
|
|
99
|
+
decl.initializer &&
|
|
100
|
+
ts.isIdentifier(decl.initializer)
|
|
101
|
+
) {
|
|
102
|
+
const inner = propMap.get(decl.initializer.text);
|
|
103
|
+
if (inner) return inner;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function visit(node: ts.Node) {
|
|
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
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const arg of node.arguments) {
|
|
127
|
+
// track(event)
|
|
128
|
+
if (ts.isIdentifier(arg)) {
|
|
129
|
+
const prop = resolveIdentifierToProp(arg);
|
|
130
|
+
if (prop) {
|
|
131
|
+
result = prop;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// track(props.a.b.event)
|
|
137
|
+
if (ts.isPropertyAccessExpression(arg)) {
|
|
138
|
+
let current = arg;
|
|
139
|
+
|
|
140
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
141
|
+
result = current.name.text;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// track(fn())
|
|
147
|
+
if (ts.isCallExpression(arg)) {
|
|
148
|
+
const inner = resolveFunctionFromCall(
|
|
149
|
+
arg.expression,
|
|
150
|
+
checker
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (inner?.body) {
|
|
154
|
+
visit(inner.body);
|
|
155
|
+
if (result) return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ts.forEachChild(node, visit);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (fn.body) visit(fn.body);
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
29
168
|
|
|
169
|
+
// MAIN SCANNER
|
|
30
170
|
export function scanSource(
|
|
31
171
|
source: ts.SourceFile,
|
|
32
172
|
checker: ts.TypeChecker,
|
|
33
173
|
config: EventraConfig,
|
|
34
174
|
visited = new Set<string>(),
|
|
35
|
-
inheritedParamMap?: Map<string, ts.Expression
|
|
175
|
+
inheritedParamMap?: Map<string, ts.Expression>,
|
|
176
|
+
wrapperCache = new Map<string, string | null>()
|
|
36
177
|
): ScanResult {
|
|
37
178
|
const events = new Set<string>();
|
|
38
179
|
const detectedFunctionWrappers = new Set<string>();
|
|
39
180
|
const detectedComponentWrappers = new Map<string, string>();
|
|
40
181
|
|
|
41
|
-
if (visited.
|
|
182
|
+
if (visited.has(source.fileName) || visited.size > 200) {
|
|
42
183
|
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
43
184
|
}
|
|
44
185
|
|
|
45
186
|
visited.add(source.fileName);
|
|
46
187
|
|
|
47
188
|
function visit(node: ts.Node) {
|
|
189
|
+
// JSX WRAPPERS
|
|
190
|
+
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
191
|
+
const tagName = getJsxTagName(node.tagName);
|
|
192
|
+
|
|
193
|
+
// skip html
|
|
194
|
+
if (tagName[0] === tagName[0].toLowerCase()) {
|
|
195
|
+
ts.forEachChild(node, visit);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let wrapper = config.wrappers.find(w => w.name === tagName);
|
|
200
|
+
|
|
201
|
+
// AUTO DETECT
|
|
202
|
+
if (!wrapper) {
|
|
203
|
+
if (wrapperCache.has(tagName)) {
|
|
204
|
+
const cached = wrapperCache.get(tagName);
|
|
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
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (wrapper) {
|
|
246
|
+
for (const attr of node.attributes.properties) {
|
|
247
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
248
|
+
|
|
249
|
+
const name = getJsxAttrName(attr.name);
|
|
250
|
+
if (name !== wrapper.prop) continue;
|
|
251
|
+
|
|
252
|
+
if (!attr.initializer) continue;
|
|
253
|
+
|
|
254
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
255
|
+
const v = attr.initializer.text;
|
|
256
|
+
if (isValidEvent(v)) events.add(v);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (ts.isJsxExpression(attr.initializer)) {
|
|
260
|
+
const expr = attr.initializer.expression;
|
|
261
|
+
if (!expr) continue;
|
|
262
|
+
|
|
263
|
+
const res = resolveNodeValue(expr, checker, inheritedParamMap);
|
|
264
|
+
|
|
265
|
+
res.values.forEach(v => {
|
|
266
|
+
if (isValidEvent(v)) events.add(v);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
ts.forEachChild(node, visit);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// FUNCTION CALLS
|
|
48
277
|
if (ts.isCallExpression(node)) {
|
|
49
|
-
|
|
278
|
+
let trackParamIndex: number | null = null;
|
|
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
|
+
}
|
|
50
291
|
|
|
51
|
-
|
|
52
|
-
|
|
292
|
+
// config wrapper
|
|
293
|
+
const manualWrapper = config.functionWrappers.find(
|
|
294
|
+
w => w.name === callName
|
|
53
295
|
);
|
|
54
296
|
|
|
55
|
-
|
|
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);
|
|
56
310
|
|
|
57
|
-
|
|
58
|
-
|
|
311
|
+
if (idx !== null) {
|
|
312
|
+
isTracking = true;
|
|
313
|
+
trackParamIndex = idx;
|
|
59
314
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
!isExternalFile(resolvedFn.getSourceFile().fileName));
|
|
315
|
+
if (!manualWrapper && callName !== "track") {
|
|
316
|
+
detectedFunctionWrappers.add(callName);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
66
320
|
|
|
67
321
|
if (!isTracking) {
|
|
68
322
|
ts.forEachChild(node, visit);
|
|
69
323
|
return;
|
|
70
324
|
}
|
|
71
325
|
|
|
72
|
-
// авто-детект wrapper
|
|
73
|
-
if (!isKnownWrapper && isAutoWrapper && name !== "track") {
|
|
74
|
-
detectedFunctionWrappers.add(name);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
326
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
78
327
|
|
|
79
328
|
if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
|
|
@@ -87,23 +336,16 @@ export function scanSource(
|
|
|
87
336
|
});
|
|
88
337
|
}
|
|
89
338
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
node.arguments[idx],
|
|
97
|
-
checker,
|
|
98
|
-
paramMap
|
|
99
|
-
);
|
|
339
|
+
if (trackParamIndex !== null && node.arguments[trackParamIndex]) {
|
|
340
|
+
const res = resolveNodeValue(
|
|
341
|
+
node.arguments[trackParamIndex],
|
|
342
|
+
checker,
|
|
343
|
+
paramMap
|
|
344
|
+
);
|
|
100
345
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
346
|
+
res.values.forEach(v => {
|
|
347
|
+
if (isValidEvent(v)) events.add(v);
|
|
348
|
+
});
|
|
107
349
|
}
|
|
108
350
|
|
|
109
351
|
// CROSS FILE
|
|
@@ -116,7 +358,8 @@ export function scanSource(
|
|
|
116
358
|
checker,
|
|
117
359
|
config,
|
|
118
360
|
visited,
|
|
119
|
-
paramMap
|
|
361
|
+
paramMap,
|
|
362
|
+
wrapperCache
|
|
120
363
|
);
|
|
121
364
|
|
|
122
365
|
res.events.forEach(e => events.add(e));
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type ScanMode = "strict" | "hybrid" | "discovery";
|
|
2
2
|
|
|
3
|
+
export type ScanResult = {
|
|
4
|
+
events: Set<string>;
|
|
5
|
+
detectedFunctionWrappers: Set<string>;
|
|
6
|
+
detectedComponentWrappers: Map<string, string>;
|
|
7
|
+
};
|
|
8
|
+
|
|
3
9
|
export type ParseResult = {
|
|
4
10
|
code: string;
|
|
5
11
|
deps: string[];
|
|
@@ -22,7 +28,6 @@ export type EventraConfig = {
|
|
|
22
28
|
mode?: ScanMode;
|
|
23
29
|
wrappers: ComponentWrapper[];
|
|
24
30
|
functionWrappers: FunctionWrapper[];
|
|
25
|
-
aliases?: Record<string, string>;
|
|
26
31
|
sync: {
|
|
27
32
|
include: string[];
|
|
28
33
|
exclude: string[];
|
package/src/utils/config.ts
CHANGED
|
@@ -13,7 +13,6 @@ export function normalizeConfig(
|
|
|
13
13
|
endpoint: config.endpoint ?? "",
|
|
14
14
|
wrappers: config.wrappers ?? [],
|
|
15
15
|
functionWrappers: config.functionWrappers ?? [],
|
|
16
|
-
aliases: config.aliases ?? {},
|
|
17
16
|
sync: config.sync ?? {
|
|
18
17
|
include: [
|
|
19
18
|
"**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
|
package/src/utils/processFile.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseUniversal } from "./universal";
|
|
2
2
|
|
|
3
3
|
export function processFile(file: string, content: string) {
|
|
4
|
-
const
|
|
4
|
+
const parsed = parseUniversal(content, file);
|
|
5
5
|
|
|
6
6
|
return {
|
|
7
|
-
content: code || "",
|
|
8
|
-
deps: [...new Set(deps.filter(Boolean))],
|
|
7
|
+
content: parsed.code || "",
|
|
8
|
+
deps: [...new Set(parsed.deps.filter(Boolean))],
|
|
9
9
|
};
|
|
10
10
|
}
|