@eventra_dev/eventra-cli 0.1.29 → 0.1.31
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/engine.js +7 -4
- package/dist/engine/functionAnalyzer.js +15 -50
- package/dist/engine/resolver.js +57 -138
- package/dist/engine/scanner.js +205 -54
- package/dist/utils/config.js +0 -1
- package/dist/utils/processFile.js +3 -3
- package/dist/utils/universal.js +91 -94
- package/package.json +1 -1
- package/src/engine/engine.ts +26 -7
- package/src/engine/functionAnalyzer.ts +14 -54
- package/src/engine/resolver.ts +83 -184
- package/src/engine/scanner.ts +263 -75
- 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 +108 -119
- package/tests/run.ts +0 -1
- package/dist/engine/trackingDetector.js +0 -68
- package/src/engine/trackingDetector.ts +0 -94
package/src/engine/scanner.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { resolveNodeValue } from "./resolver";
|
|
3
3
|
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
|
-
import { resolveExportedSymbol } from "./exportResolver";
|
|
5
4
|
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
6
|
-
import {
|
|
5
|
+
import { resolveExportedSymbol } from "./exportResolver";
|
|
6
|
+
import { EventraConfig, ScanResult } from "../types";
|
|
7
7
|
import { isExternalFile } from "./boundary";
|
|
8
|
-
|
|
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
|
+
}
|
|
9
25
|
|
|
10
26
|
function getCallName(expr: ts.Expression): string {
|
|
11
27
|
if (ts.isIdentifier(expr)) return expr.text;
|
|
@@ -23,56 +39,144 @@ function isValidEvent(v: string) {
|
|
|
23
39
|
);
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
):
|
|
30
|
-
|
|
31
|
-
|
|
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;
|
|
32
49
|
|
|
33
|
-
|
|
50
|
+
// ({ event }) => ...
|
|
51
|
+
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
52
|
+
for (const el of firstParam.name.elements) {
|
|
53
|
+
if (!ts.isBindingElement(el)) continue;
|
|
34
54
|
|
|
35
|
-
|
|
55
|
+
const propName =
|
|
56
|
+
el.propertyName?.getText() || el.name.getText();
|
|
36
57
|
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
map.set(el.name.getText(), propName);
|
|
59
|
+
}
|
|
39
60
|
}
|
|
40
61
|
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
// (props) => props.event
|
|
63
|
+
if (ts.isIdentifier(firstParam.name)) {
|
|
64
|
+
map.set(firstParam.name.text, "*");
|
|
65
|
+
}
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
47
105
|
}
|
|
48
106
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
56
159
|
}
|
|
160
|
+
|
|
161
|
+
ts.forEachChild(node, visit);
|
|
57
162
|
}
|
|
58
163
|
|
|
59
|
-
|
|
60
|
-
}
|
|
164
|
+
if (fn.body) visit(fn.body);
|
|
61
165
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
detectedFunctionWrappers: Set<string>;
|
|
65
|
-
detectedComponentWrappers: Map<string, string>;
|
|
66
|
-
};
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
67
168
|
|
|
169
|
+
// MAIN SCANNER
|
|
68
170
|
export function scanSource(
|
|
69
171
|
source: ts.SourceFile,
|
|
70
172
|
checker: ts.TypeChecker,
|
|
71
173
|
config: EventraConfig,
|
|
72
174
|
visited = new Set<string>(),
|
|
73
|
-
|
|
74
|
-
|
|
175
|
+
inheritedParamMap?: Map<string, ts.Expression>,
|
|
176
|
+
wrapperCache?: Map<string, string | null>
|
|
75
177
|
): ScanResult {
|
|
178
|
+
wrapperCache = wrapperCache ?? new Map();
|
|
179
|
+
|
|
76
180
|
const events = new Set<string>();
|
|
77
181
|
const detectedFunctionWrappers = new Set<string>();
|
|
78
182
|
const detectedComponentWrappers = new Map<string, string>();
|
|
@@ -84,63 +188,149 @@ export function scanSource(
|
|
|
84
188
|
visited.add(source.fileName);
|
|
85
189
|
|
|
86
190
|
function visit(node: ts.Node) {
|
|
87
|
-
//
|
|
191
|
+
// JSX WRAPPERS
|
|
192
|
+
if (
|
|
193
|
+
ts.isJsxSelfClosingElement(node) ||
|
|
194
|
+
ts.isJsxOpeningElement(node)
|
|
195
|
+
) {
|
|
196
|
+
const tagName = getJsxTagName(node.tagName);
|
|
197
|
+
|
|
198
|
+
// skip html
|
|
199
|
+
if (!tagName || tagName[0] === tagName[0].toLowerCase()) {
|
|
200
|
+
ts.forEachChild(node, visit);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let wrapper = config.wrappers.find(
|
|
205
|
+
(w) => w.name === tagName
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (!wrapper) {
|
|
209
|
+
let symbol = checker.getSymbolAtLocation(node.tagName);
|
|
210
|
+
|
|
211
|
+
let cacheKey = tagName;
|
|
212
|
+
|
|
213
|
+
if (symbol) {
|
|
214
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
215
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
symbol =
|
|
219
|
+
resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
220
|
+
|
|
221
|
+
cacheKey = checker.getFullyQualifiedName(symbol);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (wrapperCache?.has(cacheKey)) {
|
|
225
|
+
const cached = wrapperCache.get(cacheKey);
|
|
226
|
+
if (cached) {
|
|
227
|
+
wrapper = { name: tagName, prop: cached };
|
|
228
|
+
}
|
|
229
|
+
} else if (symbol) {
|
|
230
|
+
let detected: string | null = null;
|
|
231
|
+
|
|
232
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
233
|
+
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
234
|
+
|
|
235
|
+
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
ts.isVariableDeclaration(decl) &&
|
|
239
|
+
decl.initializer &&
|
|
240
|
+
(ts.isArrowFunction(decl.initializer) ||
|
|
241
|
+
ts.isFunctionExpression(decl.initializer))
|
|
242
|
+
) {
|
|
243
|
+
fn = decl.initializer;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!fn) continue;
|
|
247
|
+
|
|
248
|
+
const prop = findTrackedPropName(fn, checker);
|
|
249
|
+
|
|
250
|
+
if (prop) {
|
|
251
|
+
detected = prop;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
wrapperCache?.set(cacheKey, detected);
|
|
257
|
+
|
|
258
|
+
if (detected) {
|
|
259
|
+
wrapper = { name: tagName, prop: detected };
|
|
260
|
+
detectedComponentWrappers.set(tagName, detected);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (wrapper) {
|
|
266
|
+
for (const attr of node.attributes.properties) {
|
|
267
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
268
|
+
|
|
269
|
+
const name = getJsxAttrName(attr.name);
|
|
270
|
+
if (name !== wrapper.prop) continue;
|
|
271
|
+
if (!attr.initializer) continue;
|
|
272
|
+
|
|
273
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
274
|
+
if (isValidEvent(attr.initializer.text)) {
|
|
275
|
+
events.add(attr.initializer.text);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (ts.isJsxExpression(attr.initializer)) {
|
|
280
|
+
const expr = attr.initializer.expression;
|
|
281
|
+
if (!expr) continue;
|
|
282
|
+
|
|
283
|
+
const res = resolveNodeValue(
|
|
284
|
+
expr,
|
|
285
|
+
checker,
|
|
286
|
+
inheritedParamMap
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
res.values.forEach((v) => {
|
|
290
|
+
if (isValidEvent(v)) events.add(v);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
ts.forEachChild(node, visit);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// FUNCTION TRACKING
|
|
88
301
|
if (ts.isCallExpression(node)) {
|
|
89
302
|
const name = getCallName(node.expression);
|
|
90
303
|
|
|
91
304
|
const isKnownWrapper = config.functionWrappers.some(
|
|
92
|
-
w => w.name === name
|
|
305
|
+
(w) => w.name === name
|
|
93
306
|
);
|
|
94
307
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const resolvedFn = resolveFunctionDeep(
|
|
308
|
+
const resolvedFn = resolveFunctionFromCall(
|
|
98
309
|
node.expression,
|
|
99
310
|
checker
|
|
100
311
|
);
|
|
101
312
|
|
|
102
313
|
const isAutoWrapper =
|
|
103
|
-
resolvedFn && findTrackParamIndex(resolvedFn) !== null;
|
|
314
|
+
!!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
|
|
104
315
|
|
|
105
|
-
|
|
316
|
+
const isTracking =
|
|
317
|
+
name === "track" ||
|
|
106
318
|
isKnownWrapper ||
|
|
107
|
-
isTrackingByType ||
|
|
108
|
-
name.startsWith("$") ||
|
|
109
319
|
(isAutoWrapper &&
|
|
110
320
|
resolvedFn &&
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (!isTracking) {
|
|
115
|
-
if (
|
|
116
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
117
|
-
node.expression.name.text === "track"
|
|
118
|
-
) {
|
|
119
|
-
isTracking = true;
|
|
120
|
-
} else if (
|
|
121
|
-
ts.isIdentifier(node.expression) &&
|
|
122
|
-
node.expression.text === "track"
|
|
123
|
-
) {
|
|
124
|
-
isTracking = true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
321
|
+
!isExternalFile(
|
|
322
|
+
resolvedFn.getSourceFile().fileName
|
|
323
|
+
));
|
|
127
324
|
|
|
128
325
|
if (!isTracking) {
|
|
129
326
|
ts.forEachChild(node, visit);
|
|
130
327
|
return;
|
|
131
328
|
}
|
|
132
329
|
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
!isKnownWrapper &&
|
|
136
|
-
isAutoWrapper &&
|
|
137
|
-
!isTrackingByType &&
|
|
138
|
-
!name.startsWith("track")
|
|
139
|
-
) {
|
|
330
|
+
if (!isKnownWrapper && isAutoWrapper && name !== "track") {
|
|
140
331
|
detectedFunctionWrappers.add(name);
|
|
141
332
|
}
|
|
142
333
|
|
|
143
|
-
// PARAM MAP
|
|
144
334
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
145
335
|
|
|
146
336
|
if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
|
|
@@ -154,7 +344,6 @@ export function scanSource(
|
|
|
154
344
|
});
|
|
155
345
|
}
|
|
156
346
|
|
|
157
|
-
// STRICT EVENT RESOLVE
|
|
158
347
|
if (resolvedFn) {
|
|
159
348
|
const idx = findTrackParamIndex(resolvedFn);
|
|
160
349
|
|
|
@@ -165,27 +354,26 @@ export function scanSource(
|
|
|
165
354
|
paramMap
|
|
166
355
|
);
|
|
167
356
|
|
|
168
|
-
res.values.forEach(v => {
|
|
357
|
+
res.values.forEach((v) => {
|
|
169
358
|
if (isValidEvent(v)) events.add(v);
|
|
170
359
|
});
|
|
171
360
|
}
|
|
172
361
|
}
|
|
173
362
|
|
|
174
|
-
// CROSS-FILE
|
|
175
363
|
if (resolvedFn) {
|
|
176
|
-
const sf = resolvedFn.getSourceFile
|
|
364
|
+
const sf = resolvedFn.getSourceFile();
|
|
177
365
|
|
|
178
|
-
if (
|
|
366
|
+
if (!visited.has(sf.fileName)) {
|
|
179
367
|
const res = scanSource(
|
|
180
368
|
sf,
|
|
181
369
|
checker,
|
|
182
370
|
config,
|
|
183
371
|
visited,
|
|
184
|
-
|
|
185
|
-
|
|
372
|
+
paramMap,
|
|
373
|
+
wrapperCache
|
|
186
374
|
);
|
|
187
375
|
|
|
188
|
-
res.events.forEach(e => events.add(e));
|
|
376
|
+
res.events.forEach((e) => events.add(e));
|
|
189
377
|
}
|
|
190
378
|
}
|
|
191
379
|
}
|
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
|
}
|