@eventra_dev/eventra-cli 1.0.6 → 2.0.1

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.
package/README.md CHANGED
@@ -80,6 +80,20 @@ function trackFeature(name: string) {
80
80
  trackFeature("purchase");
81
81
  ```
82
82
 
83
+ The event name can also come from a property on the wrapper's parameter — plain access, optional chaining, element access, or destructuring (including aliased and nested):
84
+
85
+ ```ts
86
+ function trackWrapper(payload: { event: string }) {
87
+ sdk.track(payload.event); // also: payload?.event, payload["event"]
88
+ }
89
+ trackWrapper({ event: "checkout" });
90
+
91
+ function trackDestructured({ event: name }: { event: string }) {
92
+ sdk.track(name);
93
+ }
94
+ trackDestructured({ event: "checkout" });
95
+ ```
96
+
83
97
  ### Variables, templates, conditionals
84
98
 
85
99
  ```ts
@@ -62,26 +62,40 @@ function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCa
62
62
  }
63
63
  }
64
64
  function extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events, dynamicOccurrences) {
65
- let argument = call.arguments[propagation.sourceParameterIndex];
65
+ const argument = call.arguments[propagation.sourceParameterIndex];
66
66
  if (!argument) {
67
67
  return;
68
68
  }
69
- // wrapper({
70
- // nested: {
71
- // event: "x"
72
- // }
73
- // })
74
- if (propagation.propertyPath.length > 0) {
69
+ const context = (0, evaluationContext_1.createEvaluationContext)();
70
+ if (typescript_1.default.isIdentifier(propagation.sourceParameter.name)) {
71
+ // sourceParameter is a plain identifier (`function f(payload)`), so
72
+ // propagation.targetNode — `payload`, `payload.event`, or any expression
73
+ // a plugin builds from it (e.g. a template literal) — can resolve its own
74
+ // propertyPath by evaluating targetNode with `payload` substituted for
75
+ // the *raw*, un-narrowed call-site argument.
76
+ const parameterSymbol = checker.getSymbolAtLocation(propagation.sourceParameter.name);
77
+ if (parameterSymbol) {
78
+ context.parameterBindings.set(parameterSymbol, argument);
79
+ }
80
+ }
81
+ else {
82
+ // sourceParameter is a destructuring pattern (`function f({ event })`),
83
+ // which has no symbol of its own — `getSymbolAtLocation` on the pattern
84
+ // returns nothing. Narrow to the leaf value ourselves via propertyPath,
85
+ // then bind it to the destructured binding's own symbol (targetNode is
86
+ // that binding's reference, e.g. `event` in `sdk.track(event)`).
87
+ if (propagation.propertyPath.length === 0 || !typescript_1.default.isIdentifier(propagation.targetNode)) {
88
+ return;
89
+ }
75
90
  const resolvedPath = resolveObjectPath(argument, propagation.propertyPath);
76
91
  if (!resolvedPath) {
77
92
  return;
78
93
  }
79
- argument = resolvedPath;
80
- }
81
- const context = (0, evaluationContext_1.createEvaluationContext)();
82
- const parameterSymbol = checker.getSymbolAtLocation(propagation.sourceParameter.name);
83
- if (parameterSymbol) {
84
- context.parameterBindings.set(parameterSymbol, argument);
94
+ const bindingSymbol = checker.getSymbolAtLocation(propagation.targetNode);
95
+ if (!bindingSymbol) {
96
+ return;
97
+ }
98
+ context.parameterBindings.set(bindingSymbol, resolvedPath);
85
99
  }
86
100
  const resolved = (0, resolver_1.resolveNodeValue)(propagation.targetNode, checker, context, new Set(), evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
87
101
  for (const value of resolved.values) {
@@ -9,6 +9,7 @@ const exportResolver_1 = require("./exportResolver");
9
9
  const returnPropagationAnalyzer_1 = require("../scanner/analyzer/returnPropagationAnalyzer");
10
10
  const callResolver_1 = require("./callResolver");
11
11
  const resolvedCallCache_1 = require("../cache/resolvedCallCache");
12
+ const utils_1 = require("../shared/utils");
12
13
  function unique(values) {
13
14
  return [
14
15
  ...new Set(values),
@@ -29,42 +30,69 @@ function concat(left, right) {
29
30
  }
30
31
  return unique(result);
31
32
  }
32
- function getPropertyName(node) {
33
- if (typescript_1.default.isElementAccessExpression(node)) {
34
- if (!node.argumentExpression || !typescript_1.default.isStringLiteral(node.argumentExpression)) {
33
+ // Walks a chain rooted at a parameter-bound identifier — e.g. `payload.nested`
34
+ // inside `payload.nested.event` — down to the object literal it refers to, so
35
+ // resolvePropertyAccess can pick the final property off a nested payload, not
36
+ // just a one-level-deep one.
37
+ function resolveBoundObjectNode(rawNode, checker, context) {
38
+ const node = (0, utils_1.unwrapExpression)(rawNode);
39
+ if (typescript_1.default.isIdentifier(node)) {
40
+ const symbol = checker.getSymbolAtLocation(node);
41
+ if (!symbol || !context.parameterBindings.has(symbol)) {
35
42
  return null;
36
43
  }
37
- return node.argumentExpression.text;
44
+ return context.parameterBindings.get(symbol) ?? null;
38
45
  }
39
- return node.name.text;
46
+ if (typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isPropertyAccessChain(node) || typescript_1.default.isElementAccessExpression(node)) {
47
+ const propertyName = (0, utils_1.getPropertyName)(node);
48
+ if (!propertyName) {
49
+ return null;
50
+ }
51
+ const base = resolveBoundObjectNode(node.expression, checker, context);
52
+ if (!base || !typescript_1.default.isObjectLiteralExpression(base)) {
53
+ return null;
54
+ }
55
+ for (const property of base.properties) {
56
+ if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
57
+ continue;
58
+ }
59
+ const objectPropertyName = typescript_1.default.isIdentifier(property.name) ? property.name.text : property.name.getText();
60
+ if (objectPropertyName !== propertyName) {
61
+ continue;
62
+ }
63
+ return typescript_1.default.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
64
+ }
65
+ return null;
66
+ }
67
+ return null;
40
68
  }
41
69
  function resolvePropertyAccess(node, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache) {
42
- const propertyName = getPropertyName(node);
70
+ const propertyName = (0, utils_1.getPropertyName)(node);
43
71
  if (!propertyName) {
44
72
  return empty();
45
73
  }
46
74
  // payload.event
47
75
  // payload["event"]
76
+ // payload.nested.event
48
77
  const targetExpression = typescript_1.default.isElementAccessExpression(node) ? node.expression : node.expression;
49
- if (typescript_1.default.isIdentifier(targetExpression)) {
50
- const symbol = checker.getSymbolAtLocation(targetExpression);
51
- // parameter binding
52
- if (symbol && context.parameterBindings.has(symbol)) {
53
- const bound = context.parameterBindings.get(symbol);
54
- if (bound && typescript_1.default.isObjectLiteralExpression(bound)) {
55
- for (const property of bound.properties) {
56
- if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
57
- continue;
58
- }
59
- const objectPropertyName = typescript_1.default.isIdentifier(property.name) ? property.name.text : property.name.getText();
60
- if (objectPropertyName !== propertyName) {
61
- continue;
62
- }
63
- const initializer = typescript_1.default.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
64
- return resolveNodeValue(initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
78
+ {
79
+ const bound = resolveBoundObjectNode(targetExpression, checker, context);
80
+ if (bound && typescript_1.default.isObjectLiteralExpression(bound)) {
81
+ for (const property of bound.properties) {
82
+ if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
83
+ continue;
84
+ }
85
+ const objectPropertyName = typescript_1.default.isIdentifier(property.name) ? property.name.text : property.name.getText();
86
+ if (objectPropertyName !== propertyName) {
87
+ continue;
65
88
  }
89
+ const initializer = typescript_1.default.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
90
+ return resolveNodeValue(initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
66
91
  }
67
92
  }
93
+ }
94
+ if (typescript_1.default.isIdentifier(targetExpression)) {
95
+ const symbol = checker.getSymbolAtLocation(targetExpression);
68
96
  // const EVENTS = {
69
97
  // LOGIN: "login"
70
98
  // }
@@ -221,10 +249,11 @@ function resolveNodeValue(node, checker, context, visited, evaluationCache, reso
221
249
  dynamic: true,
222
250
  };
223
251
  }
224
- if (typescript_1.default.isParenthesizedExpression(node)) {
225
- return resolveNodeValue(node.expression, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
226
- }
227
- if (typescript_1.default.isAsExpression(node) || typescript_1.default.isTypeAssertionExpression(node)) {
252
+ if (typescript_1.default.isParenthesizedExpression(node) ||
253
+ typescript_1.default.isAsExpression(node) ||
254
+ typescript_1.default.isTypeAssertionExpression(node) ||
255
+ typescript_1.default.isNonNullExpression(node) ||
256
+ typescript_1.default.isSatisfiesExpression(node)) {
228
257
  return resolveNodeValue(node.expression, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
229
258
  }
230
259
  if (typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isPropertyAccessChain(node) || typescript_1.default.isElementAccessExpression(node)) {
@@ -6,12 +6,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.analyzeWrapperPropagation = analyzeWrapperPropagation;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const symbolUtils_1 = require("../../symbols/symbolUtils");
9
+ const utils_1 = require("../../shared/utils");
10
+ function isPropertyAccessLike(node) {
11
+ return typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isPropertyAccessChain(node) || typescript_1.default.isElementAccessExpression(node);
12
+ }
9
13
  function extractPropertyPath(expression) {
10
14
  const result = [];
11
- let current = expression;
12
- while (typescript_1.default.isPropertyAccessExpression(current) || typescript_1.default.isPropertyAccessChain(current)) {
13
- result.unshift(current.name.text);
14
- current = current.expression;
15
+ let current = (0, utils_1.unwrapExpression)(expression);
16
+ while (isPropertyAccessLike(current)) {
17
+ const propertyName = (0, utils_1.getPropertyName)(current);
18
+ if (!propertyName) {
19
+ return null;
20
+ }
21
+ result.unshift(propertyName);
22
+ current = (0, utils_1.unwrapExpression)(current.expression);
15
23
  }
16
24
  if (!typescript_1.default.isIdentifier(current)) {
17
25
  return null;
@@ -77,15 +85,18 @@ function collectPropagations(fn, sinks) {
77
85
  const result = [];
78
86
  for (const sink of sinks) {
79
87
  for (const tracked of sink.trackedArguments) {
80
- const argument = sink.call.arguments[tracked.index];
81
- if (!argument) {
88
+ const rawArgument = sink.call.arguments[tracked.index];
89
+ if (!rawArgument) {
82
90
  continue;
83
91
  }
92
+ const argument = (0, utils_1.unwrapExpression)(rawArgument);
84
93
  // direct parameter
85
94
  // track(name)
86
95
  // track(props.event)
87
96
  // track(props?.event)
88
- if (typescript_1.default.isIdentifier(argument) || typescript_1.default.isPropertyAccessExpression(argument) || typescript_1.default.isPropertyAccessChain(argument)) {
97
+ // track(props?.event as string)
98
+ // track(props["event"])
99
+ if (typescript_1.default.isIdentifier(argument) || isPropertyAccessLike(argument)) {
89
100
  let identifier = null;
90
101
  let propertyPath = [];
91
102
  if (typescript_1.default.isIdentifier(argument)) {
@@ -94,8 +105,8 @@ function collectPropagations(fn, sinks) {
94
105
  else {
95
106
  propertyPath = extractPropertyPath(argument) ?? [];
96
107
  let root = argument;
97
- while (typescript_1.default.isPropertyAccessExpression(root) || typescript_1.default.isPropertyAccessChain(root)) {
98
- root = root.expression;
108
+ while (isPropertyAccessLike(root)) {
109
+ root = (0, utils_1.unwrapExpression)(root.expression);
99
110
  }
100
111
  if (typescript_1.default.isIdentifier(root)) {
101
112
  identifier = root;
@@ -18,20 +18,6 @@ function isTrackCallee(expr) {
18
18
  }
19
19
  return false;
20
20
  }
21
- function fileImportsEventraSdk(sourceFile) {
22
- for (const stmt of sourceFile.statements) {
23
- if (!typescript_1.default.isImportDeclaration(stmt) || !stmt.moduleSpecifier) {
24
- continue;
25
- }
26
- if (!typescript_1.default.isStringLiteral(stmt.moduleSpecifier)) {
27
- continue;
28
- }
29
- if ((0, eventraSdk_1.isEventraSdkModuleSpecifier)(stmt.moduleSpecifier.text)) {
30
- return true;
31
- }
32
- }
33
- return false;
34
- }
35
21
  function getImportDeclarationForSpecifier(decl) {
36
22
  let current = decl;
37
23
  while (current) {
@@ -155,10 +141,6 @@ function isEventraSdkTrackCall(call, checker) {
155
141
  if (!isTrackCallee(expr)) {
156
142
  return false;
157
143
  }
158
- const sourceFile = call.getSourceFile();
159
- if (!fileImportsEventraSdk(sourceFile)) {
160
- return false;
161
- }
162
144
  const receiver = typescript_1.default.isPropertyAccessExpression(expr) ||
163
145
  typescript_1.default.isPropertyAccessChain(expr) ||
164
146
  typescript_1.default.isElementAccessExpression(expr)
@@ -4,9 +4,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.unwrapExpression = unwrapExpression;
8
+ exports.getPropertyName = getPropertyName;
7
9
  exports.getCallName = getCallName;
8
10
  exports.getFunctionName = getFunctionName;
9
11
  const typescript_1 = __importDefault(require("typescript"));
12
+ // track(payload?.event as string)
13
+ // track((payload.event))
14
+ // track(payload!.event)
15
+ function unwrapExpression(expression) {
16
+ let current = expression;
17
+ while (typescript_1.default.isAsExpression(current) ||
18
+ typescript_1.default.isTypeAssertionExpression(current) ||
19
+ typescript_1.default.isNonNullExpression(current) ||
20
+ typescript_1.default.isParenthesizedExpression(current) ||
21
+ typescript_1.default.isSatisfiesExpression(current)) {
22
+ current = current.expression;
23
+ }
24
+ return current;
25
+ }
26
+ // payload.event -> "event"
27
+ // payload["event"] -> "event"
28
+ function getPropertyName(node) {
29
+ if (typescript_1.default.isElementAccessExpression(node)) {
30
+ if (!node.argumentExpression || !typescript_1.default.isStringLiteral(node.argumentExpression)) {
31
+ return null;
32
+ }
33
+ return node.argumentExpression.text;
34
+ }
35
+ return node.name.text;
36
+ }
10
37
  function getCallName(expression) {
11
38
  //
12
39
  // track()
package/dist/cli/send.js CHANGED
@@ -62,6 +62,7 @@ async function send(opts = {}) {
62
62
  const config = await (0, config_1.loadConfig)();
63
63
  if (!config) {
64
64
  console.log(chalk_1.default.red("eventra.json not found. Run 'eventra init'"));
65
+ process.exitCode = 1;
65
66
  return;
66
67
  }
67
68
  const endpoint = config.endpoint || process.env.EVENTRA_ENDPOINT || undefined;
@@ -79,6 +80,7 @@ async function send(opts = {}) {
79
80
  console.log(chalk_1.default.red(`Endpoint "${endpoint}" from eventra.json is not locally approved.`));
80
81
  console.log(chalk_1.default.gray("If this is expected (e.g. a self-hosted ingest URL), approve it once with:"));
81
82
  console.log(chalk_1.default.gray(" eventra send --trust-endpoint"));
83
+ process.exitCode = 1;
82
84
  return;
83
85
  }
84
86
  await (0, config_1.trustEndpoint)(endpoint);
@@ -90,6 +92,7 @@ async function send(opts = {}) {
90
92
  if (!apiKey) {
91
93
  if (!process.stdin.isTTY) {
92
94
  console.log(chalk_1.default.red("API key required. Set the EVENTRA_API_KEY environment variable (recommended for CI) or run 'eventra init'."));
95
+ process.exitCode = 1;
93
96
  return;
94
97
  }
95
98
  const answers = await inquirer_1.default.prompt([
@@ -110,14 +113,17 @@ async function send(opts = {}) {
110
113
  // no events
111
114
  if (!config.events?.length) {
112
115
  console.log(chalk_1.default.yellow("No events found. Run 'eventra sync'"));
116
+ process.exitCode = 1;
113
117
  return;
114
118
  }
115
119
  if (!apiKey) {
116
120
  console.log(chalk_1.default.red("API key required"));
121
+ process.exitCode = 1;
117
122
  return;
118
123
  }
119
124
  if (!endpoint) {
120
125
  console.log(chalk_1.default.red("Endpoint not configured"));
126
+ process.exitCode = 1;
121
127
  return;
122
128
  }
123
129
  console.log("");
@@ -145,6 +151,7 @@ async function send(opts = {}) {
145
151
  console.log(chalk_1.default.red(`Request failed (${outcome.status})`));
146
152
  if (outcome.body)
147
153
  console.log(chalk_1.default.gray(outcome.body));
154
+ process.exitCode = 1;
148
155
  return;
149
156
  }
150
157
  lastFailure = { status: outcome.status, body: outcome.body };
@@ -173,6 +180,7 @@ async function send(opts = {}) {
173
180
  else if (lastFailure?.error instanceof Error) {
174
181
  console.log(chalk_1.default.gray(lastFailure.error.message));
175
182
  }
183
+ process.exitCode = 1;
176
184
  return;
177
185
  }
178
186
  console.log(chalk_1.default.green("Events registered successfully"));
package/dist/cli/watch.js CHANGED
@@ -35,6 +35,14 @@ async function watch() {
35
35
  const normalize = (file) => {
36
36
  return path_1.default.resolve(file).replace(/\\/g, "/");
37
37
  };
38
+ // chokidar's `ignored` receives absolute, normalized paths — reject any path
39
+ // with an excluded directory name as one of its segments (matches how
40
+ // `fast-glob`'s own `ignore` option is applied during the initial scan).
41
+ const excludedDirs = new Set(config.sync.exclude);
42
+ const isExcludedPath = (file) => {
43
+ const segments = normalize(file).split("/");
44
+ return segments.some((segment) => excludedDirs.has(segment));
45
+ };
38
46
  const files = await (0, fast_glob_1.default)(include, {
39
47
  ignore: config.sync.exclude,
40
48
  absolute: true,
@@ -68,7 +76,15 @@ async function watch() {
68
76
  const initialEvents = engine.getAllEvents();
69
77
  const initialWrappers = engine.getAllFunctionWrappers();
70
78
  console.log(chalk_1.default.gray(`Initial: ${initialEvents.length} events, ${initialWrappers.length} wrappers\n`));
71
- const watcher = chokidar_1.default.watch([...sourceFiles, ...watchedDeps], { ignoreInitial: true });
79
+ // Watch the project root (not just the files found by the initial scan) so
80
+ // that brand-new files matching `include` are noticed as they're created —
81
+ // a fixed file list only ever detects edits to files chokidar already knows
82
+ // about. Dependency files outside the root (added below via `watcher.add`)
83
+ // are unaffected by this and keep working the same way.
84
+ const watcher = chokidar_1.default.watch(process.cwd(), {
85
+ ignoreInitial: true,
86
+ ignored: isExcludedPath,
87
+ });
72
88
  const queue = new Map();
73
89
  let timer = null;
74
90
  const run = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "1.0.6",
3
+ "version": "2.0.1",
4
4
  "description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",