@eventra_dev/eventra-cli 0.1.25 → 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.
@@ -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;
@@ -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
  }
@@ -12,7 +12,7 @@ function stripComments(input: string): string {
12
12
  }
13
13
 
14
14
  /**
15
- * Extract JS expressions safely
15
+ * Extract JS expressions
16
16
  */
17
17
  function extractJS(input: string): string[] {
18
18
  const result: string[] = [];
@@ -26,7 +26,6 @@ function extractJS(input: string): string[] {
26
26
  while (i < len) {
27
27
  const ch = input[i];
28
28
 
29
- // STRING
30
29
  if (inString) {
31
30
  if (escape) escape = false;
32
31
  else if (ch === "\\") escape = true;
@@ -41,47 +40,20 @@ function extractJS(input: string): string[] {
41
40
  continue;
42
41
  }
43
42
 
44
- // { ... }
45
43
  if (ch === "{") {
46
44
  let depth = 1;
47
45
  let j = i + 1;
48
46
 
49
- let innerStr: '"' | "'" | "`" | null = null;
50
- let innerEsc = false;
51
-
52
47
  while (j < len && depth > 0) {
53
- const c = input[j];
54
-
55
- if (innerStr) {
56
- if (innerEsc) innerEsc = false;
57
- else if (c === "\\") innerEsc = true;
58
- else if (c === innerStr) innerStr = null;
59
- j++;
60
- continue;
61
- }
62
-
63
- if (c === '"' || c === "'" || c === "`") {
64
- innerStr = c;
65
- j++;
66
- continue;
67
- }
68
-
69
- if (c === "{") depth++;
70
- else if (c === "}") depth--;
71
-
48
+ if (input[j] === "{") depth++;
49
+ else if (input[j] === "}") depth--;
72
50
  j++;
73
51
  }
74
52
 
75
53
  if (depth === 0) {
76
54
  const expr = input.slice(i + 1, j - 1).trim();
77
55
 
78
- if (
79
- expr &&
80
- (expr.includes("(") ||
81
- expr.includes("=") ||
82
- expr.includes(".") ||
83
- expr.includes("track"))
84
- ) {
56
+ if (expr && /[a-zA-Z0-9_$]\s*\(/.test(expr)) {
85
57
  result.push(expr);
86
58
  }
87
59
 
@@ -90,64 +62,14 @@ function extractJS(input: string): string[] {
90
62
  }
91
63
  }
92
64
 
93
- // attr="..."
94
- if (ch === "=" && (input[i + 1] === '"' || input[i + 1] === "'")) {
95
- const quote = input[i + 1];
96
- let j = i + 2;
97
- let val = "";
98
-
99
- while (j < len) {
100
- const c = input[j];
101
-
102
- if (c === "\\" && input[j + 1]) {
103
- val += c + input[j + 1];
104
- j += 2;
105
- continue;
106
- }
107
-
108
- if (c === quote) break;
109
-
110
- val += c;
111
- j++;
112
- }
113
-
114
- if (
115
- val.trim() &&
116
- (val.includes("(") ||
117
- val.includes("=>") ||
118
- val.includes("track"))
119
- ) {
120
- result.push(val.trim());
121
- }
122
-
123
- i = j + 1;
124
- continue;
125
- }
126
-
127
65
  i++;
128
66
  }
129
67
 
130
- // Vue / Svelte / JSX handlers
131
- const extra = [
132
- /@[\w-]+\s*=\s*["']([\s\S]*?)["']/g,
133
- /v-on:[\w-]+\s*=\s*["']([\s\S]*?)["']/g,
134
- /on:[\w-]+\s*=\s*{([\s\S]*?)}/g,
135
- /on\w+\s*=\s*{([\s\S]*?)}/g,
136
- ];
137
-
138
- for (const r of extra) {
139
- const m = input.matchAll(r);
140
- for (const x of m) {
141
- const v = x[1]?.trim();
142
- if (v) result.push(v);
143
- }
144
- }
145
-
146
68
  return result;
147
69
  }
148
70
 
149
71
  /**
150
- * Extract loose JS but less noisy
72
+ * Loose JS extractor
151
73
  */
152
74
  function extractLooseJS(input: string): string[] {
153
75
  const result: string[] = [];
@@ -160,14 +82,7 @@ function extractLooseJS(input: string): string[] {
160
82
  if (!l) continue;
161
83
  if (l.startsWith("<")) continue;
162
84
 
163
- // strict filter
164
- if (
165
- l.includes("track") ||
166
- l.includes(".track(") ||
167
- l.includes("=>") ||
168
- l.startsWith("function") ||
169
- l.includes("(")
170
- ) {
85
+ if (/[a-zA-Z0-9_$]\s*\(/.test(l)) {
171
86
  result.push(l);
172
87
  }
173
88
  }
@@ -176,7 +91,14 @@ function extractLooseJS(input: string): string[] {
176
91
  }
177
92
 
178
93
  /**
179
- * Universal parser
94
+ * UNIVERSAL PARSER
95
+ * Support:
96
+ * - React / JSX
97
+ * - Vue
98
+ * - Svelte
99
+ * - Astro
100
+ * - HTML + JS
101
+ * - SSR
180
102
  */
181
103
  export function parseUniversal(
182
104
  content: string,
@@ -187,9 +109,11 @@ export function parseUniversal(
187
109
 
188
110
  const clean = stripComments(content);
189
111
 
190
- // Astro frontmatter
112
+ // ASTRO FRONTMATTER
191
113
  const fm = clean.match(/^---([\s\S]*?)---/);
192
- if (fm?.[1]) parts.push(fm[1]);
114
+ if (fm?.[1]) {
115
+ parts.push(fm[1]);
116
+ }
193
117
 
194
118
  // <script src="">
195
119
  const scriptSrc = clean.matchAll(
@@ -206,7 +130,7 @@ export function parseUniversal(
206
130
  deps.push(resolved);
207
131
  }
208
132
 
209
- // inline <script>
133
+ // INLINE <script>
210
134
  const scripts = clean.matchAll(
211
135
  /<script\b([^>]*)>([\s\S]*?)<\/script>/g
212
136
  );
@@ -214,7 +138,7 @@ export function parseUniversal(
214
138
  for (const m of scripts) {
215
139
  const attrs = m[1];
216
140
 
217
- // skip JSON / data
141
+ // skip JSON
218
142
  if (/type=["']application\/json/.test(attrs)) continue;
219
143
 
220
144
  if (m[2].trim()) {
@@ -222,24 +146,24 @@ export function parseUniversal(
222
146
  }
223
147
  }
224
148
 
225
- // remove scripts
149
+ // REMOVE SCRIPTS → TEMPLATE PART
226
150
  const noScripts = clean.replace(
227
151
  /<script[\s\S]*?<\/script>/g,
228
152
  ""
229
153
  );
230
154
 
231
- // loose JS
232
- const loose = extractLooseJS(noScripts);
233
- parts.push(...loose);
155
+ // LOOSE JS
156
+ parts.push(...extractLooseJS(noScripts));
234
157
 
235
- // expressions
158
+ // EXPRESSIONS
236
159
  const extracted = extractJS(clean);
237
160
  for (const e of extracted) {
238
161
  parts.push(e + ";");
239
162
  }
240
163
 
164
+ // RESULT
241
165
  return {
242
166
  code: [...new Set(parts)].join("\n"),
243
- deps,
167
+ deps: [...new Set(deps.filter(Boolean))],
244
168
  };
245
169
  }