@eventra_dev/eventra-cli 0.1.28 → 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;
@@ -13,25 +11,6 @@ function getCallName(expr: ts.Expression): string {
13
11
  return "";
14
12
  }
15
13
 
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
- }
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
-
35
14
  function isValidEvent(v: string) {
36
15
  return (
37
16
  v &&
@@ -42,43 +21,6 @@ function isValidEvent(v: string) {
42
21
  );
43
22
  }
44
23
 
45
- function isPropUsedInTracking(
46
- fn: ts.FunctionLikeDeclaration,
47
- checker: ts.TypeChecker,
48
- prop: string
49
- ) {
50
- let found = false;
51
-
52
- function visit(node: ts.Node) {
53
- if (ts.isCallExpression(node)) {
54
- if (!isTrackingCall(node, checker)) {
55
- ts.forEachChild(node, visit);
56
- return;
57
- }
58
-
59
- for (const arg of node.arguments) {
60
- const a = unwrap(arg as ts.Expression);
61
-
62
- if (
63
- (ts.isIdentifier(a) && a.text === prop) ||
64
- (ts.isPropertyAccessExpression(a) &&
65
- a.name.text === prop)
66
- ) {
67
- found = true;
68
- }
69
- }
70
- }
71
-
72
- ts.forEachChild(node, visit);
73
- }
74
-
75
- if (fn.body) {
76
- visit(fn.body);
77
- }
78
-
79
- return found;
80
- }
81
-
82
24
  export type ScanResult = {
83
25
  events: Set<string>;
84
26
  detectedFunctionWrappers: Set<string>;
@@ -90,7 +32,6 @@ export function scanSource(
90
32
  checker: ts.TypeChecker,
91
33
  config: EventraConfig,
92
34
  visited = new Set<string>(),
93
- depth = 0,
94
35
  inheritedParamMap?: Map<string, ts.Expression>
95
36
  ): ScanResult {
96
37
  const events = new Set<string>();
@@ -104,7 +45,6 @@ export function scanSource(
104
45
  visited.add(source.fileName);
105
46
 
106
47
  function visit(node: ts.Node) {
107
- // CALL EXPRESSIONS
108
48
  if (ts.isCallExpression(node)) {
109
49
  const name = getCallName(node.expression);
110
50
 
@@ -112,50 +52,28 @@ export function scanSource(
112
52
  w => w.name === name
113
53
  );
114
54
 
115
- const isTrackingByType = isTrackingCall(node, checker);
116
-
117
- let resolvedFn = resolveFunctionFromCall(node.expression, checker);
55
+ const resolvedFn = resolveFunctionFromCall(node.expression, checker);
118
56
 
119
57
  const isAutoWrapper =
120
- resolvedFn && findTrackParamIndex(resolvedFn) !== null;
58
+ !!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
121
59
 
122
- let isTracking =
60
+ const isTracking =
61
+ name === "track" ||
123
62
  isKnownWrapper ||
124
- isTrackingByType ||
125
- isAutoWrapper ||
126
- name.startsWith("$");
127
-
128
- // fallback track(...)
129
- if (!isTracking) {
130
- if (
131
- ts.isPropertyAccessExpression(node.expression) &&
132
- node.expression.name.text === "track"
133
- ) {
134
- isTracking = true;
135
- } else if (
136
- ts.isIdentifier(node.expression) &&
137
- node.expression.text === "track"
138
- ) {
139
- isTracking = true;
140
- }
141
- }
63
+ (isAutoWrapper &&
64
+ resolvedFn &&
65
+ !isExternalFile(resolvedFn.getSourceFile().fileName));
142
66
 
143
67
  if (!isTracking) {
144
68
  ts.forEachChild(node, visit);
145
69
  return;
146
70
  }
147
71
 
148
- // DETECT WRAPPERS
149
- if (
150
- !isKnownWrapper &&
151
- isAutoWrapper &&
152
- !isTrackingByType &&
153
- !name.startsWith("track")
154
- ) {
72
+ // авто-детект wrapper
73
+ if (!isKnownWrapper && isAutoWrapper && name !== "track") {
155
74
  detectedFunctionWrappers.add(name);
156
75
  }
157
76
 
158
- // PARAM MAP
159
77
  const paramMap = new Map(inheritedParamMap ?? []);
160
78
 
161
79
  if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
@@ -169,9 +87,7 @@ export function scanSource(
169
87
  });
170
88
  }
171
89
 
172
- let handled = false;
173
-
174
- // MAIN RESOLVE
90
+ // ✅ ТОЛЬКО правильный аргумент
175
91
  if (resolvedFn) {
176
92
  const idx = findTrackParamIndex(resolvedFn);
177
93
 
@@ -182,22 +98,11 @@ export function scanSource(
182
98
  paramMap
183
99
  );
184
100
 
185
- res.values.forEach(v => {
186
- if (isValidEvent(v)) events.add(v);
187
- });
188
-
189
- handled = true;
190
- }
191
- }
192
-
193
- // fallback
194
- if (!handled) {
195
- for (const arg of node.arguments) {
196
- const res = resolveNodeValue(arg, checker, paramMap);
197
-
198
- res.values.forEach(v => {
199
- if (isValidEvent(v)) events.add(v);
200
- });
101
+ for (const v of res.values) {
102
+ if (isValidEvent(v)) {
103
+ events.add(v);
104
+ }
105
+ }
201
106
  }
202
107
  }
203
108
 
@@ -211,7 +116,6 @@ export function scanSource(
211
116
  checker,
212
117
  config,
213
118
  visited,
214
- depth + 1,
215
119
  paramMap
216
120
  );
217
121
 
@@ -220,100 +124,6 @@ export function scanSource(
220
124
  }
221
125
  }
222
126
 
223
- // JSX
224
- if (
225
- ts.isJsxSelfClosingElement(node) ||
226
- ts.isJsxOpeningElement(node)
227
- ) {
228
- const tagNode =
229
- ts.isPropertyAccessExpression(node.tagName)
230
- ? node.tagName.name
231
- : node.tagName;
232
-
233
- let symbol = checker.getSymbolAtLocation(tagNode);
234
-
235
- if (symbol) {
236
- symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
237
- }
238
-
239
- if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
240
- symbol = checker.getAliasedSymbol(symbol);
241
- }
242
-
243
- if (!symbol) {
244
- ts.forEachChild(node, visit);
245
- return;
246
- }
247
-
248
- const tagName =
249
- ts.isPropertyAccessExpression(node.tagName)
250
- ? node.tagName.name.getText()
251
- : node.tagName.getText();
252
-
253
- for (const decl of symbol.getDeclarations() ?? []) {
254
- let fn: ts.FunctionLikeDeclaration | null = null;
255
-
256
- if (ts.isFunctionDeclaration(decl)) fn = decl;
257
-
258
- if (ts.isVariableDeclaration(decl) && decl.initializer) {
259
- const init = decl.initializer;
260
-
261
- if (
262
- ts.isArrowFunction(init) ||
263
- ts.isFunctionExpression(init)
264
- ) {
265
- fn = init;
266
- }
267
- }
268
-
269
- if (!fn) continue;
270
-
271
- const param = fn.parameters[0];
272
- if (!param || !ts.isObjectBindingPattern(param.name)) continue;
273
-
274
- for (const el of param.name.elements) {
275
- const prop = el.name.getText();
276
-
277
- const isEvent =
278
- isPropUsedInTracking(fn, checker, prop) ||
279
- config.wrappers.some(
280
- w => w.name === tagName && w.prop === prop
281
- );
282
-
283
- if (!isEvent) continue;
284
-
285
- const attr = node.attributes.properties.find(p => {
286
- if (!ts.isJsxAttribute(p)) return false;
287
- return getJsxAttrName(p.name) === prop;
288
- }) as ts.JsxAttribute | undefined;
289
-
290
- if (!attr || !attr.initializer) continue;
291
-
292
- const expr = ts.isJsxExpression(attr.initializer)
293
- ? attr.initializer.expression
294
- : attr.initializer;
295
-
296
- if (!expr) continue;
297
-
298
- const paramMap = new Map<string, ts.Expression>();
299
- paramMap.set(prop, expr);
300
-
301
- const res = scanSource(
302
- fn.getSourceFile(),
303
- checker,
304
- config,
305
- visited,
306
- depth + 1,
307
- paramMap
308
- );
309
-
310
- res.events.forEach(e => events.add(e));
311
-
312
- detectedComponentWrappers.set(tagName, prop);
313
- }
314
- }
315
- }
316
-
317
127
  ts.forEachChild(node, visit);
318
128
  }
319
129
 
@@ -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
+ );
18
24
 
