@eventra_dev/eventra-cli 0.1.6 → 0.1.7
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/commands/check.js +1 -1
- package/dist/commands/sync.js +36 -2
- package/dist/commands/watch.js +2 -2
- package/dist/engine/callResolver.js +58 -5
- package/dist/engine/engine.js +26 -19
- package/dist/engine/graph.js +39 -20
- package/dist/engine/resolver.js +5 -0
- package/dist/engine/scanner.js +163 -62
- package/package.json +1 -1
- package/src/commands/check.ts +1 -1
- package/src/commands/sync.ts +62 -2
- package/src/commands/watch.ts +2 -2
- package/src/engine/callResolver.ts +75 -8
- package/src/engine/engine.ts +30 -21
- package/src/engine/graph.ts +49 -24
- package/src/engine/resolver.ts +10 -0
- package/src/engine/scanner.ts +223 -72
package/src/engine/scanner.ts
CHANGED
|
@@ -1,120 +1,267 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { resolveNodeValue } from "./resolver";
|
|
3
3
|
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
|
+
import { resolveExportedSymbol } from "./exportResolver";
|
|
4
5
|
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
6
|
+
import { EventraConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
|
|
5
9
|
|
|
6
10
|
function getCallName(expr: ts.Expression): string {
|
|
7
11
|
if (ts.isIdentifier(expr)) return expr.text;
|
|
8
|
-
|
|
9
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
10
|
-
return expr.name.text;
|
|
11
|
-
}
|
|
12
|
-
|
|
12
|
+
if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
|
|
13
13
|
return "";
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let best: { score: number; node: ts.Expression } | null = null;
|
|
16
|
+
function getJsxAttrName(name: ts.JsxAttributeName): string | null {
|
|
17
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
18
|
+
if (ts.isJsxNamespacedName(name)) return name.name.text;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
function isValidEvent(v: string) {
|
|
23
|
+
return (
|
|
24
|
+
v &&
|
|
25
|
+
v.length > 1 &&
|
|
26
|
+
v.length < 100 &&
|
|
27
|
+
!v.includes(" ") &&
|
|
28
|
+
/^[a-zA-Z0-9._:-]+$/.test(v)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
function isPropUsedInTracking(
|
|
33
|
+
fn: ts.FunctionLikeDeclaration,
|
|
34
|
+
prop: string
|
|
35
|
+
) {
|
|
36
|
+
let found = false;
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
function visit(node: ts.Node) {
|
|
39
|
+
if (ts.isCallExpression(node)) {
|
|
40
|
+
const name = getCallName(node.expression);
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
if (res.values.every(v => v.length < 50)) score += 2;
|
|
32
|
-
if (res.values.some(v => v.includes("*"))) score -= 2;
|
|
42
|
+
if (!TRACKING_NAMES.has(name)) return;
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
for (const arg of node.arguments) {
|
|
45
|
+
if (
|
|
46
|
+
(ts.isIdentifier(arg) && arg.text === prop) ||
|
|
47
|
+
(ts.isPropertyAccessExpression(arg) &&
|
|
48
|
+
arg.name.text === prop)
|
|
49
|
+
) {
|
|
50
|
+
found = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
36
53
|
}
|
|
54
|
+
|
|
55
|
+
ts.forEachChild(node, visit);
|
|
37
56
|
}
|
|
38
57
|
|
|
39
|
-
|
|
58
|
+
visit(fn);
|
|
59
|
+
return found;
|
|
40
60
|
}
|
|
41
61
|
|
|
62
|
+
export type ScanResult = {
|
|
63
|
+
events: Set<string>;
|
|
64
|
+
detectedFunctionWrappers: Set<string>;
|
|
65
|
+
detectedComponentWrappers: Map<string, string>;
|
|
66
|
+
};
|
|
67
|
+
|
|
42
68
|
export function scanSource(
|
|
43
69
|
source: ts.SourceFile,
|
|
44
70
|
checker: ts.TypeChecker,
|
|
71
|
+
config: EventraConfig,
|
|
45
72
|
visited = new Set<string>(),
|
|
46
73
|
depth = 0
|
|
47
|
-
):
|
|
48
|
-
if (depth > 5) return new Set();
|
|
49
|
-
|
|
74
|
+
): ScanResult {
|
|
50
75
|
const events = new Set<string>();
|
|
76
|
+
const detectedFunctionWrappers = new Set<string>();
|
|
77
|
+
const detectedComponentWrappers = new Map<string, string>();
|
|
78
|
+
|
|
79
|
+
if (depth > 5 || visited.has(source.fileName)) {
|
|
80
|
+
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
81
|
+
}
|
|
51
82
|
|
|
52
|
-
if (visited.has(source.fileName)) return events;
|
|
53
83
|
visited.add(source.fileName);
|
|
54
84
|
|
|
55
85
|
function visit(node: ts.Node) {
|
|
86
|
+
// CALL EXPRESSIONS
|
|
56
87
|
if (ts.isCallExpression(node)) {
|
|
57
88
|
const name = getCallName(node.expression);
|
|
58
89
|
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
const isKnownWrapper = config.functionWrappers.some(
|
|
91
|
+
w => w.name === name
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const fn = resolveFunctionFromCall(node.expression, checker);
|
|
95
|
+
|
|
96
|
+
const isAutoWrapper =
|
|
97
|
+
fn && findTrackParamIndex(fn) !== null;
|
|
98
|
+
|
|
99
|
+
const isTracking =
|
|
100
|
+
isKnownWrapper ||
|
|
101
|
+
TRACKING_NAMES.has(name) ||
|
|
102
|
+
isAutoWrapper;
|
|
103
|
+
|
|
104
|
+
if (!isTracking) return;
|
|
105
|
+
|
|
106
|
+
// AUTO DETECT WRAPPER
|
|
107
|
+
if (
|
|
108
|
+
!isKnownWrapper &&
|
|
109
|
+
fn &&
|
|
110
|
+
!TRACKING_NAMES.has(name)
|
|
111
|
+
) {
|
|
112
|
+
detectedFunctionWrappers.add(name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const paramMap = new Map<string, ts.Expression>();
|
|
116
|
+
|
|
117
|
+
if (fn && ts.isFunctionLike(fn)) {
|
|
118
|
+
fn.parameters.forEach((param, i) => {
|
|
119
|
+
const arg = node.arguments[i];
|
|
120
|
+
if (!arg) return;
|
|
121
|
+
|
|
122
|
+
if (ts.isIdentifier(param.name)) {
|
|
123
|
+
paramMap.set(param.name.text, arg);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let handled = false;
|
|
129
|
+
|
|
130
|
+
if (fn) {
|
|
131
|
+
const idx = findTrackParamIndex(fn);
|
|
132
|
+
|
|
133
|
+
if (idx !== null && node.arguments[idx]) {
|
|
134
|
+
const res = resolveNodeValue(
|
|
135
|
+
node.arguments[idx],
|
|
136
|
+
checker,
|
|
137
|
+
paramMap
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
res?.values.forEach(v => {
|
|
141
|
+
if (isValidEvent(v)) events.add(v);
|
|
142
|
+
});
|
|
61
143
|
|
|
62
|
-
|
|
144
|
+
handled = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
63
147
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
148
|
+
// fallback
|
|
149
|
+
if (!handled) {
|
|
150
|
+
for (const arg of node.arguments) {
|
|
151
|
+
const res = resolveNodeValue(arg, checker, paramMap);
|
|
68
152
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
153
|
+
res?.values.forEach(v => {
|
|
154
|
+
if (isValidEvent(v)) events.add(v);
|
|
72
155
|
});
|
|
73
156
|
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// cross-file traversal
|
|
160
|
+
if (fn) {
|
|
161
|
+
const sf = fn.getSourceFile();
|
|
162
|
+
|
|
163
|
+
if (!visited.has(sf.fileName)) {
|
|
164
|
+
const res = scanSource(
|
|
165
|
+
sf,
|
|
166
|
+
checker,
|
|
167
|
+
config,
|
|
168
|
+
visited,
|
|
169
|
+
depth + 1
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
res.events.forEach(e => events.add(e));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// JSX COMPONENTS
|
|
178
|
+
if (
|
|
179
|
+
ts.isJsxSelfClosingElement(node) ||
|
|
180
|
+
ts.isJsxOpeningElement(node)
|
|
181
|
+
) {
|
|
182
|
+
const tagNode =
|
|
183
|
+
ts.isPropertyAccessExpression(node.tagName)
|
|
184
|
+
? node.tagName.name
|
|
185
|
+
: node.tagName;
|
|
186
|
+
|
|
187
|
+
let symbol = checker.getSymbolAtLocation(tagNode);
|
|
188
|
+
|
|
189
|
+
if (symbol) {
|
|
190
|
+
const resolved = resolveExportedSymbol(symbol, checker);
|
|
191
|
+
if (resolved) symbol = resolved;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
195
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
196
|
+
if (aliased) symbol = aliased;
|
|
197
|
+
}
|
|
74
198
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
199
|
+
if (!symbol) return;
|
|
200
|
+
|
|
201
|
+
const tagName =
|
|
202
|
+
ts.isPropertyAccessExpression(node.tagName)
|
|
203
|
+
? node.tagName.name.getText()
|
|
204
|
+
: node.tagName.getText();
|
|
205
|
+
|
|
206
|
+
const declarations = symbol.getDeclarations();
|
|
207
|
+
if (!declarations) return;
|
|
208
|
+
|
|
209
|
+
for (const decl of declarations) {
|
|
210
|
+
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
211
|
+
|
|
212
|
+
if (ts.isFunctionDeclaration(decl)) {
|
|
213
|
+
fn = decl;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
217
|
+
const init = decl.initializer;
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
ts.isArrowFunction(init) ||
|
|
221
|
+
ts.isFunctionExpression(init)
|
|
222
|
+
) {
|
|
223
|
+
fn = init;
|
|
98
224
|
}
|
|
99
225
|
}
|
|
100
226
|
|
|
101
|
-
|
|
102
|
-
|
|
227
|
+
if (!fn) continue;
|
|
228
|
+
|
|
229
|
+
const param = fn.parameters[0];
|
|
230
|
+
if (!param) continue;
|
|
231
|
+
|
|
232
|
+
if (!ts.isObjectBindingPattern(param.name)) continue;
|
|
103
233
|
|
|
104
|
-
|
|
105
|
-
const
|
|
234
|
+
for (const el of param.name.elements) {
|
|
235
|
+
const prop = el.name.getText();
|
|
236
|
+
|
|
237
|
+
const isEvent =
|
|
238
|
+
isPropUsedInTracking(fn, prop) ||
|
|
239
|
+
config.wrappers.some(
|
|
240
|
+
w => w.name === tagName && w.prop === prop
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (!isEvent) continue;
|
|
244
|
+
|
|
245
|
+
const attr = node.attributes.properties.find(p => {
|
|
246
|
+
if (!ts.isJsxAttribute(p)) return false;
|
|
247
|
+
return getJsxAttrName(p.name) === prop;
|
|
248
|
+
}) as ts.JsxAttribute | undefined;
|
|
249
|
+
|
|
250
|
+
if (!attr || !attr.initializer) continue;
|
|
251
|
+
|
|
252
|
+
const expr = ts.isJsxExpression(attr.initializer)
|
|
253
|
+
? attr.initializer.expression
|
|
254
|
+
: attr.initializer;
|
|
255
|
+
|
|
256
|
+
if (!expr) continue;
|
|
257
|
+
|
|
258
|
+
const res = resolveNodeValue(expr, checker);
|
|
106
259
|
|
|
107
260
|
res?.values.forEach(v => {
|
|
108
|
-
if (
|
|
109
|
-
v &&
|
|
110
|
-
v.length > 1 &&
|
|
111
|
-
v.length < 100 &&
|
|
112
|
-
!v.includes(" ") &&
|
|
113
|
-
/^[a-zA-Z0-9._:-]+$/.test(v)
|
|
114
|
-
) {
|
|
115
|
-
events.add(v);
|
|
116
|
-
}
|
|
261
|
+
if (isValidEvent(v)) events.add(v);
|
|
117
262
|
});
|
|
263
|
+
|
|
264
|
+
detectedComponentWrappers.set(tagName, prop);
|
|
118
265
|
}
|
|
119
266
|
}
|
|
120
267
|
}
|
|
@@ -124,5 +271,9 @@ export function scanSource(
|
|
|
124
271
|
|
|
125
272
|
visit(source);
|
|
126
273
|
|
|
127
|
-
return
|
|
274
|
+
return {
|
|
275
|
+
events,
|
|
276
|
+
detectedFunctionWrappers,
|
|
277
|
+
detectedComponentWrappers,
|
|
278
|
+
};
|
|
128
279
|
}
|