@eventra_dev/eventra-cli 0.1.29 → 0.1.30

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,11 +1,9 @@
1
1
  import ts from "typescript";
2
2
  import { resolveNodeValue } from "./resolver";
3
3
  import { resolveFunctionFromCall } from "./callResolver";
4
- import { resolveExportedSymbol } from "./exportResolver";
5
4
  import { findTrackParamIndex } from "./functionAnalyzer";
6
5
  import { EventraConfig } from "../types";
7
6
  import { isExternalFile } from "./boundary";
8
- import { isTrackingCall } from "./trackingDetector";
9
7
 
10
8
  function getCallName(expr: ts.Expression): string {
11
9
  if (ts.isIdentifier(expr)) return expr.text;
@@ -23,42 +21,6 @@ function isValidEvent(v: string) {
23
21
  );
24
22
  }
25
23
 
26
- function resolveFunctionDeep(
27
- expr: ts.Expression,
28
- checker: ts.TypeChecker
29
- ): ts.FunctionLikeDeclaration | null {
30
- let fn = resolveFunctionFromCall(expr, checker);
31
- if (fn) return fn;
32
-
33
- let symbol = checker.getSymbolAtLocation(expr);
34
-
35
- if (!symbol) return null;
36
-
37
- if (symbol.flags & ts.SymbolFlags.Alias) {
38
- symbol = checker.getAliasedSymbol(symbol);
39
- }
40
-
41
- const resolvedSymbol =
42
- resolveExportedSymbol(symbol, checker) ?? symbol;
43
-
44
- for (const decl of resolvedSymbol.getDeclarations() ?? []) {
45
- if (ts.isFunctionDeclaration(decl)) {
46
- return decl;
47
- }
48
-
49
- if (
50
- ts.isVariableDeclaration(decl) &&
51
- decl.initializer &&
52
- (ts.isArrowFunction(decl.initializer) ||
53
- ts.isFunctionExpression(decl.initializer))
54
- ) {
55
- return decl.initializer;
56
- }
57
- }
58
-
59
- return null;
60
- }
61
-
62
24
  export type ScanResult = {
63
25
  events: Set<string>;
64
26
  detectedFunctionWrappers: Set<string>;
@@ -70,7 +32,6 @@ export function scanSource(
70
32
  checker: ts.TypeChecker,
71
33
  config: EventraConfig,
72
34
  visited = new Set<string>(),
73
- depth = 0,
74
35
  inheritedParamMap?: Map<string, ts.Expression>
75
36
  ): ScanResult {
76
37
  const events = new Set<string>();
@@ -84,7 +45,6 @@ export function scanSource(
84
45
  visited.add(source.fileName);
85
46
 
86
47
  function visit(node: ts.Node) {
87
- // CALL EXPRESSIONS
88
48
  if (ts.isCallExpression(node)) {
89
49
  const name = getCallName(node.expression);
90
50
 
@@ -92,55 +52,28 @@ export function scanSource(
92
52
  w => w.name === name
93
53
  );
94
54
 
95
- const isTrackingByType = isTrackingCall(node, checker);
96
-
97
- const resolvedFn = resolveFunctionDeep(
98
- node.expression,
99
- checker
100
- );
55
+ const resolvedFn = resolveFunctionFromCall(node.expression, checker);
101
56
 
102
57
  const isAutoWrapper =
103
- resolvedFn && findTrackParamIndex(resolvedFn) !== null;
58
+ !!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
104
59
 
105
- let isTracking =
60
+ const isTracking =
61
+ name === "track" ||
106
62
  isKnownWrapper ||
107
- isTrackingByType ||
108
- name.startsWith("$") ||
109
63
  (isAutoWrapper &&
110
64
  resolvedFn &&
111
- resolvedFn.getSourceFile &&
112
65
  !isExternalFile(resolvedFn.getSourceFile().fileName));
113
66
 
114
- if (!isTracking) {
115
- if (
116
- ts.isPropertyAccessExpression(node.expression) &&
117
- node.expression.name.text === "track"
118
- ) {
119
- isTracking = true;
120
- } else if (
121
- ts.isIdentifier(node.expression) &&
122
- node.expression.text === "track"
123
- ) {
124
- isTracking = true;
125
- }
126
- }
127
-
128
67
  if (!isTracking) {
129
68
  ts.forEachChild(node, visit);
130
69
  return;
131
70
  }
132
71
 
133
- // DETECT WRAPPER
134
- if (
135
- !isKnownWrapper &&
136
- isAutoWrapper &&
137
- !isTrackingByType &&
138
- !name.startsWith("track")
139
- ) {
72
+ // авто-детект wrapper
73
+ if (!isKnownWrapper && isAutoWrapper && name !== "track") {
140
74
  detectedFunctionWrappers.add(name);
141
75
  }
142
76
 
143
- // PARAM MAP
144
77
  const paramMap = new Map(inheritedParamMap ?? []);
145
78
 
146
79
  if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
@@ -154,7 +87,7 @@ export function scanSource(
154
87
  });
155
88
  }
156
89
 
157
- // STRICT EVENT RESOLVE
90
+ // ✅ ТОЛЬКО правильный аргумент
158
91
  if (resolvedFn) {
159
92
  const idx = findTrackParamIndex(resolvedFn);
160
93
 
@@ -165,23 +98,24 @@ export function scanSource(
165
98
  paramMap
166
99
  );
167
100
 
168
- res.values.forEach(v => {
169
- if (isValidEvent(v)) events.add(v);
170
- });
101
+ for (const v of res.values) {
102
+ if (isValidEvent(v)) {
103
+ events.add(v);
104
+ }
105
+ }
171
106
  }
172
107
  }
173
108
 
174
- // CROSS-FILE
109
+ // CROSS FILE
175
110
  if (resolvedFn) {
176
- const sf = resolvedFn.getSourceFile?.();
111
+ const sf = resolvedFn.getSourceFile();
177
112
 
178
- if (sf && !visited.has(sf.fileName)) {
113
+ if (!visited.has(sf.fileName)) {
179
114
  const res = scanSource(
180
115
  sf,
181
116
  checker,
182
117
  config,
183
118
  visited,
184
- depth + 1,
185
119
  paramMap
186
120
  );
187
121
 
@@ -1,93 +1,16 @@
1
1
  import ts from "typescript";
2
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
3
  export function isTrackingCall(
54
- node: ts.CallExpression,
55
- checker: ts.TypeChecker
4
+ node: ts.CallExpression
56
5
  ): boolean {
57
6
  const expr = node.expression;
58
7
 
59
- // fn(...)
60
8
  if (ts.isIdentifier(expr)) {
61
- const type = checker.getTypeAtLocation(expr);
62
- if (hasTrackLikeSignature(type, checker)) {
63
- return true;
64
- }
9
+ return expr.text === "track";
65
10
  }
66
11
 
67
- // obj.method(...)
68
12
  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
- }
13
+ return expr.name.text === "track";
91
14
  }
92
15
 
93
16
  return false;
@@ -1,7 +1,7 @@
1
1
  import path from "path";
2
2
  import { ParseResult } from "../types";
3
3
 
4
- // Strip comments (HTML + JS)
4
+ // Remove comments
5
5
  function stripComments(input: string): string {
6
6
  return input
7
7
  .replace(/<!--[\s\S]*?-->/g, "")
@@ -9,80 +9,78 @@ function stripComments(input: string): string {
9
9
  .replace(/\/\/.*$/gm, "");
10
10
  }
11
11
 
12
- // Extract JS expressions inside {}
13
- function extractJS(input: string): string[] {
12
+ // Basic filter
13
+ function looksLikeTracking(code: string) {
14
+ return /track/i.test(code);
15
+ }
16
+
17
+ // HTML inline handlers (onclick="")
18
+ function extractHTMLHandlers(input: string): string[] {
14
19
  const result: string[] = [];
15
20
 
16
- let i = 0;
17
- const len = input.length;
21
+ const matches = input.matchAll(
22
+ /\bon\w+\s*=\s*["']([^"']+)["']/g
23
+ );
24
+
25
+ for (const m of matches) {
26
+ if (looksLikeTracking(m[1])) {
27
+ result.push(m[1]);
28
+ }
29
+ }
18
30
 
19
- let inString: '"' | "'" | "`" | null = null;
20
- let escape = false;
31
+ return result;
32
+ }
21
33
 
22
- while (i < len) {
23
- const ch = input[i];
34
+ // Vue handlers (@click / v-on)
35
+ function extractVueHandlers(input: string): string[] {
36
+ const result: string[] = [];
24
37
 
25
- if (inString) {
26
- if (escape) escape = false;
27
- else if (ch === "\\") escape = true;
28
- else if (ch === inString) inString = null;
29
- i++;
30
- continue;
31
- }
38
+ const matches = input.matchAll(
39
+ /(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g
40
+ );
32
41
 
33
- if (ch === '"' || ch === "'" || ch === "`") {
34
- inString = ch;
35
- i++;
36
- continue;
42
+ for (const m of matches) {
43
+ if (looksLikeTracking(m[1])) {
44
+ result.push(m[1]);
37
45
  }
46
+ }
38
47
 
39
- if (ch === "{") {
40
- let depth = 1;
41
- let j = i + 1;
42
-
43
- while (j < len && depth > 0) {
44
- if (input[j] === "{") depth++;
45
- else if (input[j] === "}") depth--;
46
- j++;
47
- }
48
+ return result;
49
+ }
48
50
 
49
- if (depth === 0) {
50
- const expr = input.slice(i + 1, j - 1).trim();
51
+ // Svelte handlers (on:click)
52
+ function extractSvelteHandlers(input: string): string[] {
53
+ const result: string[] = [];
51
54
 
52
- if (expr && /[a-zA-Z0-9_$]\s*\(/.test(expr)) {
53
- result.push(expr);
54
- }
55
+ const matches = input.matchAll(
56
+ /\bon:[\w]+\s*=\s*{([^}]+)}/g
57
+ );
55
58
 
56
- i = j;
57
- continue;
58
- }
59
+ for (const m of matches) {
60
+ if (looksLikeTracking(m[1])) {
61
+ result.push(m[1]);
59
62
  }
60
-
61
- i++;
62
63
  }
63
64
 
64
65
  return result;
65
66
  }
66
67
 
67
- // Extract inline HTML handlers
68
- function extractInlineHandlers(input: string): string[] {
68
+ // JSX / template { ... }
69
+ function extractJSExpressions(input: string): string[] {
69
70
  const result: string[] = [];
70
71
 
71
- const matches = input.matchAll(
72
- /\bon\w+\s*=\s*["']([^"']+)["']/g
73
- );
72
+ const matches = input.matchAll(/\{([^}]+)\}/g);
74
73
 
75
74
  for (const m of matches) {
76
- const code = m[1];
77
- if (/[a-zA-Z0-9_$]\s*\(/.test(code)) {
78
- result.push(code);
75
+ if (looksLikeTracking(m[1])) {
76
+ result.push(m[1]);
79
77
  }
80
78
  }
81
79
 
82
80
  return result;
83
81
  }
84
82
 
85
- // Extract dynamic imports
83
+ // dynamic import()
86
84
  function extractDynamicImports(
87
85
  input: string,
88
86
  file: string
@@ -102,17 +100,15 @@ function extractDynamicImports(
102
100
  : path.resolve(path.dirname(file), mod);
103
101
 
104
102
  deps.push(resolved);
105
- calls.push(`import("${mod}")`);
103
+
104
+ if (looksLikeTracking(mod)) {
105
+ calls.push(`import("${mod}")`);
106
+ }
106
107
  }
107
108
 
108
109
  return { deps, calls };
109
110
  }
110
111
 
111
- // Filter only probable tracking-related code
112
- function isProbablyTracking(code: string): boolean {
113
- return /track|event|analytics/i.test(code);
114
- }
115
-
116
112
  // UNIVERSAL PARSER
117
113
  export function parseUniversal(
118
114
  content: string,
@@ -150,39 +146,29 @@ export function parseUniversal(
150
146
  );
151
147
 
152
148
  for (const m of scripts) {
153
- const attrs = m[1];
154
-
155
- if (/type=["']application\/json/.test(attrs)) continue;
156
-
157
149
  if (m[2].trim()) {
158
150
  parts.push(m[2]);
159
151
  }
160
152
  }
161
153
 
162
- // REMOVE SCRIPTS → TEMPLATE PART
154
+ // TEMPLATE PART
163
155
  const noScripts = clean.replace(
164
156
  /<script[\s\S]*?<\/script>/g,
165
157
  ""
166
158
  );
167
159
 
168
- // INLINE HANDLERS
169
- parts.push(
170
- ...extractInlineHandlers(noScripts).filter(isProbablyTracking)
171
- );
160
+ // handlers
161
+ parts.push(...extractHTMLHandlers(noScripts));
162
+ parts.push(...extractVueHandlers(noScripts));
163
+ parts.push(...extractSvelteHandlers(noScripts));
172
164
 
173
- // DYNAMIC IMPORTS
165
+ // JSX / Astro / Vue expressions
166
+ parts.push(...extractJSExpressions(clean));
167
+
168
+ // dynamic imports
174
169
  const dyn = extractDynamicImports(clean, file);
175
170
  deps.push(...dyn.deps);
176
- parts.push(...dyn.calls.filter(isProbablyTracking));
177
-
178
- // EXPRESSIONS
179
- const extracted = extractJS(clean);
180
-
181
- for (const e of extracted) {
182
- if (isProbablyTracking(e)) {
183
- parts.push(e + ";");
184
- }
185
- }
171
+ parts.push(...dyn.calls);
186
172
 
187
173
  return {
188
174
  code: [...new Set(parts)].join("\n"),