@eventra_dev/eventra-cli 2.0.4 → 2.0.6
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 +49 -9
- package/dist/analysis/extractor/propagationExtractor.js +11 -0
- package/dist/analysis/resolver/callResolver.js +6 -0
- package/dist/analysis/resolver/exportResolver.js +25 -3
- package/dist/analysis/resolver/resolver.js +30 -0
- package/dist/analysis/sdk/isEventraTrackCall.js +41 -0
- package/dist/analysis/shared/utils.js +4 -6
- package/dist/cli/send.js +6 -0
- package/dist/compiler/compilerContext.js +2 -0
- package/dist/plugin/loadPlugins.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
<a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm version" src="https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm downloads" src="https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
|
|
10
10
|
<a href="https://github.com/and-1991/eventra-cli/actions/workflows/test.yml"><img alt="tests" src="https://img.shields.io/github/actions/workflow/status/and-1991/eventra-cli/test.yml?branch=main&label=tests&style=flat-square&logo=vitest&logoColor=white"></a>
|
|
11
|
-
<img alt="unit tests" src="https://img.shields.io/badge/unit-
|
|
11
|
+
<img alt="unit tests" src="https://img.shields.io/badge/unit-403%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
|
|
12
12
|
<img alt="e2e fixtures" src="https://img.shields.io/badge/e2e-12%20fixtures-brightgreen?style=flat-square">
|
|
13
|
-
<img alt="coverage" src="https://img.shields.io/badge/coverage-
|
|
13
|
+
<img alt="coverage" src="https://img.shields.io/badge/coverage-100%25%20(unit)-brightgreen?style=flat-square&logo=vitest&logoColor=white">
|
|
14
14
|
<img alt="node" src="https://img.shields.io/node/v/@eventra_dev/eventra-cli?style=flat-square&color=darkgreen&logo=node.js&logoColor=white">
|
|
15
15
|
<a href="https://www.typescriptlang.org/"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-ready-blue?style=flat-square&logo=typescript&logoColor=white"></a>
|
|
16
16
|
<img alt="license" src="https://img.shields.io/npm/l/@eventra_dev/eventra-cli?style=flat-square&color=lightgrey">
|
|
@@ -24,7 +24,7 @@ Eventra CLI statically discovers analytics events from [**@eventra_dev/eventra-s
|
|
|
24
24
|
|
|
25
25
|
The CLI scans TypeScript and JavaScript with the TypeScript compiler API and extracts **only** calls to `Eventra.prototype.track()` on instances of `Eventra` imported from `@eventra_dev/eventra-sdk`.
|
|
26
26
|
|
|
27
|
-
It does **not** detect generic `track()`, Segment, Google Analytics, or other libraries out of the box. Framework files (e.g. `.vue`) require an optional [plugin](#plugins).
|
|
27
|
+
It does **not** detect generic `track()`, Segment, Google Analytics, or other libraries out of the box. Framework files (e.g. `.vue`, `.svelte`, `.astro`) require an optional [plugin](#plugins).
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -228,6 +228,46 @@ The plugin:
|
|
|
228
228
|
|
|
229
229
|
`sync.include` does **not** need `**/*.vue` manually — the plugin registers `**/*.vue` via `includeGlobs`. See [`@eventra_dev/cli-plugin-vue`](https://www.npmjs.com/package/@eventra_dev/cli-plugin-vue) for details.
|
|
230
230
|
|
|
231
|
+
### Svelte (`.svelte`)
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
pnpm add -D @eventra_dev/cli-plugin-svelte
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"plugins": ["@eventra_dev/cli-plugin-svelte"]
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The plugin:
|
|
244
|
+
|
|
245
|
+
- parses each `.svelte` file with the real Svelte compiler (`svelte/compiler`) into a single virtual TypeScript module combining `context="module"` and instance `<script>` blocks
|
|
246
|
+
- extracts `track()` calls from the script exactly like a regular `.ts` file (direct calls, wrappers, propagation); Svelte 5 runes (`$props`, `$state`, …) pass through untouched
|
|
247
|
+
- detects `event="feature_name"` attributes on any tag — plain elements, components, `{#if}`/`{#each}`/`{#await}`/`{#key}` blocks, slots — both literal and dynamic (`event={expr}`); dynamic bindings resolve through the same module scope as the script, or surface as a dynamic occurrence if unresolvable
|
|
248
|
+
|
|
249
|
+
`sync.include` does **not** need `**/*.svelte` manually. See [`@eventra_dev/cli-plugin-svelte`](https://www.npmjs.com/package/@eventra_dev/cli-plugin-svelte) for details.
|
|
250
|
+
|
|
251
|
+
### Astro (`.astro`)
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pnpm add -D @eventra_dev/cli-plugin-astro
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"plugins": ["@eventra_dev/cli-plugin-astro"]
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The plugin:
|
|
264
|
+
|
|
265
|
+
- parses each `.astro` file with the real Astro compiler (`@astrojs/compiler`) into a single virtual TypeScript module from the frontmatter
|
|
266
|
+
- extracts `track()` calls from the frontmatter exactly like a regular `.ts` file (direct calls, wrappers, propagation); `Astro.props` and other globals pass through untouched
|
|
267
|
+
- detects `event="feature_name"` attributes on any tag, including inside JSX expressions (`{cond && <Button event="..." />}`, `.map()`) — both literal and dynamic (`event={expr}`, including the `{event}` shorthand); dynamic bindings resolve through the same module scope as the frontmatter, or surface as a dynamic occurrence if unresolvable
|
|
268
|
+
|
|
269
|
+
`sync.include` does **not** need `**/*.astro` manually. See [`@eventra_dev/cli-plugin-astro`](https://www.npmjs.com/package/@eventra_dev/cli-plugin-astro) for details.
|
|
270
|
+
|
|
231
271
|
### Before publishing / local development
|
|
232
272
|
|
|
233
273
|
Plugins are resolved from your project's `node_modules` (same as any dependency):
|
|
@@ -254,7 +294,7 @@ External plugins export an object (or factory) with:
|
|
|
254
294
|
| `transform({ path, source })` | Returns `{ modules: [{ path, content }] }` |
|
|
255
295
|
| `staticSinks?` | Declarative callee-based sink rules (CLI converts to internal detectors) |
|
|
256
296
|
|
|
257
|
-
No dependency on `@eventra_dev/eventra-cli` is required
|
|
297
|
+
No dependency on `@eventra_dev/eventra-cli` is required — the host CLI loads the plugin, converts `staticSinks` into its own internal sink detectors, and calls `transform()` on matching files; the plugin never imports anything from the CLI itself.
|
|
258
298
|
|
|
259
299
|
**Only official `@eventra_dev/cli-plugin-*` packages can be loaded.** `eventra.json` is meant to be committed to git, so its `plugins` array is effectively PR-editable; since the CLI `import()`s whatever is listed there, an arbitrary specifier would be arbitrary code execution on every machine that runs `sync`/`check`/`watch`. Because `@eventra_dev` is an npm scope only the Eventra maintainers can publish to, restricting to `@eventra_dev/cli-plugin-*` means nothing outside that scope can ever be loaded this way. Any other entry in `plugins` is skipped with a console warning instead of being imported — third-party/community plugins aren't supported today.
|
|
260
300
|
|
|
@@ -313,11 +353,11 @@ No runtime execution. No monkey-patching.
|
|
|
313
353
|
|
|
314
354
|
## Test Coverage
|
|
315
355
|
|
|
316
|
-
Two test layers, **
|
|
356
|
+
Two test layers, **409 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios + 1 `watch` scenario**.
|
|
317
357
|
|
|
318
|
-
**
|
|
358
|
+
**100% statement/branch/function/line coverage** (v8 provider, `pnpm test:coverage`), enforced via a `coverage.thresholds` block in `vitest.config.ts` so a regression fails the build instead of silently slipping under 100%. This measures the vitest unit layer only; the e2e/`check`/`watch` fixtures run against the compiled `dist/` binary via `tsx tests/run.ts` and aren't coverage-instrumented. A handful of genuinely unreachable defensive branches (TS-internal invariants — e.g. an aliased import symbol is always fully resolved before its own declarations are ever inspected — that don't hold a second outcome in practice) are marked with `/* v8 ignore */`, with an inline comment explaining why, rather than tested.
|
|
319
359
|
|
|
320
|
-
**Unit tests (vitest)** —
|
|
360
|
+
**Unit tests (vitest)** — 39 suites covering core modules:
|
|
321
361
|
|
|
322
362
|
| Module | Covers |
|
|
323
363
|
|---|---|
|
|
@@ -329,7 +369,7 @@ Two test layers, **110 unit tests + 12 e2e fixtures + 3 `check` exit-code scenar
|
|
|
329
369
|
| `PluginRegistry` | Built-in SDK sink, preprocessors, virtual-path mapping, include-pattern dedup |
|
|
330
370
|
| `external plugin adapter` | Transform output mapping, static sink registration, invalid result rejection |
|
|
331
371
|
| `external plugin dynamic sink` | A non-literal argument at a plugin-declared static sink surfaces as a dynamic occurrence instead of being silently dropped |
|
|
332
|
-
| `vue-shaped external plugin
|
|
372
|
+
| `vue-shaped`/`svelte-shaped`/`astro-shaped` external plugin | Adapter path for script + template virtual modules and `staticSinks`, one suite per framework |
|
|
333
373
|
| `processFile` | Script-kind detection, import/export specifier extraction |
|
|
334
374
|
| `extractTemplateExpressions` | Vue/Svelte/Astro attribute patterns |
|
|
335
375
|
| `config` | `normalizeConfig` (sort events, dedupe wrappers, defaults, preserve `apiKey`/`endpoint`/`sync`/`plugins`); `resolveApiKey` env-var > `eventra.local.json` > legacy-inline priority, `saveLocalApiKey`'s gitignore-append |
|
|
@@ -338,7 +378,7 @@ Two test layers, **110 unit tests + 12 e2e fixtures + 3 `check` exit-code scenar
|
|
|
338
378
|
| `cross-file SDK detection` | Direct `.track()` and wrapper calls resolve across files even when the call-site file never imports the SDK package itself |
|
|
339
379
|
| `dynamic event reporter` | Dynamic vs. static call classification, end-to-end `registerDynamicEventReporter` invocation |
|
|
340
380
|
| `load plugins` | Trusted-specifier allowlist (`^@eventra_dev/cli-plugin-[a-z0-9-]+$`), accepted/rejected plugin shapes |
|
|
341
|
-
| `Nuxt auto-import compatibility` | Ambient `declare const x: typeof import(...)` shape resolution — a hand-written fixture, not a real `nuxt build`
|
|
381
|
+
| `Nuxt auto-import compatibility` | Ambient `declare const x: typeof import(...)` shape resolution — a hand-written fixture, not a real `nuxt build` |
|
|
342
382
|
| `property propagation` | Direct/optional/nested property access, destructured/aliased/nested-destructured params, element access, enum member access (same-file and cross-file), wrapper detection surviving a cast/non-null assertion, and direct object-literal payloads to `track()` staying ignored (including when cast) |
|
|
343
383
|
| `send()` | Endpoint trust-on-first-use (TOFU): default-trusted, blocked-until-approved, approve-then-persists, re-block-on-change, env-var-bypasses-approval |
|
|
344
384
|
| `wrapper detector plugin` | A custom `WrapperDetector` (e.g. a template-literal wrapper shape the built-in analyzer can't cover) alongside the built-in fallback |
|
|
@@ -45,6 +45,12 @@ function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCa
|
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
const signature = checker.getSignatureFromDeclaration(resolved);
|
|
48
|
+
// resolveFunctionFromCall only ever returns a FunctionDeclaration,
|
|
49
|
+
// MethodDeclaration, ArrowFunction, or FunctionExpression (verified: every
|
|
50
|
+
// one of those kinds — including a bodyless ambient overload signature —
|
|
51
|
+
// yields a signature from getSignatureFromDeclaration), so this guard has
|
|
52
|
+
// no reachable failure case; it's defensive only.
|
|
53
|
+
/* v8 ignore if */
|
|
48
54
|
if (!signature) {
|
|
49
55
|
return;
|
|
50
56
|
}
|
|
@@ -74,6 +80,11 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
|
|
|
74
80
|
// propertyPath by evaluating targetNode with `payload` substituted for
|
|
75
81
|
// the *raw*, un-narrowed call-site argument.
|
|
76
82
|
const parameterSymbol = checker.getSymbolAtLocation(propagation.sourceParameter.name);
|
|
83
|
+
// Every real ts.ParameterDeclaration identifier is bound to a symbol by
|
|
84
|
+
// the TS binder (verified across ordinary functions, type-only function
|
|
85
|
+
// types, interface call signatures, and ambient declarations), so
|
|
86
|
+
// `parameterSymbol` is never undefined here; the guard is defensive only.
|
|
87
|
+
/* v8 ignore else */
|
|
77
88
|
if (parameterSymbol) {
|
|
78
89
|
context.parameterBindings.set(parameterSymbol, argument);
|
|
79
90
|
}
|
|
@@ -34,6 +34,12 @@ function getExpressionSymbol(expression, checker) {
|
|
|
34
34
|
return checker.getSymbolAtLocation(expression.name);
|
|
35
35
|
}
|
|
36
36
|
// analytics?.track()
|
|
37
|
+
// A PropertyAccessChain has the exact same SyntaxKind (PropertyAccessExpression)
|
|
38
|
+
// as a plain property access - the "chain" distinction is purely a flag
|
|
39
|
+
// (NodeFlags.OptionalChain) on the same node kind, not a separate kind. The
|
|
40
|
+
// isPropertyAccessExpression check above already matches (and returns for)
|
|
41
|
+
// every property-access-chain node, so this branch can never be reached.
|
|
42
|
+
/* v8 ignore next 3 */
|
|
37
43
|
if (typescript_1.default.isPropertyAccessChain(expression)) {
|
|
38
44
|
return checker.getSymbolAtLocation(expression.name);
|
|
39
45
|
}
|
|
@@ -60,7 +60,17 @@ function resolveExportedSymbol(symbol, checker, cache, visited) {
|
|
|
60
60
|
if (cached !== undefined) {
|
|
61
61
|
return cached;
|
|
62
62
|
}
|
|
63
|
-
// cycle guard
|
|
63
|
+
// cycle guard - defends against a cycle re-entering this function via the
|
|
64
|
+
// alias/export/ambient recursive calls below. In practice, a genuinely
|
|
65
|
+
// circular alias chain (e.g. two barrel files re-exporting each other's
|
|
66
|
+
// binding) is collapsed by the TypeScript checker itself: getAliasedSymbol
|
|
67
|
+
// returns its own synthetic "unknown" symbol after a single hop rather than
|
|
68
|
+
// looping, and getExportSymbolOfSymbol/the ambient-auto-import unwrap were
|
|
69
|
+
// not observed (across many realistic export shapes: named/default/star/
|
|
70
|
+
// namespace-as/CJS `export =`/ambient-module/UMD re-exports) to ever
|
|
71
|
+
// redirect to an already-visited symbol either. We could not construct a
|
|
72
|
+
// realistic source file that re-enters with the same symbol still `seen`.
|
|
73
|
+
/* v8 ignore next 4 */
|
|
64
74
|
if (seen.has(symbol)) {
|
|
65
75
|
cache.set(symbol, null);
|
|
66
76
|
return null;
|
|
@@ -73,8 +83,16 @@ function resolveExportedSymbol(symbol, checker, cache, visited) {
|
|
|
73
83
|
cache.set(symbol, resolved);
|
|
74
84
|
return resolved;
|
|
75
85
|
}
|
|
76
|
-
// export unwrap
|
|
86
|
+
// export unwrap - TypeScript's binder can in principle give a declaration a
|
|
87
|
+
// distinct "local" symbol and "export" symbol (see `declareModuleMember`'s
|
|
88
|
+
// `local.exportSymbol` link), but this was not observed via
|
|
89
|
+
// checker.getSymbolAtLocation for any realistic export shape we tried
|
|
90
|
+
// (named/default/star/namespace-as/CJS `export =`/ambient-module/UMD
|
|
91
|
+
// exports, from both the declaring file and importing files) - the split
|
|
92
|
+
// only seems to materialize for binder-internal container shapes we could
|
|
93
|
+
// not reach from ordinary source text.
|
|
77
94
|
const exportSymbol = checker.getExportSymbolOfSymbol(symbol);
|
|
95
|
+
/* v8 ignore next 4 */
|
|
78
96
|
if (exportSymbol && exportSymbol !== symbol) {
|
|
79
97
|
const resolved = resolveExportedSymbol(exportSymbol, checker, cache, seen);
|
|
80
98
|
cache.set(symbol, resolved);
|
|
@@ -95,8 +113,12 @@ function resolveExportedSymbol(symbol, checker, cache, visited) {
|
|
|
95
113
|
return symbol;
|
|
96
114
|
}
|
|
97
115
|
}
|
|
98
|
-
// final validation
|
|
116
|
+
// final validation - `.some(isConcreteDeclaration)` re-checks the exact same
|
|
117
|
+
// predicate over the exact same `declarations` array the for-loop above just
|
|
118
|
+
// iterated without finding a match; if the loop didn't return, `.some` of
|
|
119
|
+
// the identical predicate over the identical array is guaranteed false too.
|
|
99
120
|
const valid = declarations.some(isConcreteDeclaration);
|
|
121
|
+
/* v8 ignore next 3 */
|
|
100
122
|
if (valid) {
|
|
101
123
|
cache.set(symbol, symbol);
|
|
102
124
|
return symbol;
|
|
@@ -41,6 +41,11 @@ function resolveBoundObjectNode(rawNode, checker, context) {
|
|
|
41
41
|
if (!symbol || !context.parameterBindings.has(symbol)) {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
|
+
// `has(symbol)` above already guarantees a Map entry exists, and every
|
|
45
|
+
// caller only ever `.set()`s parameterBindings to a real ts.Expression
|
|
46
|
+
// (never `undefined`) - so `.get(symbol)` can't actually miss here; the
|
|
47
|
+
// `?? null` is defensive typing, not a reachable second outcome.
|
|
48
|
+
/* v8 ignore next */
|
|
44
49
|
return context.parameterBindings.get(symbol) ?? null;
|
|
45
50
|
}
|
|
46
51
|
if (typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isPropertyAccessChain(node) || typescript_1.default.isElementAccessExpression(node)) {
|
|
@@ -100,12 +105,22 @@ function resolvePropertyAccess(node, checker, context, visited, evaluationCache,
|
|
|
100
105
|
// LOGIN = "login"
|
|
101
106
|
// }
|
|
102
107
|
if (symbol) {
|
|
108
|
+
// symbol comes from checker.getSymbolAtLocation (never a hand-built
|
|
109
|
+
// duck-typed object here), and real ts.Symbol.getDeclarations() always
|
|
110
|
+
// returns an array (possibly empty) rather than undefined - the `?? []`
|
|
111
|
+
// is defensive against the wider `Declaration[] | undefined` type, not
|
|
112
|
+
// a reachable second outcome for a real symbol.
|
|
113
|
+
/* v8 ignore next */
|
|
103
114
|
const declarations = symbol.getDeclarations() ?? [];
|
|
104
115
|
for (const declaration of declarations) {
|
|
105
116
|
if (!typescript_1.default.isEnumDeclaration(declaration)) {
|
|
106
117
|
continue;
|
|
107
118
|
}
|
|
108
119
|
for (const member of declaration.members) {
|
|
120
|
+
// An EnumMember's `name` can only be an Identifier or a
|
|
121
|
+
// StringLiteral per the TS grammar - the `: null` fallback has no
|
|
122
|
+
// third case to reach.
|
|
123
|
+
/* v8 ignore next */
|
|
109
124
|
const memberName = typescript_1.default.isIdentifier(member.name) ? member.name.text : typescript_1.default.isStringLiteral(member.name) ? member.name.text : null;
|
|
110
125
|
if (memberName !== propertyName || !member.initializer) {
|
|
111
126
|
continue;
|
|
@@ -118,6 +133,9 @@ function resolvePropertyAccess(node, checker, context, visited, evaluationCache,
|
|
|
118
133
|
// LOGIN: "login"
|
|
119
134
|
// }
|
|
120
135
|
if (symbol) {
|
|
136
|
+
// See the identical `?? []` above the enum-member loop: a real
|
|
137
|
+
// ts.Symbol here always has a declarations array.
|
|
138
|
+
/* v8 ignore next */
|
|
121
139
|
const declarations = symbol.getDeclarations() ?? [];
|
|
122
140
|
for (const declaration of declarations) {
|
|
123
141
|
if (!typescript_1.default.isVariableDeclaration(declaration) || !declaration.initializer || !typescript_1.default.isObjectLiteralExpression(declaration.initializer)) {
|
|
@@ -143,6 +161,10 @@ function resolveIdentifier(node, checker, context, visited, evaluationCache, res
|
|
|
143
161
|
const boundSymbol = checker.getSymbolAtLocation(node);
|
|
144
162
|
if (boundSymbol && context.parameterBindings.has(boundSymbol)) {
|
|
145
163
|
const bound = context.parameterBindings.get(boundSymbol);
|
|
164
|
+
// `has(boundSymbol)` above already guarantees a Map entry, and every
|
|
165
|
+
// caller only ever `.set()`s parameterBindings to a real ts.Expression
|
|
166
|
+
// (never `undefined`) - so this is always truthy when reached.
|
|
167
|
+
/* v8 ignore else */
|
|
146
168
|
if (bound) {
|
|
147
169
|
return resolveNodeValue(bound, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
148
170
|
}
|
|
@@ -158,6 +180,9 @@ function resolveIdentifier(node, checker, context, visited, evaluationCache, res
|
|
|
158
180
|
}
|
|
159
181
|
const resolving = empty();
|
|
160
182
|
evaluationCache.set(symbol, resolving);
|
|
183
|
+
// See the identical `?? []` above in resolvePropertyAccess: a real
|
|
184
|
+
// ts.Symbol here always has a declarations array.
|
|
185
|
+
/* v8 ignore next */
|
|
161
186
|
const declarations = symbol.getDeclarations() ?? [];
|
|
162
187
|
for (const declaration of declarations) {
|
|
163
188
|
if (typescript_1.default.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
@@ -195,7 +220,12 @@ function resolveCallExpression(node, checker, context, visited, evaluationCache,
|
|
|
195
220
|
const nextContext = {
|
|
196
221
|
parameterBindings: new Map(context.parameterBindings),
|
|
197
222
|
};
|
|
223
|
+
// returnPropagationAnalyzer's resolveParameter only ever returns a
|
|
224
|
+
// parameter whose `.name` is a plain ts.Identifier (destructured/
|
|
225
|
+
// computed parameter names are filtered out there), so a real program's
|
|
226
|
+
// checker always resolves a symbol for it here.
|
|
198
227
|
const parameterSymbol = checker.getSymbolAtLocation(propagation.parameter.name);
|
|
228
|
+
/* v8 ignore next 2 */
|
|
199
229
|
if (parameterSymbol) {
|
|
200
230
|
nextContext.parameterBindings.set(parameterSymbol, argument);
|
|
201
231
|
}
|
|
@@ -18,6 +18,17 @@ function isTrackCallee(expr) {
|
|
|
18
18
|
}
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
|
+
// An ImportSpecifier/ImportClause's own symbol always carries
|
|
22
|
+
// SymbolFlags.Alias (that's how TS's binder models every import), so
|
|
23
|
+
// isEventraSymbol's alias-unwrap below always recurses into
|
|
24
|
+
// checker.getAliasedSymbol(...) first and never falls through to inspect
|
|
25
|
+
// these declarations directly - getAliasedSymbol also fully resolves a
|
|
26
|
+
// multi-hop re-export chain in a single call (verified empirically: a
|
|
27
|
+
// 12-deep `export { Eventra } from "./prev"` barrel chain still resolves
|
|
28
|
+
// in exactly one getAliasedSymbol hop), so there's no realistic import
|
|
29
|
+
// shape where a symbol reaches isEventraSymbol's declaration loop while
|
|
30
|
+
// one of its own declarations is still an ImportSpecifier/ImportClause.
|
|
31
|
+
/* v8 ignore start */
|
|
21
32
|
function getImportDeclarationForSpecifier(decl) {
|
|
22
33
|
let current = decl;
|
|
23
34
|
while (current) {
|
|
@@ -43,6 +54,7 @@ function isEventraImportDeclaration(decl) {
|
|
|
43
54
|
}
|
|
44
55
|
return false;
|
|
45
56
|
}
|
|
57
|
+
/* v8 ignore stop */
|
|
46
58
|
function isEventraClassDeclaration(decl) {
|
|
47
59
|
if (!typescript_1.default.isClassDeclaration(decl) || decl.name?.text !== "Eventra") {
|
|
48
60
|
return false;
|
|
@@ -51,12 +63,21 @@ function isEventraClassDeclaration(decl) {
|
|
|
51
63
|
return file.includes("eventra-sdk") || file.includes("@eventra_dev");
|
|
52
64
|
}
|
|
53
65
|
function isEventraSymbol(symbol, checker, depth = 0) {
|
|
66
|
+
// cycle guard - the only recursive call below is the alias-unwrap, and
|
|
67
|
+
// checker.getAliasedSymbol always resolves a whole (even multi-hop)
|
|
68
|
+
// alias/re-export chain in a single call rather than one hop at a time
|
|
69
|
+
// (verified empirically), so depth realistically never exceeds 1.
|
|
70
|
+
/* v8 ignore next 3 */
|
|
54
71
|
if (depth > 10) {
|
|
55
72
|
return false;
|
|
56
73
|
}
|
|
57
74
|
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
58
75
|
return isEventraSymbol(checker.getAliasedSymbol(symbol), checker, depth + 1);
|
|
59
76
|
}
|
|
77
|
+
// A real ts.Symbol.getDeclarations() always returns an array (possibly
|
|
78
|
+
// empty), never undefined - the `?? []` is defensive against the wider
|
|
79
|
+
// `Declaration[] | undefined` type, not a reachable second outcome.
|
|
80
|
+
/* v8 ignore next */
|
|
60
81
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
61
82
|
if (isEventraImportDeclaration(decl) || isEventraClassDeclaration(decl)) {
|
|
62
83
|
return true;
|
|
@@ -73,6 +94,11 @@ function isEventraNewExpression(node, checker) {
|
|
|
73
94
|
return symbol ? isEventraSymbol(symbol, checker) : false;
|
|
74
95
|
}
|
|
75
96
|
function typeLooksLikeEventra(type, checker, depth = 0) {
|
|
97
|
+
// cycle guard - TypeScript flattens union/intersection types (a literal
|
|
98
|
+
// `A | (B | C)` normalizes to the single-level `A | B | C`), so the only
|
|
99
|
+
// recursive call below in practice never nests deep enough to approach
|
|
100
|
+
// this bound.
|
|
101
|
+
/* v8 ignore next 3 */
|
|
76
102
|
if (depth > 8) {
|
|
77
103
|
return false;
|
|
78
104
|
}
|
|
@@ -105,6 +131,9 @@ function isEventraReceiverExpression(receiver, checker) {
|
|
|
105
131
|
if (!symbol) {
|
|
106
132
|
return false;
|
|
107
133
|
}
|
|
134
|
+
// See the identical `?? []` above isEventraSymbol's declaration loop: a
|
|
135
|
+
// real ts.Symbol here always has a declarations array.
|
|
136
|
+
/* v8 ignore next */
|
|
108
137
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
109
138
|
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
110
139
|
if (typescript_1.default.isNewExpression(decl.initializer) && isEventraNewExpression(decl.initializer, checker)) {
|
|
@@ -122,6 +151,13 @@ function isEventraReceiverExpression(receiver, checker) {
|
|
|
122
151
|
return true;
|
|
123
152
|
}
|
|
124
153
|
}
|
|
154
|
+
// A class field can only ever be reached through a qualified access
|
|
155
|
+
// (`this.field`/`instance.field`), which recurses through the
|
|
156
|
+
// PropertyAccessExpression branch below instead of ever landing here -
|
|
157
|
+
// a *bare* identifier's resolved symbol can't have a PropertyDeclaration
|
|
158
|
+
// among its own declarations, so this is unreachable from the
|
|
159
|
+
// `ts.isIdentifier(receiver)` branch above.
|
|
160
|
+
/* v8 ignore next 5 */
|
|
125
161
|
if (typescript_1.default.isPropertyDeclaration(decl) && decl.type) {
|
|
126
162
|
const propType = checker.getTypeFromTypeNode(decl.type);
|
|
127
163
|
if (typeLooksLikeEventra(propType, checker)) {
|
|
@@ -141,11 +177,16 @@ function isEventraSdkTrackCall(call, checker) {
|
|
|
141
177
|
if (!isTrackCallee(expr)) {
|
|
142
178
|
return false;
|
|
143
179
|
}
|
|
180
|
+
// isTrackCallee above only returns true for these same three shapes, so
|
|
181
|
+
// the `: undefined` side of this ternary is never actually taken.
|
|
182
|
+
/* v8 ignore next 5 */
|
|
144
183
|
const receiver = typescript_1.default.isPropertyAccessExpression(expr) ||
|
|
145
184
|
typescript_1.default.isPropertyAccessChain(expr) ||
|
|
146
185
|
typescript_1.default.isElementAccessExpression(expr)
|
|
147
186
|
? expr.expression
|
|
148
187
|
: undefined;
|
|
188
|
+
// ...and so `receiver` is never actually undefined here either.
|
|
189
|
+
/* v8 ignore next 3 */
|
|
149
190
|
if (!receiver) {
|
|
150
191
|
return false;
|
|
151
192
|
}
|
|
@@ -43,14 +43,12 @@ function getCallName(expression) {
|
|
|
43
43
|
}
|
|
44
44
|
//
|
|
45
45
|
// analytics.track()
|
|
46
|
-
//
|
|
47
|
-
if (typescript_1.default.isPropertyAccessExpression(expression)) {
|
|
48
|
-
return expression.name.text;
|
|
49
|
-
}
|
|
50
|
-
//
|
|
51
46
|
// analytics?.track()
|
|
52
47
|
//
|
|
53
|
-
|
|
48
|
+
// ts.isPropertyAccessExpression is also true for a PropertyAccessChain
|
|
49
|
+
// (optional-chain access is the same SyntaxKind with questionDotToken set),
|
|
50
|
+
// so there is no separate isPropertyAccessChain case to handle here.
|
|
51
|
+
if (typescript_1.default.isPropertyAccessExpression(expression)) {
|
|
54
52
|
return expression.name.text;
|
|
55
53
|
}
|
|
56
54
|
return "";
|
package/dist/cli/send.js
CHANGED
|
@@ -28,6 +28,12 @@ function backoffMs(attempt) {
|
|
|
28
28
|
// single delivery attempt with abort timeout
|
|
29
29
|
async function attemptDelivery(endpoint, apiKey, payload) {
|
|
30
30
|
const controller = new AbortController();
|
|
31
|
+
// Exercising this callback for real means either a genuine 10s wait per
|
|
32
|
+
// attempt (too slow for a unit test, especially across MAX_ATTEMPTS
|
|
33
|
+
// retries) or faking timers through an AbortController+fetch+retry-loop
|
|
34
|
+
// chain, which deadlocks vi.runAllTimersAsync/advanceTimersByTimeAsync in
|
|
35
|
+
// practice - not worth the added flakiness for one timeout wire-up line.
|
|
36
|
+
/* v8 ignore next */
|
|
31
37
|
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
32
38
|
try {
|
|
33
39
|
const res = await fetch(endpoint, {
|
|
@@ -89,9 +89,11 @@ class CompilerContext {
|
|
|
89
89
|
return fromRegistry;
|
|
90
90
|
}
|
|
91
91
|
const fromDisk = originalGetSourceFile(normalized, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
92
|
+
/* v8 ignore if -- unreachable: host.readFile (below) only ever returns a string or throws (fs.readFileSync semantics); ts's default getSourceFile only yields undefined when readFile resolves to undefined without throwing, which this host can never produce */
|
|
92
93
|
if (!fromDisk) {
|
|
93
94
|
return undefined;
|
|
94
95
|
}
|
|
96
|
+
/* v8 ignore else -- unreachable: reaching this line already required an undefined snapshot above, and DocumentRecord always carries a snapshot once registry.has() is true, so has() cannot have flipped true in between */
|
|
95
97
|
if (!this.registry.has(normalized)) {
|
|
96
98
|
this.registry.update(normalized, fromDisk.getFullText());
|
|
97
99
|
}
|
|
@@ -70,6 +70,11 @@ function pluginLoadHint(spec) {
|
|
|
70
70
|
}
|
|
71
71
|
async function loadExternalPlugin(registry, spec) {
|
|
72
72
|
const trimmed = spec.trim();
|
|
73
|
+
// Defensive-only: the sole caller (createPluginRegistry) already does
|
|
74
|
+
// `if (!trimmed) continue;` before ever calling this function, so `spec`
|
|
75
|
+
// here is always non-blank. Kept as a guard in case a future caller is
|
|
76
|
+
// added that doesn't pre-filter.
|
|
77
|
+
/* v8 ignore next */
|
|
73
78
|
if (!trimmed)
|
|
74
79
|
return;
|
|
75
80
|
let mod;
|
|
@@ -77,6 +82,12 @@ async function loadExternalPlugin(registry, spec) {
|
|
|
77
82
|
mod = await Promise.resolve(`${trimmed}`).then(s => __importStar(require(s)));
|
|
78
83
|
}
|
|
79
84
|
catch (err) {
|
|
85
|
+
// Node's dynamic import() only ever rejects with an Error (module
|
|
86
|
+
// resolution, syntax, or evaluation errors are all Error instances) —
|
|
87
|
+
// the String(err) fallback can't be reached without a plugin module
|
|
88
|
+
// whose top-level code does a bare `throw "non-error value"`, which
|
|
89
|
+
// we won't fabricate as a persistent test fixture. Defensive-only.
|
|
90
|
+
/* v8 ignore next */
|
|
80
91
|
const message = err instanceof Error ? err.message : String(err);
|
|
81
92
|
throw new Error(`Failed to load plugin "${trimmed}": ${message}.${pluginLoadHint(trimmed)}`);
|
|
82
93
|
}
|