@eventra_dev/eventra-cli 0.1.24 → 0.1.26

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.
@@ -12,12 +12,13 @@ export function resolveNodeValue(
12
12
  paramMap?: Map<string, ts.Expression>,
13
13
  seen = new Set<ts.Node>()
14
14
  ): ResolveResult {
15
+
15
16
  if (seen.has(node)) {
16
17
  return { values: [], dynamic: true };
17
18
  }
18
19
  seen.add(node);
19
20
 
20
- // param mapping
21
+ // PARAM MAPPING
21
22
  if (ts.isIdentifier(node) && paramMap?.has(node.text)) {
22
23
  return resolveNodeValue(
23
24
  paramMap.get(node.text)!,
@@ -43,12 +44,15 @@ export function resolveNodeValue(
43
44
  }
44
45
  }
45
46
 
46
- return { values, dynamic: true };
47
+ return {
48
+ values: [...new Set(values)],
49
+ dynamic: true
50
+ };
47
51
  }
48
52
 
49
53
  // TEMPLATE
50
54
  if (ts.isTemplateExpression(node)) {
51
- let results = [node.head.text];
55
+ let results = node.head.text ? [node.head.text] : [""];
52
56
 
53
57
  for (const span of node.templateSpans) {
54
58
  const res = resolveNodeValue(span.expression, checker, paramMap, seen);
@@ -83,14 +87,7 @@ export function resolveNodeValue(
83
87
  let symbol = checker.getSymbolAtLocation(node);
84
88
  if (!symbol) return { values: [], dynamic: true };
85
89
 
86
- // unwrap export
87
- symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
88
-
89
- if (symbol.flags & ts.SymbolFlags.Alias) {
90
- symbol = checker.getAliasedSymbol(symbol);
91
- }
92
-
93
- // inline variable
90
+ // INLINE FIRST
94
91
  for (const decl of symbol.getDeclarations() ?? []) {
95
92
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
96
93
  return resolveNodeValue(
@@ -100,7 +97,16 @@ export function resolveNodeValue(
100
97
  seen
101
98
  );
102
99
  }
100
+ }
101
+
102
+ // unwrap export
103
+ symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
104
+
105
+ if (symbol.flags & ts.SymbolFlags.Alias) {
106
+ symbol = checker.getAliasedSymbol(symbol);
107
+ }
103
108
 
109
+ for (const decl of symbol.getDeclarations() ?? []) {
104
110
  if (ts.isEnumMember(decl) && decl.initializer) {
105
111
  if (ts.isStringLiteral(decl.initializer)) {
106
112
  return {
@@ -111,15 +117,15 @@ export function resolveNodeValue(
111
117
  }
112
118
  }
113
119
 
114
- // UNION TYPES
115
120
  const type = checker.getTypeAtLocation(node);
116
121
 
122
+ // UNION
117
123
  if (type.isUnion()) {
118
124
  const values: string[] = [];
119
125
 
120
126
  for (const t of type.types) {
121
- if ((t as any).isStringLiteral?.()) {
122
- values.push((t as any).value);
127
+ if (t.flags & ts.TypeFlags.StringLiteral) {
128
+ values.push((t as ts.StringLiteralType).value);
123
129
  }
124
130
  }
125
131
 
@@ -128,15 +134,18 @@ export function resolveNodeValue(
128
134
  }
129
135
  }
130
136
 
131
- // TYPE FALLBACK (as const)
132
- if ((type as any).isStringLiteral?.()) {
133
- return { values: [(type as any).value], dynamic: false };
137
+ // STRING LITERAL TYPE
138
+ if (type.flags & ts.TypeFlags.StringLiteral) {
139
+ return {
140
+ values: [(type as ts.StringLiteralType).value],
141
+ dynamic: false
142
+ };
134
143
  }
135
144
 
136
145
  return { values: [], dynamic: true };
137
146
  }
138
147
 
139
- // PROPERTY ACCESS (obj.a.b)
148
+ // PROPERTY ACCESS
140
149
  if (ts.isPropertyAccessExpression(node)) {
141
150
 
142
151
  const symbol = checker.getSymbolAtLocation(node);
@@ -160,18 +169,16 @@ export function resolveNodeValue(
160
169
  }
161
170
  }
162
171
 
163
- // constant enum / readonly
164
172
  const constantValue = checker.getConstantValue(node as any);
165
173
  if (typeof constantValue === "string") {
166
174
  return { values: [constantValue], dynamic: false };
167
175
  }
168
176
 
169
- // type fallback
170
177
  const type = checker.getTypeAtLocation(node);
171
178
 
172
- if ((type as any).isStringLiteral?.()) {
179
+ if (type.flags & ts.TypeFlags.StringLiteral) {
173
180
  return {
174
- values: [(type as any).value],
181
+ values: [(type as ts.StringLiteralType).value],
175
182
  dynamic: false,
176
183
  };
177
184
  }
@@ -206,10 +213,11 @@ export function resolveNodeValue(
206
213
  }
207
214
  }
208
215
 
209
- return { values: [], dynamic: true };
216
+ // fallback
217
+ return resolveNodeValue(node.name, checker, paramMap, seen);
210
218
  }
211
219
 
212
- // ARRAY ACCESS (arr[0])
220
+ // ARRAY ACCESS
213
221
  if (ts.isElementAccessExpression(node)) {
214
222
  const obj = resolveNodeValue(node.expression, checker, paramMap, seen);
215
223
 
@@ -224,7 +232,6 @@ export function resolveNodeValue(
224
232
  seen
225
233
  );
226
234
 
227
- // if index known
228
235
  if (index.values.length === 1) {
229
236
  const i = Number(index.values[0]);
230
237
 
@@ -236,7 +243,6 @@ export function resolveNodeValue(
236
243
  }
237
244
  }
238
245
 
239
- // fallback → dynamic
240
246
  return obj.values.length
241
247
  ? { values: obj.values, dynamic: true }
242
248
  : { values: [], dynamic: true };
@@ -257,7 +263,10 @@ export function resolveNodeValue(
257
263
  }
258
264
  }
259
265
 
260
- return { values, dynamic: true };
266
+ return {
267
+ values: [...new Set(values)],
268
+ dynamic: true
269
+ };
261
270
  }
262
271
 
263
272
  // BINARY ("btn_" + name)
@@ -5,8 +5,7 @@ import { resolveExportedSymbol } from "./exportResolver";
5
5
  import { findTrackParamIndex } from "./functionAnalyzer";
6
6
  import { EventraConfig } from "../types";
7
7
  import { isExternalFile } from "./boundary";
8
-
9
- const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
8
+ import { isTrackingCall } from "./trackingDetector";
10
9
 
11
10
  function getCallName(expr: ts.Expression): string {
12
11
  if (ts.isIdentifier(expr)) return expr.text;
@@ -20,6 +19,19 @@ function getJsxAttrName(name: ts.JsxAttributeName): string | null {
20
19
  return null;
21
20
  }
22
21
 
22
+ // unwrap
23
+ function unwrap(expr: ts.Expression): ts.Expression {
24
+ while (
25
+ ts.isAsExpression(expr) ||
26
+ ts.isParenthesizedExpression(expr) ||
27
+ ts.isTypeAssertionExpression(expr) ||
28
+ ts.isNonNullExpression(expr)
29
+ ) {
30
+ expr = expr.expression;
31
+ }
32
+ return expr;
33
+ }
34
+
23
35
  function isValidEvent(v: string) {
24
36
  return (
25
37
  v &&
@@ -32,24 +44,25 @@ function isValidEvent(v: string) {
32
44
 
33
45
  function isPropUsedInTracking(
34
46
  fn: ts.FunctionLikeDeclaration,
47
+ checker: ts.TypeChecker,
35
48
  prop: string
36
49
  ) {
37
50
  let found = false;
38
51
 
39
52
  function visit(node: ts.Node) {
40
53
  if (ts.isCallExpression(node)) {
41
- const name = getCallName(node.expression);
42
-
43
- if (!TRACKING_NAMES.has(name)) {
54
+ if (!isTrackingCall(node, checker)) {
44
55
  ts.forEachChild(node, visit);
45
56
  return;
46
57
  }
47
58
 
48
59
  for (const arg of node.arguments) {
60
+ const a = unwrap(arg as ts.Expression);
61
+
49
62
  if (
50
- (ts.isIdentifier(arg) && arg.text === prop) ||
51
- (ts.isPropertyAccessExpression(arg) &&
52
- arg.name.text === prop)
63
+ (ts.isIdentifier(a) && a.text === prop) ||
64
+ (ts.isPropertyAccessExpression(a) &&
65
+ a.name.text === prop)
53
66
  ) {
54
67
  found = true;
55
68
  }
@@ -59,7 +72,10 @@ function isPropUsedInTracking(
59
72
  ts.forEachChild(node, visit);
60
73
  }
61
74
 
62
- visit(fn);
75
+ if (fn.body) {
76
+ visit(fn.body);
77
+ }
78
+
63
79
  return found;
64
80
  }
65
81
 
@@ -81,7 +97,11 @@ export function scanSource(
81
97
  const detectedFunctionWrappers = new Set<string>();
82
98
  const detectedComponentWrappers = new Map<string, string>();
83
99
 
84
- if (depth > 5 || visited.has(source.fileName)) {
100
+ if (
101
+ visited.has(source.fileName) ||
102
+ depth > 10 ||
103
+ visited.size > 200
104
+ ) {
85
105
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
86
106
  }
87
107
 
@@ -90,53 +110,55 @@ export function scanSource(
90
110
  function visit(node: ts.Node) {
91
111
  // CALL EXPRESSIONS
92
112
  if (ts.isCallExpression(node)) {
93
- const name = getCallName(node.expression);
113
+ const expr = unwrap(node.expression);
114
+ const name = getCallName(expr);
94
115
 
95
116
  const isKnownWrapper = config.functionWrappers.some(
96
117
  w => w.name === name
97
118
  );
98
119
 
99
- let resolvedFn = resolveFunctionFromCall(node.expression, checker);
100
-
101
- // fallback: tracker.track(...)
102
- if (!resolvedFn && ts.isPropertyAccessExpression(node.expression)) {
103
- const symbol = checker.getSymbolAtLocation(node.expression.name);
104
-
105
- if (symbol) {
106
- const decls = symbol.getDeclarations();
120
+ const isTrackingByType = isTrackingCall(node, checker);
107
121
 
108
- if (decls?.length) {
109
- const d = decls[0];
110
-
111
- if (
112
- ts.isMethodDeclaration(d) ||
113
- ts.isFunctionDeclaration(d) ||
114
- ts.isFunctionExpression(d) ||
115
- ts.isArrowFunction(d)
116
- ) {
117
- resolvedFn = d;
118
- }
119
- }
120
- }
121
- }
122
+ let resolvedFn = resolveFunctionFromCall(expr, checker);
122
123
 
123
124
  const isAutoWrapper =
124
125
  resolvedFn && findTrackParamIndex(resolvedFn) !== null;
125
126
 
126
- const isTracking =
127
+ let isTracking =
127
128
  isKnownWrapper ||
128
- TRACKING_NAMES.has(name) ||
129
- name.startsWith("$") || // NUXT FIX
129
+ isTrackingByType ||
130
+ name.startsWith("$") ||
130
131
  (isAutoWrapper &&
131
132
  resolvedFn &&
132
133
  !isExternalFile(resolvedFn.getSourceFile().fileName));
133
134
 
135
+ // fallback
136
+ if (!isTracking) {
137
+ if (
138
+ ts.isPropertyAccessExpression(expr) &&
139
+ expr.name.text === "track"
140
+ ) {
141
+ isTracking = true;
142
+ } else if (
143
+ ts.isIdentifier(expr) &&
144
+ expr.text === "track"
145
+ ) {
146
+ isTracking = true;
147
+ }
148
+ }
149
+
134
150
  if (!isTracking) {
135
151
  ts.forEachChild(node, visit);
136
152
  return;
137
153
  }
138
154
 
139
- if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
155
+ // detect wrapper
156
+ if (
157
+ !isKnownWrapper &&
158
+ isAutoWrapper &&
159
+ !isTrackingByType &&
160
+ !name.startsWith("track")
161
+ ) {
140
162
  detectedFunctionWrappers.add(name);
141
163
  }
142
164
 
@@ -161,16 +183,14 @@ export function scanSource(
161
183
 
162
184
  if (idx !== null && node.arguments[idx]) {
163
185
  const res = resolveNodeValue(
164
- node.arguments[idx],
186
+ unwrap(node.arguments[idx]),
165
187
  checker,
166
188
  paramMap
167
189
  );
168
190
 
169
- if (res && res.values.length) {
170
- res.values.forEach(v => {
171
- if (isValidEvent(v)) events.add(v);
172
- });
173
- }
191
+ res.values.forEach(v => {
192
+ if (isValidEvent(v)) events.add(v);
193
+ });
174
194
 
175
195
  handled = true;
176
196
  }
@@ -179,13 +199,15 @@ export function scanSource(
179
199
  // FALLBACK
180
200
  if (!handled) {
181
201
  for (const arg of node.arguments) {
182
- const res = resolveNodeValue(arg, checker, paramMap);
202
+ const res = resolveNodeValue(
203
+ unwrap(arg),
204
+ checker,
205
+ paramMap
206
+ );
183
207
 
184
- if (res && res.values.length) {
185
- res.values.forEach(v => {
186
- if (isValidEvent(v)) events.add(v);
187
- });
188
- }
208
+ res.values.forEach(v => {
209
+ if (isValidEvent(v)) events.add(v);
210
+ });
189
211
  }
190
212
  }
191
213
 
@@ -263,7 +285,7 @@ export function scanSource(
263
285
  const prop = el.name.getText();
264
286
 
265
287
  const isEvent =
266
- isPropUsedInTracking(fn, prop) ||
288
+ isPropUsedInTracking(fn, checker, prop) ||
267
289
  config.wrappers.some(
268
290
  w => w.name === tagName && w.prop === prop
269
291
  );
@@ -0,0 +1,94 @@
1
+ import ts from "typescript";
2
+
3
+ function isStringLike(type: ts.Type) {
4
+ return (
5
+ (type.flags & ts.TypeFlags.StringLike) !== 0 ||
6
+ (type.flags & ts.TypeFlags.StringLiteral) !== 0
7
+ );
8
+ }
9
+
10
+ function hasTrackLikeSignature(
11
+ type: ts.Type,
12
+ checker: ts.TypeChecker
13
+ ): boolean {
14
+ const sigs = type.getCallSignatures();
15
+
16
+ for (const sig of sigs) {
17
+ const params = sig.getParameters();
18
+ if (!params.length) continue;
19
+
20
+ const t = checker.getTypeOfSymbolAtLocation(
21
+ params[0],
22
+ params[0].valueDeclaration!
23
+ );
24
+
25
+ if (isStringLike(t)) {
26
+ return true;
27
+ }
28
+ }
29
+
30
+ return false;
31
+ }
32
+
33
+ function isEventraLikeType(
34
+ type: ts.Type,
35
+ checker: ts.TypeChecker
36
+ ): boolean {
37
+ const props = type.getProperties();
38
+
39
+ for (const prop of props) {
40
+ const decl = prop.valueDeclaration ?? prop.declarations?.[0];
41
+ if (!decl) continue;
42
+
43
+ const t = checker.getTypeOfSymbolAtLocation(prop, decl);
44
+
45
+ if (hasTrackLikeSignature(t, checker)) {
46
+ return true;
47
+ }
48
+ }
49
+
50
+ return false;
51
+ }
52
+
53
+ export function isTrackingCall(
54
+ node: ts.CallExpression,
55
+ checker: ts.TypeChecker
56
+ ): boolean {
57
+ const expr = node.expression;
58
+
59
+ // fn(...)
60
+ if (ts.isIdentifier(expr)) {
61
+ const type = checker.getTypeAtLocation(expr);
62
+ if (hasTrackLikeSignature(type, checker)) {
63
+ return true;
64
+ }
65
+ }
66
+
67
+ // obj.method(...)
68
+ if (ts.isPropertyAccessExpression(expr)) {
69
+ const objType = checker.getTypeAtLocation(expr.expression);
70
+
71
+ // объект похож на tracker
72
+ if (isEventraLikeType(objType, checker)) {
73
+ return true;
74
+ }
75
+
76
+ // fallback
77
+ const methodType = checker.getTypeAtLocation(expr.name);
78
+
79
+ if (hasTrackLikeSignature(methodType, checker)) {
80
+ return true;
81
+ }
82
+ }
83
+
84
+ // obj["track"](...)
85
+ if (ts.isElementAccessExpression(expr)) {
86
+ const objType = checker.getTypeAtLocation(expr.expression);
87
+
88
+ if (isEventraLikeType(objType, checker)) {
89
+ return true;
90
+ }
91
+ }
92
+
93
+ return false;
94
+ }
package/src/types.ts CHANGED
@@ -5,9 +5,6 @@ export type ParseResult = {
5
5
  deps: string[];
6
6
  };
7
7
 
8
- export type Parser = (content: string, file: string) => ParseResult;
9
-
10
-
11
8
  export type ComponentWrapper = {
12
9
  name: string;
13
10
  prop: string;
@@ -0,0 +1,5 @@
1
+ export function getVirtualFile(file: string) {
2
+ return /\.(vue|svelte|astro|html)$/i.test(file)
3
+ ? file + ".tsx"
4
+ : file;
5
+ }
@@ -1,19 +1,10 @@
1
- import path from "path";
2
1
  import { parseUniversal } from "./universal";
3
2
 
4
3
  export function processFile(file: string, content: string) {
5
4
  const { code, deps } = parseUniversal(content, file);
6
5
 
7
- const ext = path.extname(file);
8
-
9
- const virtualFile =
10
- /\.(vue|svelte|astro|html)$/i.test(file)
11
- ? file.replace(ext, "") + ".tsx"
12
- : file;
13
-
14
6
  return {
15
7
  content: code || "",
16
- virtualFile,
17
8
  deps: [...new Set(deps.filter(Boolean))],
18
9
  };
19
10
  }