@eventra_dev/eventra-cli 0.1.29 → 0.1.31

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,27 @@
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
- import { EventraConfig } from "../types";
5
+ import { resolveExportedSymbol } from "./exportResolver";
6
+ import { EventraConfig, ScanResult } from "../types";
7
7
  import { isExternalFile } from "./boundary";
8
- import { isTrackingCall } from "./trackingDetector";
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
+ }
9
25
 
10
26
  function getCallName(expr: ts.Expression): string {
11
27
  if (ts.isIdentifier(expr)) return expr.text;
@@ -23,56 +39,144 @@ function isValidEvent(v: string) {
23
39
  );
24
40
  }
25
41
 
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;
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;
32
49
 
33
- let symbol = checker.getSymbolAtLocation(expr);
50
+ // ({ event }) => ...
51
+ if (ts.isObjectBindingPattern(firstParam.name)) {
52
+ for (const el of firstParam.name.elements) {
53
+ if (!ts.isBindingElement(el)) continue;
34
54
 
35
- if (!symbol) return null;
55
+ const propName =
56
+ el.propertyName?.getText() || el.name.getText();
36
57
 
37
- if (symbol.flags & ts.SymbolFlags.Alias) {
38
- symbol = checker.getAliasedSymbol(symbol);
58
+ map.set(el.name.getText(), propName);
59
+ }
39
60
  }
40
61
 
41
- const resolvedSymbol =
42
- resolveExportedSymbol(symbol, checker) ?? symbol;
62
+ // (props) => props.event
63
+ if (ts.isIdentifier(firstParam.name)) {
64
+ map.set(firstParam.name.text, "*");
65
+ }
43
66
 
44
- for (const decl of resolvedSymbol.getDeclarations() ?? []) {
45
- if (ts.isFunctionDeclaration(decl)) {
46
- return decl;
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
+ }
47
105
  }
48
106
 
49
- if (
50
- ts.isVariableDeclaration(decl) &&
51
- decl.initializer &&
52
- (ts.isArrowFunction(decl.initializer) ||
53
- ts.isFunctionExpression(decl.initializer))
54
- ) {
55
- return decl.initializer;
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
+ }
56
159
  }
160
+
161
+ ts.forEachChild(node, visit);
57
162
  }
58
163
 
59
- return null;
60
- }
164
+ if (fn.body) visit(fn.body);
61
165
 
62
- export type ScanResult = {
63
- events: Set<string>;
64
- detectedFunctionWrappers: Set<string>;
65
- detectedComponentWrappers: Map<string, string>;
66
- };
166
+ return result;
167
+ }
67
168
 
