@eventra_dev/eventra-cli 0.1.33 → 0.2.1
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 +14 -30
- package/dist/commands/sync.js +9 -27
- package/dist/commands/watch.js +38 -121
- package/dist/engine/callResolver.js +32 -131
- package/dist/engine/engine.js +14 -9
- package/dist/engine/languageService.js +7 -38
- package/dist/engine/resolver.js +88 -77
- package/dist/engine/scanner.js +131 -196
- package/dist/utils/universal.js +1 -1
- package/package.json +1 -1
- package/src/commands/check.ts +17 -48
- package/src/commands/sync.ts +14 -44
- package/src/commands/watch.ts +48 -158
- package/src/engine/callResolver.ts +24 -159
- package/src/engine/engine.ts +20 -12
- package/src/engine/languageService.ts +22 -52
- package/src/engine/resolver.ts +99 -120
- package/src/engine/scanner.ts +149 -243
- package/src/utils/universal.ts +1 -1
package/src/engine/scanner.ts
CHANGED
|
@@ -1,196 +1,181 @@
|
|
|
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
|
-
//
|
|
7
|
+
// HELPERS
|
|
8
|
+
function isValidEvent(v: string) {
|
|
9
|
+
return (
|
|
10
|
+
v &&
|
|
11
|
+
v.length > 1 &&
|
|
12
|
+
v.length < 120 &&
|
|
13
|
+
!v.includes(" ") &&
|
|
14
|
+
/^[a-zA-Z0-9._:-]+$/.test(v)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getCallName(expr: ts.Expression): string {
|
|
19
|
+
if (ts.isIdentifier(expr)) return expr.text;
|
|
20
|
+
if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getJsxTagName(tag: ts.JsxTagNameExpression): string {
|
|
25
|
+
if (ts.isIdentifier(tag)) return tag.text;
|
|
26
|
+
if (ts.isPropertyAccessExpression(tag)) return tag.name.text;
|
|
27
|
+
return tag.getText();
|
|
28
|
+
}
|
|
29
|
+
|
|
10
30
|
function getJsxAttrName(name: ts.JsxAttributeName): string {
|
|
11
31
|
if (ts.isIdentifier(name)) return name.text;
|
|
12
32
|
if (ts.isJsxNamespacedName(name)) return name.name.text;
|
|
13
33
|
return "";
|
|
14
34
|
}
|
|
15
35
|
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
// MAIN
|
|
37
|
+
export function scanSource(
|
|
38
|
+
source: ts.SourceFile,
|
|
39
|
+
checker: ts.TypeChecker,
|
|
40
|
+
config: EventraConfig,
|
|
41
|
+
visited = new Set<string>(),
|
|
42
|
+
inheritedParamMap?: Map<string, ts.Expression>,
|
|
43
|
+
wrapperCache = new Map<string, string | null>()
|
|
44
|
+
): ScanResult {
|
|
45
|
+
const events = new Set<string>();
|
|
46
|
+
const detectedFunctionWrappers = new Set<string>();
|
|
47
|
+
const detectedComponentWrappers = new Map<string, string>();
|
|
18
48
|
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
49
|
+
if (visited.has(source.fileName)) {
|
|
50
|
+
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
21
51
|
}
|
|
22
52
|
|
|
23
|
-
|
|
24
|
-
}
|
|
53
|
+
visited.add(source.fileName);
|
|
25
54
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
55
|
+
// TRACK DETECTION
|
|
56
|
+
function isTrackCall(expr: ts.Expression) {
|
|
57
|
+
if (ts.isIdentifier(expr)) {
|
|
58
|
+
return expr.text === "track";
|
|
59
|
+
}
|
|
31
60
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
v.length > 1 &&
|
|
36
|
-
v.length < 100 &&
|
|
37
|
-
!v.includes(" ") &&
|
|
38
|
-
/^[a-zA-Z0-9._:-]+$/.test(v)
|
|
39
|
-
);
|
|
40
|
-
}
|
|
61
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
62
|
+
return expr.name.text === "track";
|
|
63
|
+
}
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
function extractTrackCall(
|
|
69
|
+
node: ts.CallExpression,
|
|
70
|
+
paramMap: Map<string, ts.Expression>
|
|
71
|
+
) {
|
|
72
|
+
if (!isTrackCall(node.expression)) return;
|
|
54
73
|
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
const arg = node.arguments[0];
|
|
75
|
+
if (!arg) return;
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
const res = resolveNodeValue(arg, checker, paramMap);
|
|
78
|
+
|
|
79
|
+
for (const v of res.values) {
|
|
80
|
+
if (isValidEvent(v)) {
|
|
81
|
+
events.add(v);
|
|
82
|
+
}
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
// WRAPPER DETECTION
|
|
87
|
+
function isWrapperFunction(fn: ts.FunctionLikeDeclaration) {
|
|
88
|
+
let found = false;
|
|
89
|
+
|
|
90
|
+
function walk(n: ts.Node) {
|
|
91
|
+
if (found) return;
|
|
92
|
+
|
|
93
|
+
if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
|
|
94
|
+
found = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ts.forEachChild(n, walk);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (fn.body) walk(fn.body);
|
|
102
|
+
return found;
|
|
65
103
|
}
|
|
66
104
|
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
// COMPONENT WRAPPER
|
|
106
|
+
function detectComponentWrapper(tag: ts.JsxTagNameExpression): string | null {
|
|
107
|
+
const tagName = getJsxTagName(tag);
|
|
108
|
+
|
|
109
|
+
if (wrapperCache.has(tagName)) {
|
|
110
|
+
return wrapperCache.get(tagName)!;
|
|
111
|
+
}
|
|
69
112
|
|
|
70
|
-
|
|
71
|
-
fn: ts.FunctionLikeDeclaration,
|
|
72
|
-
checker: ts.TypeChecker
|
|
73
|
-
): string | null {
|
|
74
|
-
const propMap = getComponentPropsMap(fn);
|
|
75
|
-
let result: string | null = null;
|
|
113
|
+
let symbol = checker.getSymbolAtLocation(tag);
|
|
76
114
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const mapped = propMap.get(node.text);
|
|
81
|
-
if (mapped && mapped !== "*") return mapped;
|
|
115
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
116
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
117
|
+
}
|
|
82
118
|
|
|
83
|
-
|
|
84
|
-
|
|
119
|
+
if (!symbol) {
|
|
120
|
+
wrapperCache.set(tagName, null);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
85
125
|
|
|
86
126
|
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
|
-
}
|
|
127
|
+
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
128
|
+
|
|
129
|
+
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
95
130
|
|
|
96
|
-
// const e = event
|
|
97
131
|
if (
|
|
98
132
|
ts.isVariableDeclaration(decl) &&
|
|
99
133
|
decl.initializer &&
|
|
100
|
-
ts.
|
|
134
|
+
(ts.isArrowFunction(decl.initializer) ||
|
|
135
|
+
ts.isFunctionExpression(decl.initializer))
|
|
101
136
|
) {
|
|
102
|
-
|
|
103
|
-
if (inner) return inner;
|
|
137
|
+
fn = decl.initializer;
|
|
104
138
|
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
139
|
|
|
110
|
-
|
|
111
|
-
if (result) return;
|
|
140
|
+
if (!fn || !fn.body) continue;
|
|
112
141
|
|
|
113
|
-
|
|
114
|
-
const expr = node.expression;
|
|
142
|
+
let prop: string | null = null;
|
|
115
143
|
|
|
116
|
-
|
|
117
|
-
(
|
|
118
|
-
(ts.isPropertyAccessExpression(expr) &&
|
|
119
|
-
expr.name.text === "track");
|
|
144
|
+
function walk(n: ts.Node) {
|
|
145
|
+
if (prop) return;
|
|
120
146
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
147
|
+
if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
|
|
148
|
+
const arg = n.arguments[0];
|
|
125
149
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (ts.isIdentifier(arg)) {
|
|
129
|
-
const prop = resolveIdentifierToProp(arg);
|
|
130
|
-
if (prop) {
|
|
131
|
-
result = prop;
|
|
150
|
+
if (ts.isPropertyAccessExpression(arg)) {
|
|
151
|
+
prop = arg.name.text;
|
|
132
152
|
return;
|
|
133
153
|
}
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let current = arg;
|
|
156
|
+
ts.forEachChild(n, walk);
|
|
157
|
+
}
|
|
139
158
|
|
|
140
|
-
|
|
141
|
-
result = current.name.text;
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
159
|
+
walk(fn.body);
|
|
145
160
|
|
|
146
|
-
|
|
147
|
-
if (ts.isCallExpression(arg)) {
|
|
148
|
-
const inner = resolveFunctionFromCall(
|
|
149
|
-
arg.expression,
|
|
150
|
-
checker
|
|
151
|
-
);
|
|
161
|
+
wrapperCache.set(tagName, prop);
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
163
|
+
if (prop) {
|
|
164
|
+
detectedComponentWrappers.set(tagName, prop);
|
|
165
|
+
return prop;
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
|
|
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 };
|
|
169
|
+
wrapperCache.set(tagName, null);
|
|
170
|
+
return null;
|
|
184
171
|
}
|
|
185
172
|
|
|
186
|
-
|
|
187
|
-
|
|
173
|
+
// VISITOR
|
|
188
174
|
function visit(node: ts.Node) {
|
|
189
|
-
// JSX
|
|
175
|
+
// JSX
|
|
190
176
|
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
191
177
|
const tagName = getJsxTagName(node.tagName);
|
|
192
178
|
|
|
193
|
-
// skip html
|
|
194
179
|
if (tagName[0] === tagName[0].toLowerCase()) {
|
|
195
180
|
ts.forEachChild(node, visit);
|
|
196
181
|
return;
|
|
@@ -198,48 +183,9 @@ export function scanSource(
|
|
|
198
183
|
|
|
199
184
|
let wrapper = config.wrappers.find(w => w.name === tagName);
|
|
200
185
|
|
|
201
|
-
// AUTO DETECT
|
|
202
186
|
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
|
-
}
|
|
187
|
+
const prop = detectComponentWrapper(node.tagName);
|
|
188
|
+
if (prop) wrapper = { name: tagName, prop };
|
|
243
189
|
}
|
|
244
190
|
|
|
245
191
|
if (wrapper) {
|
|
@@ -252,16 +198,16 @@ export function scanSource(
|
|
|
252
198
|
if (!attr.initializer) continue;
|
|
253
199
|
|
|
254
200
|
if (ts.isStringLiteral(attr.initializer)) {
|
|
255
|
-
|
|
256
|
-
|
|
201
|
+
if (isValidEvent(attr.initializer.text)) {
|
|
202
|
+
events.add(attr.initializer.text);
|
|
203
|
+
}
|
|
257
204
|
}
|
|
258
205
|
|
|
259
206
|
if (ts.isJsxExpression(attr.initializer)) {
|
|
260
207
|
const expr = attr.initializer.expression;
|
|
261
208
|
if (!expr) continue;
|
|
262
209
|
|
|
263
|
-
const res = resolveNodeValue(expr, checker
|
|
264
|
-
|
|
210
|
+
const res = resolveNodeValue(expr, checker);
|
|
265
211
|
res.values.forEach(v => {
|
|
266
212
|
if (isValidEvent(v)) events.add(v);
|
|
267
213
|
});
|
|
@@ -273,83 +219,38 @@ export function scanSource(
|
|
|
273
219
|
return;
|
|
274
220
|
}
|
|
275
221
|
|
|
276
|
-
//
|
|
222
|
+
// CALL
|
|
277
223
|
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
|
-
}
|
|
224
|
+
const resolvedFn = resolveFunctionFromCall(node.expression, checker);
|
|
325
225
|
|
|
326
226
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
327
227
|
|
|
328
|
-
if (resolvedFn
|
|
329
|
-
resolvedFn.parameters.forEach((
|
|
228
|
+
if (resolvedFn) {
|
|
229
|
+
resolvedFn.parameters.forEach((p, i) => {
|
|
330
230
|
const arg = node.arguments[i];
|
|
331
231
|
if (!arg) return;
|
|
332
232
|
|
|
333
|
-
if (ts.isIdentifier(
|
|
334
|
-
paramMap.set(
|
|
233
|
+
if (ts.isIdentifier(p.name)) {
|
|
234
|
+
paramMap.set(p.name.text, arg);
|
|
335
235
|
}
|
|
336
236
|
});
|
|
337
237
|
}
|
|
338
238
|
|
|
339
|
-
|
|
340
|
-
const res = resolveNodeValue(
|
|
341
|
-
node.arguments[trackParamIndex],
|
|
342
|
-
checker,
|
|
343
|
-
paramMap
|
|
344
|
-
);
|
|
239
|
+
extractTrackCall(node, paramMap);
|
|
345
240
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
241
|
+
if (resolvedFn && isWrapperFunction(resolvedFn)) {
|
|
242
|
+
const name = getCallName(node.expression);
|
|
243
|
+
|
|
244
|
+
if (name && name !== "track") {
|
|
245
|
+
detectedFunctionWrappers.add(name);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (resolvedFn.body) {
|
|
249
|
+
visit(resolvedFn.body);
|
|
250
|
+
}
|
|
349
251
|
}
|
|
350
252
|
|
|
351
|
-
|
|
352
|
-
if (resolvedFn) {
|
|
253
|
+
if (resolvedFn && resolvedFn.body) {
|
|
353
254
|
const sf = resolvedFn.getSourceFile();
|
|
354
255
|
|
|
355
256
|
if (!visited.has(sf.fileName)) {
|
|
@@ -365,6 +266,11 @@ export function scanSource(
|
|
|
365
266
|
res.events.forEach(e => events.add(e));
|
|
366
267
|
}
|
|
367
268
|
}
|
|
269
|
+
|
|
270
|
+
node.arguments.forEach(visit);
|
|
271
|
+
visit(node.expression);
|
|
272
|
+
|
|
273
|
+
return;
|
|
368
274
|
}
|
|
369
275
|
|
|
370
276
|
ts.forEachChild(node, visit);
|