@eventra_dev/eventra-cli 2.0.7 → 2.0.8

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.
@@ -24,7 +24,7 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
24
24
  }
25
25
  }
26
26
  if (resolved.dynamic) {
27
- (0, dynamicOccurrence_1.recordDynamicOccurrence)(call.node, resolved.values, dynamicOccurrences);
27
+ (0, dynamicOccurrence_1.recordDynamicOccurrence)(call.node, call.calleeText, resolved.values, dynamicOccurrences);
28
28
  }
29
29
  }
30
30
  }
@@ -68,7 +68,10 @@ function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCa
68
68
  }
69
69
  }
70
70
  function extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events, dynamicOccurrences) {
71
- const argument = call.arguments[propagation.sourceParameterIndex];
71
+ // Call site can omit the argument and rely on the parameter's own default
72
+ // value (`function f(event = "x") {}` called as `f()`) - that default is a
73
+ // real static literal, so fall back to it instead of dropping the call site.
74
+ const argument = call.arguments[propagation.sourceParameterIndex] ?? propagation.sourceParameter.initializer;
72
75
  if (!argument) {
73
76
  return;
74
77
  }
@@ -116,6 +119,6 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
116
119
  }
117
120
  }
118
121
  if (resolved.dynamic) {
119
- (0, dynamicOccurrence_1.recordDynamicOccurrence)(call, resolved.values, dynamicOccurrences);
122
+ (0, dynamicOccurrence_1.recordDynamicOccurrence)(call, call.expression.getText(), resolved.values, dynamicOccurrences);
120
123
  }
121
124
  }
@@ -71,6 +71,54 @@ function resolveBoundObjectNode(rawNode, checker, context) {
71
71
  }
72
72
  return null;
73
73
  }
74
+ // Walks up from a `this` reference to the class it's lexically inside of, so
75
+ // `this.EVENT_NAME` can be resolved against that class's own property
76
+ // declarations - the same idea as resolving a plain identifier against its
77
+ // variable declaration, just rooted at the enclosing class instead of a
78
+ // module-scope symbol (`this` itself has no symbol/declaration of its own to
79
+ // look up via the checker).
80
+ function findEnclosingClass(node) {
81
+ let current = node;
82
+ while (current) {
83
+ if (typescript_1.default.isClassDeclaration(current) || typescript_1.default.isClassExpression(current)) {
84
+ return current;
85
+ }
86
+ current = current.parent;
87
+ }
88
+ return null;
89
+ }
90
+ function isFunctionLikeBoundary(node) {
91
+ return (typescript_1.default.isFunctionDeclaration(node) ||
92
+ typescript_1.default.isFunctionExpression(node) ||
93
+ typescript_1.default.isArrowFunction(node) ||
94
+ typescript_1.default.isMethodDeclaration(node) ||
95
+ typescript_1.default.isGetAccessorDeclaration(node) ||
96
+ typescript_1.default.isSetAccessorDeclaration(node));
97
+ }
98
+ // Collects every `return <expr>;` belonging directly to `getter` - i.e. not
99
+ // one that belongs to a function/method/accessor nested inside its body.
100
+ // A getter with conditional returns is legitimately multi-valued (which
101
+ // branch runs depends on runtime state), so the caller unions more than one
102
+ // of these the same way it already does for a reassigned `let`.
103
+ function collectReturnExpressions(getter) {
104
+ const expressions = [];
105
+ function visit(node) {
106
+ if (node !== getter && isFunctionLikeBoundary(node)) {
107
+ return;
108
+ }
109
+ if (typescript_1.default.isReturnStatement(node) && node.expression) {
110
+ expressions.push(node.expression);
111
+ }
112
+ typescript_1.default.forEachChild(node, visit);
113
+ }
114
+ if (getter.body) {
115
+ visit(getter.body);
116
+ }
117
+ return expressions;
118
+ }
119
+ function getMemberName(member) {
120
+ return typescript_1.default.isIdentifier(member.name) ? member.name.text : typescript_1.default.isStringLiteral(member.name) ? member.name.text : null;
121
+ }
74
122
  function resolvePropertyAccess(node, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache) {
75
123
  const propertyName = (0, utils_1.getPropertyName)(node);
76
124
  if (!propertyName) {
@@ -96,6 +144,40 @@ function resolvePropertyAccess(node, checker, context, visited, evaluationCache,
96
144
  }
97
145
  }
98
146
  }