169
+ // MAIN SCANNER
68
170
  export function scanSource(
69
171
  source: ts.SourceFile,
70
172
  checker: ts.TypeChecker,
71
173
  config: EventraConfig,
72
174
  visited = new Set<string>(),
73
- depth = 0,
74
- inheritedParamMap?: Map<string, ts.Expression>
175
+ inheritedParamMap?: Map<string, ts.Expression>,
176
+ wrapperCache?: Map<string, string | null>
75
177
  ): ScanResult {
178
+ wrapperCache = wrapperCache ?? new Map();
179
+
76
180
  const events = new Set<string>();
77
181
  const detectedFunctionWrappers = new Set<string>();
78
182
  const detectedComponentWrappers = new Map<string, string>();
@@ -84,63 +188,149 @@ export function scanSource(
84
188
  visited.add(source.fileName);
85
189
 
86
190
  function visit(node: ts.Node) {
87
- // CALL EXPRESSIONS
191
+ // JSX WRAPPERS
192
+ if (
193
+ ts.isJsxSelfClosingElement(node) ||
194
+ ts.isJsxOpeningElement(node)
195
+ ) {
196
+ const tagName = getJsxTagName(node.tagName);
197
+
198
+ // skip html
199
+ if (!tagName || tagName[0] === tagName[0].toLowerCase()) {
200
+ ts.forEachChild(node, visit);
201
+ return;
202
+ }
203
+
204
+ let wrapper = config.wrappers.find(
205
+ (w) => w.name === tagName
206
+ );
207
+
208
+ if (!wrapper) {
209
+ let symbol = checker.getSymbolAtLocation(node.tagName);
210
+
211
+ let cacheKey = tagName;
212
+
213
+ if (symbol) {
214
+ if (symbol.flags & ts.SymbolFlags.Alias) {
215
+ symbol = checker.getAliasedSymbol(symbol);
216
+ }
217
+
218
+ symbol =
219
+ resolveExportedSymbol(symbol, checker) ?? symbol;
220
+
221
+ cacheKey = checker.getFullyQualifiedName(symbol);
222
+ }
223
+
224
+ if (wrapperCache?.has(cacheKey)) {
225
+ const cached = wrapperCache.get(cacheKey);
226
+ if (cached) {
227
+ wrapper = { name: tagName, prop: cached };
228
+ }
229
+ } else if (symbol) {
230
+ let detected: string | null = null;
231
+
232
+ for (const decl of symbol.getDeclarations() ?? []) {
233
+ let fn: ts.FunctionLikeDeclaration | null = null;
234
+
235
+ if (ts.isFunctionDeclaration(decl)) fn = decl;
236
+
237
+ if (
238
+ ts.isVariableDeclaration(decl) &&
239
+ decl.initializer &&
240
+ (ts.isArrowFunction(decl.initializer) ||
241
+ ts.isFunctionExpression(decl.initializer))
242
+ ) {
243
+ fn = decl.initializer;
244
+ }
245
+
246
+ if (!fn) continue;
247
+
248
+ const prop = findTrackedPropName(fn, checker);
249
+
250
+ if (prop) {
251
+ detected = prop;
252
+ break;
253
+ }
254
+ }
255
+
256
+ wrapperCache?.set(cacheKey, detected);
257
+
258
+ if (detected) {
259
+ wrapper = { name: tagName, prop: detected };
260
+ detectedComponentWrappers.set(tagName, detected);
261
+ }
262
+ }
263
+ }
264
+
265
+ if (wrapper) {
266
+ for (const attr of node.attributes.properties) {
267
+ if (!ts.isJsxAttribute(attr)) continue;
268
+
269
+ const name = getJsxAttrName(attr.name);
270
+ if (name !== wrapper.prop) continue;
271
+ if (!attr.initializer) continue;
272
+
273
+ if (ts.isStringLiteral(attr.initializer)) {
274
+ if (isValidEvent(attr.initializer.text)) {
275
+ events.add(attr.initializer.text);
276
+ }
277
+ }
278
+
279
+ if (ts.isJsxExpression(attr.initializer)) {
280
+ const expr = attr.initializer.expression;
281
+ if (!expr) continue;
282
+
283
+ const res = resolveNodeValue(
284
+ expr,
285
+ checker,
286
+ inheritedParamMap
287
+ );
288
+
289
+ res.values.forEach((v) => {
290
+ if (isValidEvent(v)) events.add(v);
291
+ });
292
+ }
293
+ }
294
+ }
295
+
296
+ ts.forEachChild(node, visit);
297
+ return;
298
+ }
299
+
300
+ // FUNCTION TRACKING
88
301
  if (ts.isCallExpression(node)) {
89
302
  const name = getCallName(node.expression);
90
303
 
91
304
  const isKnownWrapper = config.functionWrappers.some(
92
- w => w.name === name
305
+ (w) => w.name === name
93
306
  );
94
307
 
95
- const isTrackingByType = isTrackingCall(node, checker);
96
-
97
- const resolvedFn = resolveFunctionDeep(
308
+ const resolvedFn = resolveFunctionFromCall(
98
309
  node.expression,
99
310
  checker
100
311
  );
101
312
 
102
313
  const isAutoWrapper =
103
- resolvedFn && findTrackParamIndex(resolvedFn) !== null;
314
+ !!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
104
315
 
105
- let isTracking =
316
+ const isTracking =
317
+ name === "track" ||
106
318
  isKnownWrapper ||
107
- isTrackingByType ||
108
- name.startsWith("$") ||
109
319
  (isAutoWrapper &&
110
320
  resolvedFn &&
111
- resolvedFn.getSourceFile &&
112
- !isExternalFile(resolvedFn.getSourceFile().fileName));
113
-
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
- }
321
+ !isExternalFile(
322
+ resolvedFn.getSourceFile().fileName
323
+ ));
127
324
 
128
325
  if (!isTracking) {
129
326
  ts.forEachChild(node, visit);
130
327
  return;
131
328
  }
132
329
 
133
- // DETECT WRAPPER
134
- if (
135
- !isKnownWrapper &&
136
- isAutoWrapper &&
137
- !isTrackingByType &&
138
- !name.startsWith("track")
139
- ) {
330
+ if (!isKnownWrapper && isAutoWrapper && name !== "track") {
140
331
  detectedFunctionWrappers.add(name);
141
332
  }
142
333
 
143
- // PARAM MAP
144
334
  const paramMap = new Map(inheritedParamMap ?? []);
145
335
 
146
336
  if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
@@ -154,7 +344,6 @@ export function scanSource(
154
344
  });
155
345
  }
156
346
 
157
- // STRICT EVENT RESOLVE
158
347
  if (resolvedFn) {
159
348
  const idx = findTrackParamIndex(resolvedFn);
160
349
 
@@ -165,27 +354,26 @@ export function scanSource(
165
354
  paramMap
166
355
  );
167
356
 
168
- res.values.forEach(v => {
357
+ res.values.forEach((v) => {
169
358
  if (isValidEvent(v)) events.add(v);
170
359
  });
171
360
  }
172
361
  }
173
362
 
174
- // CROSS-FILE
175
363
  if (resolvedFn) {
176
- const sf = resolvedFn.getSourceFile?.();
364
+ const sf = resolvedFn.getSourceFile();
177
365
 
178
- if (sf && !visited.has(sf.fileName)) {
366
+ if (!visited.has(sf.fileName)) {
179
367
  const res = scanSource(
180
368
  sf,
181
369
  checker,
182
370
  config,
183
371
  visited,
184
- depth + 1,
185
- paramMap
372
+ paramMap,
373
+ wrapperCache
186
374
  );
187
375
 
188
- res.events.forEach(e => events.add(e));
376
+ res.events.forEach((e) => events.add(e));
189
377
  }
190
378
  }
191
379
  }
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
  }