@eventra_dev/eventra-cli 0.1.30 → 0.1.32

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.
@@ -2,9 +2,27 @@ import ts from "typescript";
2
2
  import { resolveNodeValue } from "./resolver";
3
3
  import { resolveFunctionFromCall } from "./callResolver";
4
4
  import { findTrackParamIndex } from "./functionAnalyzer";
5
- import { EventraConfig } from "../types";
5
+ import { resolveExportedSymbol } from "./exportResolver";
6
+ import { EventraConfig, ScanResult } from "../types";
6
7
  import { isExternalFile } from "./boundary";
7
8
 
9
+ // UTILS
10
+ function getJsxAttrName(name: ts.JsxAttributeName): string {
11
+ if (ts.isIdentifier(name)) return name.text;
12
+ if (ts.isJsxNamespacedName(name)) return name.name.text;
13
+ return "";
14
+ }
15
+
16
+ function getJsxTagName(tag: ts.JsxTagNameExpression): string {
17
+ if (ts.isIdentifier(tag)) return tag.text;
18
+
19
+ if (ts.isPropertyAccessExpression(tag)) {
20
+ return tag.name.text;
21
+ }
22
+
23
+ return "";
24
+ }
25
+
8
26
  function getCallName(expr: ts.Expression): string {
9
27
  if (ts.isIdentifier(expr)) return expr.text;
10
28
  if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
@@ -21,59 +39,290 @@ function isValidEvent(v: string) {
21
39
  );
22
40
  }
23
41
 