19
- let inString: '"' | "'" | "`" | null = null;
20
- let escape = false;
25
+ for (const m of matches) {
26
+ if (looksLikeTracking(m[1])) {
27
+ result.push(m[1]);
28
+ }
29
+ }
21
30
 
22
- while (i < len) {
23
- const ch = input[i];
31
+ return result;
32
+ }
24
33
 
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
- }
34
+ // Vue handlers (@click / v-on)
35
+ function extractVueHandlers(input: string): string[] {
36
+ const result: string[] = [];
32
37
 
33
- if (ch === '"' || ch === "'" || ch === "`") {
34
- inString = ch;
35
- i++;
36
- continue;
37
- }
38
+ const matches = input.matchAll(
39
+ /(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g
40
+ );
38
41
 
39
- if (ch === "{") {
40
- let depth = 1;
41
- let j = i + 1;
42
+ for (const m of matches) {
43
+ if (looksLikeTracking(m[1])) {
44
+ result.push(m[1]);
45
+ }
46
+ }
42
47
 
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 handlers (onclick="...")
68
- function extractInlineHandlers(input: string): string[] {
68
+ // JSX / template { ... }
69
+ function extractJSExpressions(input: string): string[] {
69
70
  const result: string[] = [];
70
71
 
71
- const regex = /\bon\w+\s*=\s*["']([^"']+)["']/g;
72
+ const matches = input.matchAll(/\{([^}]+)\}/g);
72
73
 
73
- let match;
74
- while ((match = regex.exec(input))) {
75
- const code = match[1].trim();
76
-
77
- if (code && /[a-zA-Z0-9_$]\s*\(/.test(code)) {
78
- result.push(code);
74
+ for (const m of matches) {
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
@@ -90,47 +88,25 @@ function extractDynamicImports(
90
88
  const deps: string[] = [];
91
89
  const calls: string[] = [];
92
90
 
93
- const regex = /import\(\s*["'](.+?)["']\s*\)/g;
91
+ const matches = input.matchAll(
92
+ /import\(\s*["'](.+?)["']\s*\)/g
93
+ );
94
94
 
95
- let match;
96
- while ((match = regex.exec(input))) {
97
- const mod = match[1];
95
+ for (const m of matches) {
96
+ const mod = m[1];
98
97
 
99
98
  const resolved = mod.startsWith("/")
100
99
  ? path.resolve(process.cwd(), "." + mod)
101
100
  : path.resolve(path.dirname(file), mod);
102
101
 
103
102
  deps.push(resolved);
104
- }
105
-
106
- // also extract calls chained after import
107
- const callRegex = /import\([^)]+\)\.then\(([^)]+)\)/g;
108
-
109
- while ((match = callRegex.exec(input))) {
110
- calls.push(match[1]);
111
- }
112
-
113
- return { deps, calls };
114
- }
115
-
116
- // Loose JS extractor
117
- function extractLooseJS(input: string): string[] {
118
- const result: string[] = [];
119
-
120
- const lines = input.split("\n");
121
103
 
122
- for (const line of lines) {
123
- const l = line.trim();
124
-
125
- if (!l) continue;
126
- if (l.startsWith("<")) continue;
127
-
128
- if (/[a-zA-Z0-9_$]\s*\(/.test(l)) {
129
- result.push(l);
104
+ if (looksLikeTracking(mod)) {
105
+ calls.push(`import("${mod}")`);
130
106
  }
131
107
  }
132
108
 
133
- return result;
109
+ return { deps, calls };
134
110
  }
135
111
 
136
112
  // UNIVERSAL PARSER
@@ -170,38 +146,30 @@ export function parseUniversal(
170
146
  );
171
147
 
172
148
  for (const m of scripts) {
173
- const attrs = m[1];
174
-
175
- if (/type=["']application\/json/.test(attrs)) continue;
176
-
177
149
  if (m[2].trim()) {
178
150
  parts.push(m[2]);
179
151
  }
180
152
  }
181
153
 
182
- // REMOVE scripts
154
+ // TEMPLATE PART
183
155
  const noScripts = clean.replace(
184
156
  /<script[\s\S]*?<\/script>/g,
185
157
  ""
186
158
  );
187
159
 
188
- // INLINE HANDLERS
189
- parts.push(...extractInlineHandlers(noScripts));
160
+ // handlers
161
+ parts.push(...extractHTMLHandlers(noScripts));
162
+ parts.push(...extractVueHandlers(noScripts));
163
+ parts.push(...extractSvelteHandlers(noScripts));
164
+
165
+ // JSX / Astro / Vue expressions
166
+ parts.push(...extractJSExpressions(clean));
190
167
 
191
- // DYNAMIC IMPORTS
168
+ // dynamic imports
192
169
  const dyn = extractDynamicImports(clean, file);
193
170
  deps.push(...dyn.deps);
194
171
  parts.push(...dyn.calls);
195
172
 
196
- // LOOSE JS
197
- parts.push(...extractLooseJS(noScripts));
198
-
199
- // EXPRESSIONS
200
- const extracted = extractJS(clean);
201
- for (const e of extracted) {
202
- parts.push(e + ";");
203
- }
204
-
205
173
  return {
206
174
  code: [...new Set(parts)].join("\n"),
207
175
  deps: [...new Set(deps.filter(Boolean))],