147
+ // this.EVENT_NAME
148
+ // class Foo { EVENT_NAME = "checkout"; bar() { track(this.EVENT_NAME); } }
149
+ //
150
+ // this.computedEventName (getter)
151
+ // class Foo { get computedEventName() { return "checkout"; } }
152
+ if (targetExpression.kind === typescript_1.default.SyntaxKind.ThisKeyword) {
153
+ const enclosingClass = findEnclosingClass(node);
154
+ if (enclosingClass) {
155
+ for (const member of enclosingClass.members) {
156
+ if (typescript_1.default.isPropertyDeclaration(member) && member.initializer && getMemberName(member) === propertyName) {
157
+ return resolveNodeValue(member.initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
158
+ }
159
+ if (typescript_1.default.isGetAccessorDeclaration(member) && getMemberName(member) === propertyName) {
160
+ const returnExpressions = collectReturnExpressions(member);
161
+ if (returnExpressions.length === 0) {
162
+ return empty();
163
+ }
164
+ if (returnExpressions.length === 1) {
165
+ return resolveNodeValue(returnExpressions[0], checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
166
+ }
167
+ const values = [];
168
+ for (const expression of returnExpressions) {
169
+ const resolved = resolveNodeValue(expression, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
170
+ values.push(...resolved.values);
171
+ }
172
+ return {
173
+ values: unique(values),
174
+ dynamic: true,
175
+ };
176
+ }
177
+ }
178
+ }
179
+ return empty();
180
+ }
99
181
  if (typescript_1.default.isIdentifier(targetExpression)) {
100
182
  let symbol = checker.getSymbolAtLocation(targetExpression);
101
183
  if (symbol) {
@@ -157,6 +239,63 @@ function resolvePropertyAccess(node, checker, context, visited, evaluationCache,
157
239
  }
158
240
  return empty();
159
241
  }
242
+ function isConstDeclaration(declaration) {
243
+ return (typescript_1.default.getCombinedNodeFlags(declaration) & typescript_1.default.NodeFlags.Const) !== 0;
244
+ }
245
+ // `ts.isAssignmentOperator` exists on the compiled module at runtime but
246
+ // isn't part of the public .d.ts (an internal-only export), so the operator
247
+ // set is spelled out here instead of depending on an undocumented API.
248
+ const ASSIGNMENT_OPERATOR_KINDS = new Set([
249
+ typescript_1.default.SyntaxKind.EqualsToken,
250
+ typescript_1.default.SyntaxKind.PlusEqualsToken,
251
+ typescript_1.default.SyntaxKind.MinusEqualsToken,
252
+ typescript_1.default.SyntaxKind.AsteriskEqualsToken,
253
+ typescript_1.default.SyntaxKind.AsteriskAsteriskEqualsToken,
254
+ typescript_1.default.SyntaxKind.SlashEqualsToken,
255
+ typescript_1.default.SyntaxKind.PercentEqualsToken,
256
+ typescript_1.default.SyntaxKind.LessThanLessThanEqualsToken,
257
+ typescript_1.default.SyntaxKind.GreaterThanGreaterThanEqualsToken,
258
+ typescript_1.default.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
259
+ typescript_1.default.SyntaxKind.AmpersandEqualsToken,
260
+ typescript_1.default.SyntaxKind.BarEqualsToken,
261
+ typescript_1.default.SyntaxKind.CaretEqualsToken,
262
+ typescript_1.default.SyntaxKind.AmpersandAmpersandEqualsToken,
263
+ typescript_1.default.SyntaxKind.BarBarEqualsToken,
264
+ typescript_1.default.SyntaxKind.QuestionQuestionEqualsToken,
265
+ ]);
266
+ // A `let`/`var` binding can be reassigned after its declaration - resolving
267
+ // only the initializer would confidently report a value that a later
268
+ // assignment (still just as static - a literal, another variable, a
269
+ // template, etc.) overwrites before any sink ever sees it. This walks the
270
+ // declaring file(s) for every assignment to `symbol` so the caller can treat
271
+ // the binding as multi-valued (or at least uncertain) instead of silently
272
+ // trusting the first value it was ever given.
273
+ function collectReassignments(symbol, checker) {
274
+ const values = [];
275
+ let hasUnresolvedReassignment = false;
276
+ // See the identical `?? []` elsewhere in this file: a real ts.Symbol here
277
+ // always has a declarations array.
278
+ /* v8 ignore next */
279
+ const sourceFiles = new Set((symbol.getDeclarations() ?? []).map(declaration => declaration.getSourceFile()));
280
+ for (const sourceFile of sourceFiles) {
281
+ const visit = (node) => {
282
+ if (typescript_1.default.isBinaryExpression(node) &&
283
+ ASSIGNMENT_OPERATOR_KINDS.has(node.operatorToken.kind) &&
284
+ typescript_1.default.isIdentifier(node.left) &&
285
+ checker.getSymbolAtLocation(node.left) === symbol) {
286
+ if (node.operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken) {
287
+ values.push(node.right);
288
+ }
289
+ else {
290
+ hasUnresolvedReassignment = true;
291
+ }
292
+ }
293
+ typescript_1.default.forEachChild(node, visit);
294
+ };
295
+ visit(sourceFile);
296
+ }
297
+ return { values, hasUnresolvedReassignment };
298
+ }
160
299
  function resolveIdentifier(node, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache) {
161
300
  const boundSymbol = checker.getSymbolAtLocation(node);
162
301
  if (boundSymbol && context.parameterBindings.has(boundSymbol)) {
@@ -186,7 +325,21 @@ function resolveIdentifier(node, checker, context, visited, evaluationCache, res
186
325
  const declarations = symbol.getDeclarations() ?? [];
187
326
  for (const declaration of declarations) {
188
327
  if (typescript_1.default.isVariableDeclaration(declaration) && declaration.initializer) {
189
- const resolved = resolveNodeValue(declaration.initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
328
+ const reassignment = isConstDeclaration(declaration) ? null : collectReassignments(symbol, checker);
329
+ if (!reassignment || (reassignment.values.length === 0 && !reassignment.hasUnresolvedReassignment)) {
330
+ const resolved = resolveNodeValue(declaration.initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
331
+ evaluationCache.set(symbol, resolved);
332
+ return resolved;
333
+ }
334
+ const values = [];
335
+ for (const expression of [declaration.initializer, ...reassignment.values]) {
336
+ const resolvedValue = resolveNodeValue(expression, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
337
+ values.push(...resolvedValue.values);
338
+ }
339
+ const resolved = {
340
+ values: unique(values),
341
+ dynamic: true,
342
+ };
190
343
  evaluationCache.set(symbol, resolved);
191
344
  return resolved;
192
345
  }
@@ -213,7 +366,11 @@ function resolveCallExpression(node, checker, context, visited, evaluationCache,
213
366
  }
214
367
  const values = [];
215
368
  for (const propagation of semantic.propagations) {
216
- const argument = node.arguments[propagation.parameterIndex];
369
+ // Call site can omit the argument and rely on the parameter's own default
370
+ // value (`function build(name = "x") {}` called as `build()`) - that
371
+ // default is a real static literal, so fall back to it instead of
372
+ // dropping the call site.
373
+ const argument = node.arguments[propagation.parameterIndex] ?? propagation.parameter.initializer;
217
374
  if (!argument) {
218
375
  continue;
219
376
  }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.detectJsxEventAttribute = detectJsxEventAttribute;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ const EVENT_ATTRIBUTE_NAME = "event";
9
+ /**
10
+ * Detects a JSX `event="literal"` / `event={expr}` attribute - the same
11
+ * declarative-tracking convention the Vue/Svelte/Astro/Angular CLI plugins
12
+ * already use for their own templates, recognized on any tag (plain
13
+ * elements and components alike). No plugin or preprocessing is needed for
14
+ * React: JSX is already part of the TypeScript AST the core walks natively,
15
+ * so this is a plain node-kind check alongside the existing CallExpression
16
+ * sink detection, not a separate virtual-module bridge.
17
+ *
18
+ * Reported through the exact same `TrackCall` shape as a real track() call:
19
+ * a literal value resolves directly, and a dynamic expression is resolved
20
+ * through the same checker-based pipeline as any other TS expression,
21
+ * falling back to a dynamic occurrence (not silently dropped) when it
22
+ * doesn't resolve to a literal.
23
+ */
24
+ function detectJsxEventAttribute(node) {
25
+ if (!typescript_1.default.isJsxAttribute(node)) {
26
+ return null;
27
+ }
28
+ if (!typescript_1.default.isIdentifier(node.name) || node.name.text !== EVENT_ATTRIBUTE_NAME) {
29
+ return null;
30
+ }
31
+ const initializer = node.initializer;
32
+ if (!initializer) {
33
+ // Boolean shorthand `<Button event />` - no name to extract, same as
34
+ // the other plugins' own boolean-shorthand `event` handling.
35
+ return null;
36
+ }
37
+ let value;
38
+ if (typescript_1.default.isStringLiteral(initializer)) {
39
+ // event="literal"
40
+ value = initializer;
41
+ }
42
+ else if (typescript_1.default.isJsxExpression(initializer)) {
43
+ // event={expr} - an empty event={} has no expression to extract.
44
+ if (!initializer.expression) {
45
+ return null;
46
+ }
47
+ value = initializer.expression;
48
+ }
49
+ else {
50
+ return null;
51
+ }
52
+ return {
53
+ node,
54
+ sourceFile: node.getSourceFile(),
55
+ trackedArguments: [value],
56
+ calleeText: node.parent.parent.tagName.getText(),
57
+ };
58
+ }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.scanSource = scanSource;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const sinkDetector_1 = require("./sinkDetector");
9
+ const jsxEventAttribute_1 = require("./jsxEventAttribute");
9
10
  const propagationAnalyzer_1 = require("./analyzer/propagationAnalyzer");
10
11
  function scanSource(source, checker, wrapperRegistry, plugins) {
11
12
  const sinks = [];
@@ -51,9 +52,16 @@ function scanSource(source, checker, wrapperRegistry, plugins) {
51
52
  node,
52
53
  sourceFile: source,
53
54
  trackedArguments,
55
+ calleeText: node.expression.getText(source),
54
56
  });
55
57
  }
56
58
  }
59
+ // JSX `event="..."`/`event={expr}` attribute detection (React/JSX files
60
+ // only - a plain .ts/.js file never contains a JsxAttribute node).
61
+ const jsxEventAttribute = (0, jsxEventAttribute_1.detectJsxEventAttribute)(node);
62
+ if (jsxEventAttribute) {
63
+ trackCalls.push(jsxEventAttribute);
64
+ }
57
65
  typescript_1.default.forEachChild(node, visit);
58
66
  }
59
67
  visit(source);
@@ -7,15 +7,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.recordDynamicOccurrence = recordDynamicOccurrence;
8
8
  const typescript_1 = __importDefault(require("typescript"));
9
9
  const CALL_TEXT_MAX_LENGTH = 120;
10
- function recordDynamicOccurrence(call, resolvedValues, out) {
11
- const sourceFile = call.getSourceFile();
12
- const { line, character } = typescript_1.default.getLineAndCharacterOfPosition(sourceFile, call.getStart(sourceFile));
13
- const callText = call.getText(sourceFile);
10
+ // `node` is a CallExpression for a real track() call, or a JsxAttribute for
11
+ // a JSX `event="..."`/`event={expr}` binding (see jsxEventAttribute.ts) -
12
+ // `calleeText` is supplied by the caller since it's derived differently per
13
+ // node kind (a call's callee text vs. a JSX element's tag name).
14
+ function recordDynamicOccurrence(node, calleeText, resolvedValues, out) {
15
+ const sourceFile = node.getSourceFile();
16
+ const { line, character } = typescript_1.default.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
17
+ const callText = node.getText(sourceFile);
14
18
  out.push({
15
19
  fileName: sourceFile.fileName,
16
20
  line: line + 1,
17
21
  character,
18
- calleeText: call.expression.getText(sourceFile),
22
+ calleeText,
19
23
  callText: callText.length > CALL_TEXT_MAX_LENGTH ? `${callText.slice(0, CALL_TEXT_MAX_LENGTH)}…` : callText,
20
24
  resolvedValues,
21
25
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",