24
- export type ScanResult = {
25
- events: Set<string>;
26
- detectedFunctionWrappers: Set<string>;
27
- detectedComponentWrappers: Map<string, string>;
28
- };
42
+ // COMPONENT ANALYSIS
43
+ function getComponentPropsMap(
44
+ fn: ts.FunctionLikeDeclaration
45
+ ): Map<string, string> {
46
+ const map = new Map<string, string>();
47
+ const firstParam = fn.parameters[0];
48
+ if (!firstParam) return map;
49
+
50
+ // ({ event }) => ...
51
+ if (ts.isObjectBindingPattern(firstParam.name)) {
52
+ for (const el of firstParam.name.elements) {
53
+ if (!ts.isBindingElement(el)) continue;
54
+
55
+ const propName =
56
+ el.propertyName?.getText() || el.name.getText();
57
+
58
+ map.set(el.name.getText(), propName);
59
+ }
60
+ }
61
+
62
+ // (props) => props.event
63
+ if (ts.isIdentifier(firstParam.name)) {
64
+ map.set(firstParam.name.text, "*");
65
+ }
66
+
67
+ return map;
68
+ }
69
+
70
+ function findTrackedPropName(
71
+ fn: ts.FunctionLikeDeclaration,
72
+ checker: ts.TypeChecker
73
+ ): string | null {
74
+ const propMap = getComponentPropsMap(fn);
75
+ let result: string | null = null;
76
+
77
+ function resolveIdentifierToProp(
78
+ node: ts.Identifier
79
+ ): string | null {
80
+ const mapped = propMap.get(node.text);
81
+ if (mapped && mapped !== "*") return mapped;
82
+
83
+ const symbol = checker.getSymbolAtLocation(node);
84
+ if (!symbol) return null;
85
+
86
+ for (const decl of symbol.getDeclarations() ?? []) {
87
+ // const e = props.event
88
+ if (
89
+ ts.isVariableDeclaration(decl) &&
90
+ decl.initializer &&
91
+ ts.isPropertyAccessExpression(decl.initializer)
92
+ ) {
93
+ return decl.initializer.name.text;
94
+ }
95
+
96
+ // const e = event
97
+ if (
98
+ ts.isVariableDeclaration(decl) &&
99
+ decl.initializer &&
100
+ ts.isIdentifier(decl.initializer)
101
+ ) {
102
+ const inner = propMap.get(decl.initializer.text);
103
+ if (inner) return inner;
104
+ }
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ function visit(node: ts.Node) {
111
+ if (result) return;
112
+
113
+ if (ts.isCallExpression(node)) {
114
+ const expr = node.expression;
115
+
116
+ const isTrack =
117
+ (ts.isIdentifier(expr) && expr.text === "track") ||
118
+ (ts.isPropertyAccessExpression(expr) &&
119
+ expr.name.text === "track");
120
+
121
+ if (!isTrack) {
122
+ ts.forEachChild(node, visit);
123
+ return;
124
+ }
125
+
126
+ for (const arg of node.arguments) {
127
+ // track(event)
128
+ if (ts.isIdentifier(arg)) {
129
+ const prop = resolveIdentifierToProp(arg);
130
+ if (prop) {
131
+ result = prop;
132
+ return;
133
+ }
134
+ }
135
+
136
+ // track(props.a.b.event)
137
+ if (ts.isPropertyAccessExpression(arg)) {
138
+ let current = arg;
139
+
140
+ while (ts.isPropertyAccessExpression(current)) {
141
+ result = current.name.text;
142
+ return;
143
+ }
144
+ }
145
+
146
+ // track(fn())
147
+ if (ts.isCallExpression(arg)) {
148
+ const inner = resolveFunctionFromCall(
149
+ arg.expression,
150
+ checker
151
+ );
152
+
153
+ if (inner?.body) {
154
+ visit(inner.body);
155
+ if (result) return;
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ ts.forEachChild(node, visit);
162
+ }
163
+
164
+ if (fn.body) visit(fn.body);
165
+
166
+ return result;
167
+ }
29
168
 
169
+ // MAIN SCANNER
30
170
  export function scanSource(
31
171
  source: ts.SourceFile,
32
172
  checker: ts.TypeChecker,
33
173
  config: EventraConfig,
34
174
  visited = new Set<string>(),
35
- inheritedParamMap?: Map<string, ts.Expression>
175
+ inheritedParamMap?: Map<string, ts.Expression>,
176
+ wrapperCache = new Map<string, string | null>()
36
177
  ): ScanResult {
37
178
  const events = new Set<string>();
38
179
  const detectedFunctionWrappers = new Set<string>();
39
180
  const detectedComponentWrappers = new Map<string, string>();
40
181
 
41
- if (visited.size > 200 || visited.has(source.fileName)) {
182
+ if (visited.has(source.fileName) || visited.size > 200) {
42
183
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
43
184
  }
44
185
 
45
186
  visited.add(source.fileName);
46
187
 
47
188
  function visit(node: ts.Node) {
189
+ // JSX WRAPPERS
190
+ if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
191
+ const tagName = getJsxTagName(node.tagName);
192
+
193
+ // skip html
194
+ if (tagName[0] === tagName[0].toLowerCase()) {
195
+ ts.forEachChild(node, visit);
196
+ return;
197
+ }
198
+
199
+ let wrapper = config.wrappers.find(w => w.name === tagName);
200
+
201
+ // AUTO DETECT
202
+ if (!wrapper) {
203
+ if (wrapperCache.has(tagName)) {
204
+ const cached = wrapperCache.get(tagName);
205
+ if (cached) wrapper = { name: tagName, prop: cached };
206
+ } else {
207
+ let symbol = checker.getSymbolAtLocation(node.tagName);
208
+
209
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
210
+ symbol = checker.getAliasedSymbol(symbol);
211
+ }
212
+
213
+ if (symbol) {
214
+ symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
215
+
216
+ for (const decl of symbol.getDeclarations() ?? []) {
217
+ let fn: ts.FunctionLikeDeclaration | null = null;
218
+
219
+ if (ts.isFunctionDeclaration(decl)) fn = decl;
220
+
221
+ if (
222
+ ts.isVariableDeclaration(decl) &&
223
+ decl.initializer &&
224
+ (ts.isArrowFunction(decl.initializer) ||
225
+ ts.isFunctionExpression(decl.initializer))
226
+ ) {
227
+ fn = decl.initializer;
228
+ }
229
+
230
+ if (!fn) continue;
231
+
232
+ const prop = findTrackedPropName(fn, checker);
233
+ wrapperCache.set(tagName, prop ?? null);
234
+
235
+ if (prop) {
236
+ wrapper = { name: tagName, prop };
237
+ detectedComponentWrappers.set(tagName, prop);
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ if (wrapper) {
246
+ for (const attr of node.attributes.properties) {
247
+ if (!ts.isJsxAttribute(attr)) continue;
248
+
249
+ const name = getJsxAttrName(attr.name);
250
+ if (name !== wrapper.prop) continue;
251
+
252
+ if (!attr.initializer) continue;
253
+
254
+ if (ts.isStringLiteral(attr.initializer)) {
255
+ const v = attr.initializer.text;
256
+ if (isValidEvent(v)) events.add(v);
257
+ }
258
+
259
+ if (ts.isJsxExpression(attr.initializer)) {
260
+ const expr = attr.initializer.expression;
261
+ if (!expr) continue;
262
+
263
+ const res = resolveNodeValue(expr, checker, inheritedParamMap);
264
+
265
+ res.values.forEach(v => {
266
+ if (isValidEvent(v)) events.add(v);
267
+ });
268
+ }
269
+ }
270
+ }
271
+
272
+ ts.forEachChild(node, visit);
273
+ return;
274
+ }
275
+
276
+ // FUNCTION CALLS
48
277
  if (ts.isCallExpression(node)) {
49
- const name = getCallName(node.expression);
278
+ let trackParamIndex: number | null = null;
279
+ let isTracking = false;
280
+
281
+ const callName = getCallName(node.expression);
282
+
283
+ // direct .track()
284
+ if (
285
+ ts.isPropertyAccessExpression(node.expression) &&
286
+ node.expression.name.text === "track"
287
+ ) {
288
+ isTracking = true;
289
+ trackParamIndex = 0;
290
+ }
50
291
 
51
- const isKnownWrapper = config.functionWrappers.some(
52
- w => w.name === name
292
+ // config wrapper
293
+ const manualWrapper = config.functionWrappers.find(
294
+ w => w.name === callName
53
295
  );
54
296
 
55
- const resolvedFn = resolveFunctionFromCall(node.expression, checker);
297
+ if (manualWrapper) {
298
+ isTracking = true;
299
+ trackParamIndex = 0;
300
+ }
301
+
302
+ // resolve function → check body
303
+ const resolvedFn = resolveFunctionFromCall(
304
+ node.expression,
305
+ checker
306
+ );
307
+
308
+ if (resolvedFn) {
309
+ const idx = findTrackParamIndex(resolvedFn);
56
310
 
57
- const isAutoWrapper =
58
- !!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
311
+ if (idx !== null) {
312
+ isTracking = true;
313
+ trackParamIndex = idx;
59
314
 
60
- const isTracking =
61
- name === "track" ||
62
- isKnownWrapper ||
63
- (isAutoWrapper &&
64
- resolvedFn &&
65
- !isExternalFile(resolvedFn.getSourceFile().fileName));
315
+ if (!manualWrapper && callName !== "track") {
316
+ detectedFunctionWrappers.add(callName);
317
+ }
318
+ }
319
+ }
66
320
 
67
321
  if (!isTracking) {
68
322
  ts.forEachChild(node, visit);
69
323
  return;
70
324
  }
71
325
 
72
- // авто-детект wrapper
73
- if (!isKnownWrapper && isAutoWrapper && name !== "track") {
74
- detectedFunctionWrappers.add(name);
75
- }
76
-
77
326
  const paramMap = new Map(inheritedParamMap ?? []);
78
327
 
79
328
  if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
@@ -87,23 +336,16 @@ export function scanSource(
87
336
  });
88
337
  }
89
338
 
90
- // ✅ ТОЛЬКО правильный аргумент
91
- if (resolvedFn) {
92
- const idx = findTrackParamIndex(resolvedFn);
93
-
94
- if (idx !== null && node.arguments[idx]) {
95
- const res = resolveNodeValue(
96
- node.arguments[idx],
97
- checker,
98
- paramMap
99
- );
339
+ if (trackParamIndex !== null && node.arguments[trackParamIndex]) {
340
+ const res = resolveNodeValue(
341
+ node.arguments[trackParamIndex],
342
+ checker,
343
+ paramMap
344
+ );
100
345
 
101
- for (const v of res.values) {
102
- if (isValidEvent(v)) {
103
- events.add(v);
104
- }
105
- }
106
- }
346
+ res.values.forEach(v => {
347
+ if (isValidEvent(v)) events.add(v);
348
+ });
107
349
  }
108
350
 
109
351
  // CROSS FILE
@@ -116,7 +358,8 @@ export function scanSource(
116
358
  checker,
117
359
  config,
118
360
  visited,
119
- paramMap
361
+ paramMap,
362
+ wrapperCache
120
363
  );
121
364
 
122
365
  res.events.forEach(e => events.add(e));
package/src/types.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  export type ScanMode = "strict" | "hybrid" | "discovery";
2
2
 
3
+ export type ScanResult = {
4
+ events: Set<string>;
5
+ detectedFunctionWrappers: Set<string>;
6
+ detectedComponentWrappers: Map<string, string>;
7
+ };
8
+
3
9
  export type ParseResult = {
4
10
  code: string;
5
11
  deps: string[];
@@ -22,7 +28,6 @@ export type EventraConfig = {
22
28
  mode?: ScanMode;
23
29
  wrappers: ComponentWrapper[];
24
30
  functionWrappers: FunctionWrapper[];
25
- aliases?: Record<string, string>;
26
31
  sync: {
27
32
  include: string[];
28
33
  exclude: string[];
@@ -13,7 +13,6 @@ export function normalizeConfig(
13
13
  endpoint: config.endpoint ?? "",
14
14
  wrappers: config.wrappers ?? [],
15
15
  functionWrappers: config.functionWrappers ?? [],
16
- aliases: config.aliases ?? {},
17
16
  sync: config.sync ?? {
18
17
  include: [
19
18
  "**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
@@ -1,10 +1,10 @@
1
1
  import { parseUniversal } from "./universal";
2
2
 
3
3
  export function processFile(file: string, content: string) {
4
- const { code, deps } = parseUniversal(content, file);
4
+ const parsed = parseUniversal(content, file);
5
5
 
6
6
  return {
7
- content: code || "",
8
- deps: [...new Set(deps.filter(Boolean))],
7
+ content: parsed.code || "",
8
+ deps: [...new Set(parsed.deps.filter(Boolean))],
9
9
  };
10
10
  }