@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.
@@ -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 pickBestArg(
17
- args: readonly ts.Expression[],
18
- checker: ts.TypeChecker,
19
- paramMap: Map<string, ts.Expression>
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
- for (const arg of args) {
24
- const res = resolveNodeValue(arg, checker, paramMap);
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
- if (!res?.values?.length) continue;
32
+ function isPropUsedInTracking(
33
+ fn: ts.FunctionLikeDeclaration,
34
+ prop: string
35
+ ) {
36
+ let found = false;
27
37
 
28
- let score = 0;
38
+ function visit(node: ts.Node) {
39
+ if (ts.isCallExpression(node)) {
40
+ const name = getCallName(node.expression);
29
41
 
30
- if (!res.dynamic) score += 3;
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
- if (!best || score > best.score) {
35
- best = { score, node: arg };
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
- return best?.node;
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
- ): Set<string> {
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
- if (/(track|event|capture|send)/i.test(name)) {
60
- const fn = resolveFunctionFromCall(node.expression, checker);
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
- const paramMap = new Map<string, ts.Expression>();
144
+ handled = true;
145
+ }
146
+ }
63
147
 
64
- if (fn && ts.isFunctionLike(fn)) {
65
- fn.parameters.forEach((param, index) => {
66
- const arg = node.arguments[index];
67
- if (!arg) return;
148
+ // fallback
149
+ if (!handled) {
150
+ for (const arg of node.arguments) {
151
+ const res = resolveNodeValue(arg, checker, paramMap);
68
152
 
69
- if (ts.isIdentifier(param.name)) {
70
- paramMap.set(param.name.text, arg);
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
- if (fn && ts.isFunctionLike(fn)) {
76
- const idx = findTrackParamIndex(fn);
77
-
78
- if (idx !== null) {
79
- const realArg = node.arguments[idx];
80
-
81
- if (realArg) {
82
- const res = resolveNodeValue(realArg, checker, paramMap);
83
-
84
- res?.values.forEach(v => {
85
- if (
86
- v &&
87
- v.length > 1 &&
88
- v.length < 100 &&
89
- !v.includes(" ") &&
90
- /^[a-zA-Z0-9._:-]+$/.test(v)
91
- ) {
92
- events.add(v);
93
- }
94
- });
95
-
96
- return;
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
- // fallback (heuristic)
102
- const target = pickBestArg(node.arguments, checker, paramMap);
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
- if (target) {
105
- const res = resolveNodeValue(target, checker, paramMap);
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 events;
274
+ return {
275
+ events,
276
+ detectedFunctionWrappers,
277
+ detectedComponentWrappers,
278
+ };
128
279
  }