@eventra_dev/eventra-cli 2.1.0 → 2.3.0
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 -11
- package/dist/analysis/resolver/callResolver.js +16 -2
- package/dist/analysis/scanner/scanner.js +1 -0
- package/dist/analysis/scanner/wrapperChains.js +115 -0
- package/dist/analysis/sdk/eventraSdk.js +6 -4
- package/dist/analysis/symbols/wrapperRegistry.js +14 -5
- package/dist/cli/check.js +25 -9
- package/dist/cli/init.js +87 -23
- package/dist/cli/projectDependencies.js +8 -3
- package/dist/cli/send.js +31 -2
- package/dist/cli/sync.js +13 -10
- package/dist/cli/watch.js +163 -62
- package/dist/compiler/compilerContext.js +246 -6
- package/dist/compiler/documentRegistry.js +15 -1
- package/dist/compiler/importGraph.js +8 -0
- package/dist/compiler/workspacePackages.js +58 -0
- package/dist/config/config.js +61 -3
- package/dist/core/EventraEngine.js +143 -16
- package/dist/core/catalogMeta.js +91 -0
- package/dist/core/fileLimits.js +12 -0
- package/dist/core/projectScanner.js +14 -24
- package/dist/core/scanFilters.js +146 -0
- package/dist/core/scanResults.js +21 -7
- package/dist/core/skipped.js +77 -0
- package/dist/index.js +49 -38
- package/dist/plugin/builtins/configuredSinks.js +84 -17
- package/dist/plugin/builtins/consoleDynamicEventReporter.js +21 -2
- package/dist/plugin/loadPlugins.js +20 -2
- package/dist/plugin/registry.js +83 -1
- package/dist/respawn.js +89 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
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-783%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
|
|
12
12
|
<img alt="e2e fixtures" src="https://img.shields.io/badge/e2e-14%20fixtures-brightgreen?style=flat-square">
|
|
13
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">
|
|
@@ -95,6 +95,27 @@ function trackDestructured({ event: name }: { event: string }) {
|
|
|
95
95
|
trackDestructured({ event: "checkout" });
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
A wrapper can call another wrapper, at any depth and across files. `trackEvent("x")` is found even when it only reaches the SDK through `capture`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// capture.ts
|
|
102
|
+
export function capture(name: string, props?: object) { posthog.capture(name, props); }
|
|
103
|
+
|
|
104
|
+
// track.ts
|
|
105
|
+
export function trackEvent<E extends string>(event: E) { capture(event); }
|
|
106
|
+
|
|
107
|
+
// page.ts
|
|
108
|
+
trackEvent("checkout_started");
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Functions handed back by a hook count too (`const analytics = useAnalytics(); analytics.capture("x")`, where the hook ends with `return { capture }`), and so does a library loaded on demand (`import("posthog-js").then(({ default: ph }) => ph.capture(name))`, `await import(...)`).
|
|
112
|
+
|
|
113
|
+
### Monorepos
|
|
114
|
+
|
|
115
|
+
Each import is resolved with the compiler options of the nearest `tsconfig.json` above the file that writes it (a config that sets no `paths` keeps the root's aliases; `node_modules` is never read for options), so an app with its own `@/*` or `~/*` aliases works without a tsconfig at the repository root. When a root `tsconfig.json` only lists `references` (the usual Vite and Vue layout), the aliases of the referenced config are used. Packages of the same repository (`workspaces` or `pnpm-workspace.yaml`) are found by their folder (`@acme/lib`, `@acme/lib/hooks/use-x`, also when the package keeps its sources in `src/`), so the scan does not need `node_modules` to link them. A real install still takes precedence.
|
|
116
|
+
|
|
117
|
+
`sync`, `check`, `watch` and `send` on a machine with room to spare (a container's memory limit counts) start themselves again with a larger memory limit (half of the machine's memory, at most 8 GB), unless you set `--max-old-space-size` yourself; `init` and `--version` never do. A new `eventra.json` written by `init` leaves out tests and coverage output (see `sync.exclude`); a config that already exists keeps scanning what it scanned.
|
|
118
|
+
|
|
98
119
|
### Variables, templates, conditionals
|
|
99
120
|
|
|
100
121
|
```ts
|
|
@@ -156,7 +177,7 @@ Already have analytics in place? Declare it in `eventra.json` and the CLI treats
|
|
|
156
177
|
|
|
157
178
|
Rules are plain data and never run code, so they are safe to commit. A rule with a typo, an invalid `when` or an unknown preset is skipped with a warning. A function with its own name (`trackEvent({ event: "x" })`) is not covered by a preset: add a rule such as `{ "callee": "trackEvent", "nameProperty": "event" }`.
|
|
158
179
|
|
|
159
|
-
The [name rules](#event-name-rules-aligned-with-sdk) below apply to these events too. Spaces and colons are allowed, so names like `App: User Sign Up` or `Order Completed` are kept as written.
|
|
180
|
+
The [name rules](#event-name-rules-aligned-with-sdk) below apply to these events too. Spaces and colons are allowed, so names like `App: User Sign Up` or `Order Completed` are kept as written. PostHog's `$`-prefixed names (`$pageview`, `$pageleave`) are kept as well. Names with other characters, such as one ending in `!`, are excluded from the scan result.
|
|
160
181
|
|
|
161
182
|
---
|
|
162
183
|
|
|
@@ -165,7 +186,7 @@ The [name rules](#event-name-rules-aligned-with-sdk) below apply to these events
|
|
|
165
186
|
| Rule | Limit |
|
|
166
187
|
|------|--------|
|
|
167
188
|
| Max length | **64 characters** (same as SDK) |
|
|
168
|
-
| Allowed characters | `a-zA-Z0-9
|
|
189
|
+
| Allowed characters | `a-zA-Z0-9$:_./-` and the space (so `App: User Sign Up` and `$pageview` work); tabs, newlines and any other character are rejected |
|
|
169
190
|
| Position | First argument of `.track(name, options?)` |
|
|
170
191
|
|
|
171
192
|
The second argument (`userId`, `properties`) is **not** used as the event name.
|
|
@@ -194,7 +215,7 @@ tracker.track({ event: "click" });
|
|
|
194
215
|
|
|
195
216
|
### `eventra init`
|
|
196
217
|
|
|
197
|
-
Creates `eventra.json`. It asks for an API key and an endpoint, then looks through `package.json` (and the `package.json` of every workspace member in a monorepo: `workspaces` or `pnpm-workspace.yaml`) for analytics libraries:
|
|
218
|
+
Creates `eventra.json`. It asks for an API key and an endpoint, then looks through `package.json` (and the `package.json` of every workspace member in a monorepo: `workspaces` or `pnpm-workspace.yaml`) for analytics libraries and for the framework (`vue`, `svelte`, `astro`, `@angular/core`), and writes the matching plugin into `plugins` when it is installed. If it is not, `init` prints the install command and the line to add instead: `sync` stops on a plugin it cannot load, so `init` never writes one that would make the next `sync` fail:
|
|
198
219
|
|
|
199
220
|
```
|
|
200
221
|
Found analytics in package.json: posthog-js; @segment/analytics-next
|
|
@@ -212,7 +233,7 @@ Everything found is pre-selected; press Enter to accept or untick what you do no
|
|
|
212
233
|
| `amplitude` | `@amplitude/analytics-browser`, `@amplitude/analytics-node`, `@amplitude/analytics-react-native`, `@amplitude/unified`, `@amplitude/marketing-analytics-browser`, and the older `amplitude-js` |
|
|
213
234
|
| `ga4` | `react-ga4`, `vue-gtag`, `@next/third-parties`, `nuxt-gtag`, `@types/gtag.js`, and the older `vue-gtag-next` |
|
|
214
235
|
|
|
215
|
-
Detection reads dependencies only. A tag pasted straight into the HTML (a Segment or `gtag.js` snippet with no package) is not seen: add the preset to `sinks` yourself. In a monorepo only the block-list form of `packages:` in `pnpm-workspace.yaml` is understood (not `packages: [a, b]`), and workspace patterns that are absolute or contain `..` are ignored.
|
|
236
|
+
Detection reads dependencies only. A tag pasted straight into the HTML (a Segment or `gtag.js` snippet with no package) is not seen: add the preset to `sinks` yourself. In a monorepo only the block-list form of `packages:` in `pnpm-workspace.yaml` is understood (not `packages: [a, b]`), and workspace patterns that are absolute or contain `..` are ignored. If `eventra.json` already exists, `init` asks before overwriting it (the default answer is no). `eventra init --yes` never prompts: it uses the default endpoint, accepts every detected preset, and leaves an existing `eventra.json` untouched, so it is safe in scripts and CI.
|
|
216
237
|
|
|
217
238
|
### `eventra sync`
|
|
218
239
|
|
|
@@ -221,9 +242,11 @@ Full project scan → updates `eventra.json`:
|
|
|
221
242
|
- `events` — discovered event names
|
|
222
243
|
- `functionWrappers` — detected wrapper functions
|
|
223
244
|
|
|
245
|
+
A file that cannot be analysed (a broken `.vue`, `.svelte`, `.astro` or `.html`, an unreadable file, or one over 2 MB) is skipped, and `sync` prints one summary at the end listing each skipped file and why. While anything is skipped, events already in `eventra.json` are kept, because a missing event may simply live in a skipped file. The exit code stays `0`; `eventra sync --strict` exits `1` if any file was skipped.
|
|
246
|
+
|
|
224
247
|
### `eventra check`
|
|
225
248
|
|
|
226
|
-
Compares config with the current scan (events + wrappers). Exit code `1` on drift.
|
|
249
|
+
Compares config with the current scan (events + wrappers). Exit code `1` on drift. When files were skipped, known events that were not found are not reported as removed (they cannot be verified) and the skipped files are listed; `eventra check --strict` also exits `1` if any file was skipped.
|
|
227
250
|
|
|
228
251
|
### `eventra check --fix`
|
|
229
252
|
|
|
@@ -255,6 +278,14 @@ This records the endpoint in `eventra.local.json` (gitignored). If `eventra.json
|
|
|
255
278
|
|
|
256
279
|
Once `eventra.json` already commits an `endpoint`, that committed value is what events actually get sent to - `EVENTRA_ENDPOINT` only controls whether the approval prompt is skipped; its own value is not read or compared against anything in that case. `EVENTRA_ENDPOINT` becomes the real destination only when `eventra.json` has no `endpoint` set at all. In other words, setting the variable to point somewhere else does not redirect traffic away from an already-committed endpoint - it only means you won't be asked to approve it.
|
|
257
280
|
|
|
281
|
+
**Catalog snapshot.** Besides the event list, `send` scans the project once more and tells the server how trustworthy that list is, so the dashboard can compare what is in your code with what actually arrived. It adds four optional fields to the request: `complete`, `dynamicCount`, `branch` and `cliVersion`.
|
|
282
|
+
|
|
283
|
+
- `complete` is `true` only when the scan skipped no file, hit no error, and `eventra.json` matches what the scan just found. The server marks names that are missing from a complete snapshot as removed from code, so anything doubtful is `false` (a file that could not be parsed, a failed scan, or an `eventra.json` that is out of date: run `eventra sync` first). A request that is not complete never removes anything.
|
|
284
|
+
- `dynamicCount` is the number of event-name call sites that could not be resolved to any string (for example `track(name)` with an unknown `name`). The dashboard uses it to warn that such names may show up as unexpected events.
|
|
285
|
+
- `branch` is the current branch, read from the usual CI variables (`GITHUB_REF_NAME`, `CI_COMMIT_REF_NAME`, `BRANCH_NAME`, `BITBUCKET_BRANCH`, `CIRCLE_BRANCH`, `BUILDKITE_BRANCH`, `VERCEL_GIT_COMMIT_REF`) and otherwise from `git`; it is left out when unknown (for example a detached checkout with no CI variable). `cliVersion` is the CLI version.
|
|
286
|
+
|
|
287
|
+
Run `send` from the main branch in CI: a snapshot from a feature branch describes that branch. A server that predates these fields rejects them, so `send` repeats the request once with the plain event list. The extra scan adds the same time as one `eventra sync`.
|
|
288
|
+
|
|
258
289
|
Network resilience:
|
|
259
290
|
|
|
260
291
|
- Up to 4 attempts with exponential backoff + jitter (capped at 8 s)
|
|
@@ -291,6 +322,7 @@ The plugin:
|
|
|
291
322
|
- parses each `.vue` file with the real Vue compiler (`@vue/compiler-sfc`) into a single virtual TypeScript module combining `<script>`/`<script setup>`
|
|
292
323
|
- extracts `track()` calls from the script exactly like a regular `.ts` file (direct calls, wrappers, propagation)
|
|
293
324
|
- detects `event="feature_name"` attributes in `<template>` — 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
|
|
325
|
+
- reads calls written anywhere in the template: `@click`/`v-on` handlers (several statements, multi-line arrows), `{{ }}` interpolations and the expressions of `v-bind`, `v-if`, `v-show`, `v-model`, `v-html`, `v-text`
|
|
294
326
|
- Nuxt `.vue` files (pages/layouts/components) are handled the same way; auto-imported composables resolve once the project has run `nuxt prepare`/`nuxt dev`
|
|
295
327
|
|
|
296
328
|
`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.
|
|
@@ -311,6 +343,7 @@ The plugin:
|
|
|
311
343
|
|
|
312
344
|
- parses each `.svelte` file with the real Svelte compiler (`svelte/compiler`) into a single virtual TypeScript module combining `context="module"` and instance `<script>` blocks
|
|
313
345
|
- extracts `track()` calls from the script exactly like a regular `.ts` file (direct calls, wrappers, propagation); Svelte 5 runes (`$props`, `$state`, …) pass through untouched
|
|
346
|
+
- reads calls anywhere in the markup: handlers, `{expr}`, `{@html}`, block expressions (`{#if}`, `{#each}`, `{#key}`, `{#await}`), `use:`/`class:`/`transition:` directives and attribute values
|
|
314
347
|
- 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
|
|
315
348
|
|
|
316
349
|
`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.
|
|
@@ -331,6 +364,7 @@ The plugin:
|
|
|
331
364
|
|
|
332
365
|
- parses each `.astro` file with the real Astro compiler (`@astrojs/compiler`) into a single virtual TypeScript module from the frontmatter
|
|
333
366
|
- extracts `track()` calls from the frontmatter exactly like a regular `.ts` file (direct calls, wrappers, propagation); `Astro.props` and other globals pass through untouched
|
|
367
|
+
- reads calls in template expressions (`{track("x")}`, `attr={expr}`, `{...spread}`) and in the page's `<script>` tags, each as a module of its own (quoted `onclick="..."` strings are browser text and are not read)
|
|
334
368
|
- 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
|
|
335
369
|
|
|
336
370
|
`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.
|
|
@@ -355,6 +389,7 @@ The plugin:
|
|
|
355
389
|
|
|
356
390
|
- parses each component template with the real Angular compiler (`@angular/compiler`) into a virtual TypeScript module, paired with its component class (`foo.component.html` ↔ `foo.component.ts`, Angular CLI's default naming)
|
|
357
391
|
- extracts `track()` calls from the component class exactly like a regular `.ts` file (direct calls, wrappers, propagation) - no plugin needed for that part
|
|
392
|
+
- reads calls in the template: `(click)` handlers, `[input]` bindings, `{{ }}` interpolations, `*ngIf` and `@if`/`@switch`/`@for`, written against the component class (`track('x')` is `this.track('x')`); an `.html` file no component names as its `templateUrl` is skipped when it does not parse
|
|
358
393
|
- detects `event="feature_name"` attributes on any tag, including `*ngIf`/`*ngFor` and the `@if`/`@for`/`@switch` block syntax - both literal and dynamic; a dynamic binding resolves against the component instance (`this.field`, including getters), or surfaces as a dynamic occurrence if unresolvable
|
|
359
394
|
- use `[attr.event]="expr"`, not `[event]="expr"`, for a dynamic binding on a plain HTML element - `[event]` is a property binding that Angular's real compiler rejects (`NG8002`) unless the element is a component that actually declares `@Input() event`; both forms are detected identically by the plugin, only `[attr.event]` is guaranteed to compile
|
|
360
395
|
|
|
@@ -416,7 +451,7 @@ No dependency on `@eventra_dev/eventra-cli` is required — the host CLI loads t
|
|
|
416
451
|
| `plugins` | `@eventra_dev/cli-plugin-*` specifiers to `import()` at startup. Anything outside that scope is skipped (see [Plugin contract](#plugin-contract-for-authors)) |
|
|
417
452
|
| `sinks` | Third-party analytics calls to treat as event sources, e.g. `[{ "preset": "posthog" }]` (see [Third-party analytics](#third-party-analytics-sinks)) |
|
|
418
453
|
| `sync.include` | Base glob patterns; plugin `includeGlobs` are merged automatically |
|
|
419
|
-
| `sync.exclude` | Paths skipped during scan |
|
|
454
|
+
| `sync.exclude` | Paths skipped during scan. Default for a config that has none: `node_modules`, `dist`, `.next`, `.git`. `eventra init` writes a longer list into a new `eventra.json`: that plus `coverage`, test files (`**/*.{test,spec}.{ts,tsx,js,jsx}`) and `__tests__`/`__mocks__` folders. A plain name such as `node_modules` or `dist` matches at any depth (nested `packages/*/node_modules` too); entries containing `/` or glob characters are used as written |
|
|
420
455
|
|
|
421
456
|
`eventra init`/`eventra send` never write a real key into this file — they write to `eventra.local.json` instead, which is automatically added to `.gitignore`.
|
|
422
457
|
|
|
@@ -429,7 +464,7 @@ No dependency on `@eventra_dev/eventra-cli` is required — the host CLI loads t
|
|
|
429
464
|
3. Run file preprocessors (e.g. `.vue` → virtual `.ts` modules).
|
|
430
465
|
4. Load sources into an incremental TypeScript program (with SDK type shim).
|
|
431
466
|
5. **Phase 1** — find `Eventra` instances from `@eventra_dev/eventra-sdk` and register function wrappers that call `.track()`.
|
|
432
|
-
6. **Phase 2**
|
|
467
|
+
6. **Phase 2** - resolve static event names and wrapper propagation chains, a wrapper that calls another wrapper included (sink detector chain includes plugin sinks).
|
|
433
468
|
7. Write results to `eventra.json`.
|
|
434
469
|
|
|
435
470
|
`watch` tracks **disk source files** (including `.vue`), re-runs preprocessors on change, and incrementally updates the engine.
|
|
@@ -440,14 +475,14 @@ No runtime execution. No monkey-patching.
|
|
|
440
475
|
|
|
441
476
|
## Requirements
|
|
442
477
|
|
|
443
|
-
- Node.js 18+
|
|
478
|
+
- Node.js 18+ (`eventra watch` needs Node.js 20.19+, which its file-watching dependency requires; `init`, `sync`, `check` and `send` run on 18)
|
|
444
479
|
- TypeScript/JavaScript source using `@eventra_dev/eventra-sdk`
|
|
445
480
|
|
|
446
481
|
---
|
|
447
482
|
|
|
448
483
|
## Test Coverage
|
|
449
484
|
|
|
450
|
-
Two test layers, **
|
|
485
|
+
Two test layers, **925 unit tests + 14 e2e fixtures + 3 `check` exit-code scenarios + 1 `watch` scenario**.
|
|
451
486
|
|
|
452
487
|
**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.
|
|
453
488
|
|
|
@@ -458,7 +493,10 @@ Two test layers, **665 unit tests + 14 e2e fixtures + 3 `check` exit-code scenar
|
|
|
458
493
|
| `ImportGraph` | Forward/reverse edges, cycles, stale-edge cleanup, file removal |
|
|
459
494
|
| `Scheduler` | Batch coalescing, last-write-wins per file, sequential bursts, error propagation |
|
|
460
495
|
| `DocumentRegistry` | Path normalization, version bumping, no-op on identical content, `ensure()` from disk |
|
|
461
|
-
| `CompilerContext` | Stage/update/remove files, `resolveModule` with `tsconfig.json` `paths`, source-file enumeration |
|
|
496
|
+
| `CompilerContext` | Stage/update/remove files, `resolveModule` with `tsconfig.json` `paths`, source-file enumeration, nearest `tsconfig.json` per file, `references`, workspace packages without an install |
|
|
497
|
+
| `wrapper chains` | A wrapper calling a wrapper (depth 2 and 3), either file order, generics, destructuring, cycles, watch updates |
|
|
498
|
+
| `hook wrappers` | `return { capture }`, dynamic `import()` receivers, `.then` callbacks, promise-typed getters |
|
|
499
|
+
| `respawn` | Heap sizing (a container's lower memory limit counts), only scanning commands respawn, signals reach the child, no second respawn, explicit `--max-old-space-size` respected |
|
|
462
500
|
| `EventraEngine` | Direct calls, SDK isolation, cross-file wrappers, file updates, file removal, wrapper filtering |
|
|
463
501
|
| `PluginRegistry` | Built-in SDK sink, preprocessors, virtual-path mapping, include-pattern dedup |
|
|
464
502
|
| `external plugin adapter` | Transform output mapping, static sink registration, invalid result rejection |
|
|
@@ -9,7 +9,7 @@ const exportResolver_1 = require("./exportResolver");
|
|
|
9
9
|
function isFunctionLike(node) {
|
|
10
10
|
return (typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isArrowFunction(node) || typescript_1.default.isFunctionExpression(node));
|
|
11
11
|
}
|
|
12
|
-
function resolveDeclaration(declaration) {
|
|
12
|
+
function resolveDeclaration(declaration, checker) {
|
|
13
13
|
// function foo() {}
|
|
14
14
|
if (isFunctionLike(declaration)) {
|
|
15
15
|
return declaration;
|
|
@@ -26,6 +26,20 @@ function resolveDeclaration(declaration) {
|
|
|
26
26
|
if (typescript_1.default.isPropertyAssignment(declaration) && declaration.initializer && isFunctionLike(declaration.initializer)) {
|
|
27
27
|
return declaration.initializer;
|
|
28
28
|
}
|
|
29
|
+
// `return { capture }` / `return { capture: capture }`: a hook or factory that hands back functions
|
|
30
|
+
// it defined, so `analytics.capture(...)` is a call of the `capture` declared next to the return
|
|
31
|
+
const target = typescript_1.default.isShorthandPropertyAssignment(declaration)
|
|
32
|
+
? checker.getShorthandAssignmentValueSymbol(declaration)
|
|
33
|
+
: typescript_1.default.isPropertyAssignment(declaration) && typescript_1.default.isIdentifier(declaration.initializer)
|
|
34
|
+
? checker.getSymbolAtLocation(declaration.initializer)
|
|
35
|
+
: undefined;
|
|
36
|
+
// the target is a variable, never another property, so this does not recurse into a cycle
|
|
37
|
+
for (const next of target?.getDeclarations() ?? []) {
|
|
38
|
+
const resolved = resolveDeclaration(next, checker);
|
|
39
|
+
if (resolved) {
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
29
43
|
return null;
|
|
30
44
|
}
|
|
31
45
|
function getExpressionSymbol(expression, checker) {
|
|
@@ -69,7 +83,7 @@ function resolveFunctionFromCall(expression, checker, callCache, exportCache) {
|
|
|
69
83
|
// declaration resolution
|
|
70
84
|
const declarations = symbol.getDeclarations() ?? [];
|
|
71
85
|
for (const declaration of declarations) {
|
|
72
|
-
const resolved = resolveDeclaration(declaration);
|
|
86
|
+
const resolved = resolveDeclaration(declaration, checker);
|
|
73
87
|
if (!resolved) {
|
|
74
88
|
continue;
|
|
75
89
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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.resolveWrapperChains = resolveWrapperChains;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const callResolver_1 = require("../resolver/callResolver");
|
|
9
|
+
const propagationAnalyzer_1 = require("./analyzer/propagationAnalyzer");
|
|
10
|
+
/**
|
|
11
|
+
* Each round can only discover wrappers one call deeper than the previous one, so this is also the
|
|
12
|
+
* longest wrapper chain that is followed (`track` -> `capture` -> `posthog.capture` is two rounds).
|
|
13
|
+
* Real chains are two or three deep; the bound mostly stops a cycle from spinning.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_ROUNDS = 8;
|
|
16
|
+
/** Every call inside `fn`, nested functions included (the same reach the direct sinks have). */
|
|
17
|
+
function callsWithin(fn) {
|
|
18
|
+
const calls = [];
|
|
19
|
+
const visit = (node) => {
|
|
20
|
+
if (typescript_1.default.isCallExpression(node))
|
|
21
|
+
calls.push(node);
|
|
22
|
+
typescript_1.default.forEachChild(node, visit);
|
|
23
|
+
};
|
|
24
|
+
/* v8 ignore else -- only functions with a body are collected */
|
|
25
|
+
if (fn.body)
|
|
26
|
+
visit(fn.body);
|
|
27
|
+
return calls;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A call of an already known wrapper is, for the function containing it, the same thing a call of a
|
|
31
|
+
* real sink is: the arguments the wrapper forwards to its sink are the ones to follow.
|
|
32
|
+
*
|
|
33
|
+
* Only a plain parameter (`event`) or an object literal read by one property (`{ event }`) can be
|
|
34
|
+
* followed: a wrapper that reads `options.event` from a whole object has no argument in the caller
|
|
35
|
+
* to point at, so it is not chained rather than guessed.
|
|
36
|
+
*/
|
|
37
|
+
function wrapperCallAsSink(call, self, checker, registry, callCache, exportCache) {
|
|
38
|
+
const callee = (0, callResolver_1.resolveFunctionFromCall)(call.expression, checker, callCache, exportCache);
|
|
39
|
+
if (!callee || callee === self)
|
|
40
|
+
return null;
|
|
41
|
+
const wrapper = registry.get(callee);
|
|
42
|
+
if (!wrapper)
|
|
43
|
+
return null;
|
|
44
|
+
const trackedArguments = wrapper.propagations
|
|
45
|
+
.filter((p) => {
|
|
46
|
+
const argument = call.arguments[p.sourceParameterIndex];
|
|
47
|
+
if (!argument)
|
|
48
|
+
return false;
|
|
49
|
+
return p.propertyPath.length === 0 || typescript_1.default.isObjectLiteralExpression(argument);
|
|
50
|
+
})
|
|
51
|
+
.map((p) => ({ index: p.sourceParameterIndex, propertyPath: p.propertyPath }));
|
|
52
|
+
return trackedArguments.length > 0 ? { call, trackedArguments } : null;
|
|
53
|
+
}
|
|
54
|
+
const sameNode = (a, b) => a.targetNode === b.targetNode &&
|
|
55
|
+
a.sourceParameterIndex === b.sourceParameterIndex &&
|
|
56
|
+
a.propertyPath.join("\0") === b.propertyPath.join("\0");
|
|
57
|
+
/**
|
|
58
|
+
* Finds the wrappers that only reach a sink through another wrapper (`trackEvent(event)` calls
|
|
59
|
+
* `capture(event)`, which calls `posthog.capture(event)`), which scanning one file at a time cannot:
|
|
60
|
+
* the inner wrapper may live in a file that has not been indexed yet. Repeats over every file until
|
|
61
|
+
* a round finds nothing new, registers what it finds, and returns the files whose wrapper list grew.
|
|
62
|
+
*/
|
|
63
|
+
function resolveWrapperChains(indices, checker, registry, callCache, exportCache) {
|
|
64
|
+
const found = new Map();
|
|
65
|
+
const files = [...indices].filter((index) => (index.functions?.length ?? 0) > 0);
|
|
66
|
+
// the calls of a function never change between rounds, so they are collected once; a function with
|
|
67
|
+
// none (most of them) costs nothing in the rounds that follow
|
|
68
|
+
const callsOf = new Map();
|
|
69
|
+
for (const index of files) {
|
|
70
|
+
for (const fn of index.functions) {
|
|
71
|
+
if (fn.body) {
|
|
72
|
+
const calls = callsWithin(fn);
|
|
73
|
+
if (calls.length > 0)
|
|
74
|
+
callsOf.set(fn, calls);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
79
|
+
let changed = false;
|
|
80
|
+
for (const index of files) {
|
|
81
|
+
for (const fn of index.functions) {
|
|
82
|
+
const calls = callsOf.get(fn);
|
|
83
|
+
if (!calls)
|
|
84
|
+
continue;
|
|
85
|
+
const sinks = [];
|
|
86
|
+
for (const call of calls) {
|
|
87
|
+
const sink = wrapperCallAsSink(call, fn, checker, registry, callCache, exportCache);
|
|
88
|
+
if (sink)
|
|
89
|
+
sinks.push(sink);
|
|
90
|
+
}
|
|
91
|
+
if (sinks.length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
const chained = (0, propagationAnalyzer_1.analyzeWrapperPropagation)(fn, checker, sinks);
|
|
94
|
+
if (!chained)
|
|
95
|
+
continue;
|
|
96
|
+
const known = registry.get(fn);
|
|
97
|
+
const added = chained.propagations.filter((p) => !known?.propagations.some((k) => sameNode(k, p)));
|
|
98
|
+
if (added.length === 0)
|
|
99
|
+
continue;
|
|
100
|
+
const merged = {
|
|
101
|
+
symbol: chained.symbol,
|
|
102
|
+
declaration: fn,
|
|
103
|
+
propagations: [...(known?.propagations ?? []), ...added],
|
|
104
|
+
};
|
|
105
|
+
registry.set(merged);
|
|
106
|
+
const list = found.get(index.fileName) ?? [];
|
|
107
|
+
found.set(index.fileName, [...list.filter((w) => w.declaration !== fn), merged]);
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!changed)
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
@@ -4,7 +4,7 @@ exports.EVENTRA_SDK_SHIM = exports.MAX_EVENT_NAME_LENGTH = exports.EVENTRA_SDK_P
|
|
|
4
4
|
exports.isEventraSdkModuleSpecifier = isEventraSdkModuleSpecifier;
|
|
5
5
|
exports.normalizeEventName = normalizeEventName;
|
|
6
6
|
exports.isValidEventName = isValidEventName;
|
|
7
|
-
/** Mirrors @eventra_dev/eventra-sdk
|
|
7
|
+
/** Mirrors the length limit of @eventra_dev/eventra-sdk (see packages/sdk/src/client.ts). */
|
|
8
8
|
exports.EVENTRA_SDK_PACKAGE = "@eventra_dev/eventra-sdk";
|
|
9
9
|
exports.MAX_EVENT_NAME_LENGTH = 64;
|
|
10
10
|
function isEventraSdkModuleSpecifier(specifier) {
|
|
@@ -13,10 +13,12 @@ function isEventraSdkModuleSpecifier(specifier) {
|
|
|
13
13
|
normalized.startsWith(`${exports.EVENTRA_SDK_PACKAGE}/`));
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Letters, digits and
|
|
17
|
-
* `App: User Sign Up` survive.
|
|
16
|
+
* Letters, digits and `$ : _ . / -`, plus the plain space - so third-party names such as
|
|
17
|
+
* `App: User Sign Up` and PostHog's own `$pageview` survive. The SDK and the API accept any text up
|
|
18
|
+
* to the length limit; the CLI stays stricter so that a name read out of code is a clean identifier:
|
|
19
|
+
* tabs, newlines and every other character are rejected.
|
|
18
20
|
*/
|
|
19
|
-
const EVENT_NAME_PATTERN = /^[a-zA-Z0-9
|
|
21
|
+
const EVENT_NAME_PATTERN = /^[a-zA-Z0-9$:_./ -]+$/;
|
|
20
22
|
function normalizeEventName(value) {
|
|
21
23
|
const trimmed = value.trim();
|
|
22
24
|
if (!trimmed || trimmed.length > exports.MAX_EVENT_NAME_LENGTH) {
|
|
@@ -7,18 +7,27 @@ function isSymbol(value) {
|
|
|
7
7
|
return typeof value.getDeclarations === "function";
|
|
8
8
|
}
|
|
9
9
|
class WrapperRegistry {
|
|
10
|
-
|
|
10
|
+
pendingChecker;
|
|
11
11
|
exportCache;
|
|
12
12
|
wrappers = new WeakMap();
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
resolvedChecker;
|
|
14
|
+
/** `checker` may be a provider, called once on first use (setChecker replaces it). */
|
|
15
|
+
constructor(pendingChecker, exportCache) {
|
|
16
|
+
this.pendingChecker = pendingChecker;
|
|
15
17
|
this.exportCache = exportCache;
|
|
16
18
|
}
|
|
17
19
|
setChecker(checker) {
|
|
18
|
-
this.
|
|
20
|
+
this.resolvedChecker = checker;
|
|
19
21
|
}
|
|
20
22
|
getChecker() {
|
|
21
|
-
|
|
23
|
+
if (!this.resolvedChecker) {
|
|
24
|
+
this.resolvedChecker =
|
|
25
|
+
typeof this.pendingChecker === "function" ? this.pendingChecker() : this.pendingChecker;
|
|
26
|
+
}
|
|
27
|
+
return this.resolvedChecker;
|
|
28
|
+
}
|
|
29
|
+
get checker() {
|
|
30
|
+
return this.getChecker();
|
|
22
31
|
}
|
|
23
32
|
normalize(symbol) {
|
|
24
33
|
return ((0, exportResolver_1.resolveExportedSymbol)(symbol, this.checker, this.exportCache) ?? symbol);
|
package/dist/cli/check.js
CHANGED
|
@@ -8,30 +8,41 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
8
8
|
const config_1 = require("../config/config");
|
|
9
9
|
const projectScanner_1 = require("../core/projectScanner");
|
|
10
10
|
const scanResults_1 = require("../core/scanResults");
|
|
11
|
+
const skipped_1 = require("../core/skipped");
|
|
11
12
|
function diffSets(found, known) {
|
|
12
13
|
return {
|
|
13
14
|
added: [...found].filter((e) => !known.has(e)),
|
|
14
15
|
removed: [...known].filter((e) => !found.has(e)),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
|
-
async function check({ fix = false }) {
|
|
18
|
-
const config = await (0, config_1.
|
|
19
|
-
if (!config)
|
|
20
|
-
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
18
|
+
async function check({ fix = false, strict = false } = {}) {
|
|
19
|
+
const config = await (0, config_1.requireConfig)();
|
|
20
|
+
if (!config)
|
|
21
|
+
return;
|
|
23
22
|
console.log(chalk_1.default.blue("Checking events and function wrappers..."));
|
|
24
|
-
const { engine, plugins } = await (0, projectScanner_1.scanProject)(config);
|
|
23
|
+
const { engine, plugins, skipped, ignored } = await (0, projectScanner_1.scanProject)(config);
|
|
25
24
|
await plugins.runDynamicEventReporters(engine.getAllDynamicOccurrences());
|
|
25
|
+
const partial = skipped.length > 0;
|
|
26
26
|
const foundEvents = new Set(engine.getAllEvents());
|
|
27
27
|
const knownEvents = new Set(config.events ?? []);
|
|
28
28
|
const eventDiff = diffSets(foundEvents, knownEvents);
|
|
29
29
|
const foundWrappers = new Set(engine.getAllFunctionWrappers());
|
|
30
30
|
const knownWrappers = new Set(config.functionWrappers?.map((w) => w.name) ?? []);
|
|
31
31
|
const wrapperDiff = diffSets(foundWrappers, knownWrappers);
|
|
32
|
+
// With skipped files the scan is incomplete: a known event that was not found
|
|
33
|
+
// may simply live in a skipped file, so it is not reported as removed.
|
|
34
|
+
const unverified = partial ? eventDiff.removed.length + wrapperDiff.removed.length : 0;
|
|
35
|
+
if (partial) {
|
|
36
|
+
eventDiff.removed = [];
|
|
37
|
+
wrapperDiff.removed = [];
|
|
38
|
+
}
|
|
32
39
|
if (fix) {
|
|
33
|
-
await (0, config_1.saveConfig)((0, scanResults_1.buildConfigFromScan)(config, engine));
|
|
40
|
+
await (0, config_1.saveConfig)((0, scanResults_1.buildConfigFromScan)(config, engine, { preserveKnown: partial }));
|
|
34
41
|
console.log(chalk_1.default.green("Synced"));
|
|
42
|
+
(0, skipped_1.reportSkipped)(skipped);
|
|
43
|
+
(0, skipped_1.reportIgnored)(ignored);
|
|
44
|
+
if (strict && partial)
|
|
45
|
+
process.exitCode = 1;
|
|
35
46
|
return;
|
|
36
47
|
}
|
|
37
48
|
let hasDiff = false;
|
|
@@ -47,7 +58,12 @@ async function check({ fix = false }) {
|
|
|
47
58
|
wrapperDiff.added.forEach((w) => console.log(chalk_1.default.red(`+ ${w}`)));
|
|
48
59
|
wrapperDiff.removed.forEach((w) => console.log(chalk_1.default.yellow(`- ${w}`)));
|
|
49
60
|
}
|
|
50
|
-
|
|
61
|
+
(0, skipped_1.reportSkipped)(skipped);
|
|
62
|
+
(0, skipped_1.reportIgnored)(ignored);
|
|
63
|
+
if (unverified > 0) {
|
|
64
|
+
console.error(chalk_1.default.gray(`${(0, skipped_1.plural)(unverified, "known entry", "known entries")} could not be verified because files were skipped.`));
|
|
65
|
+
}
|
|
66
|
+
if (hasDiff || (strict && partial)) {
|
|
51
67
|
process.exit(1);
|
|
52
68
|
}
|
|
53
69
|
console.log(chalk_1.default.green("All good"));
|
package/dist/cli/init.js
CHANGED
|
@@ -3,9 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isInstalled = isInstalled;
|
|
6
7
|
exports.init = init;
|
|
7
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
10
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const module_1 = require("module");
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
9
13
|
const config_1 = require("../config/config");
|
|
10
14
|
const sinkPresets_1 = require("../plugin/builtins/sinkPresets");
|
|
11
15
|
const projectDependencies_1 = require("./projectDependencies");
|
|
@@ -13,11 +17,13 @@ const projectDependencies_1 = require("./projectDependencies");
|
|
|
13
17
|
* Looks for analytics libraries in package.json (and workspace members) and, if there are any,
|
|
14
18
|
* asks which of them Eventra should read events from. Everything detected is pre-selected.
|
|
15
19
|
*/
|
|
16
|
-
async function chooseSinks() {
|
|
17
|
-
const detected = (0, sinkPresets_1.detectSinkPresets)(
|
|
20
|
+
async function chooseSinks(yes, dependencyNames) {
|
|
21
|
+
const detected = (0, sinkPresets_1.detectSinkPresets)(dependencyNames);
|
|
18
22
|
if (detected.length === 0)
|
|
19
23
|
return [];
|
|
20
24
|
console.log(chalk_1.default.gray(`Found analytics in package.json: ${detected.map((d) => d.packages.join(", ")).join("; ")}`));
|
|
25
|
+
if (yes)
|
|
26
|
+
return detected.map((d) => ({ preset: d.preset }));
|
|
21
27
|
const { presets } = await inquirer_1.default.prompt([
|
|
22
28
|
{
|
|
23
29
|
type: "checkbox",
|
|
@@ -32,22 +38,77 @@ async function chooseSinks() {
|
|
|
32
38
|
]);
|
|
33
39
|
return presets.map((preset) => ({ preset }));
|
|
34
40
|
}
|
|
35
|
-
|
|
41
|
+
/** Framework dependency -> the plugin that reads its template files (`.vue`, `.svelte`, `.astro`, Angular templates). */
|
|
42
|
+
const FRAMEWORK_PLUGINS = {
|
|
43
|
+
vue: "@eventra_dev/cli-plugin-vue",
|
|
44
|
+
svelte: "@eventra_dev/cli-plugin-svelte",
|
|
45
|
+
astro: "@eventra_dev/cli-plugin-astro",
|
|
46
|
+
"@angular/core": "@eventra_dev/cli-plugin-angular",
|
|
47
|
+
};
|
|
48
|
+
/** True when the plugin package can be loaded from the project, which is where `sync` will look for it. */
|
|
49
|
+
function isInstalled(plugin) {
|
|
50
|
+
try {
|
|
51
|
+
(0, module_1.createRequire)(path_1.default.join(process.cwd(), "package.json")).resolve(plugin);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The plugins for the frameworks found in package.json (and workspace members), split by whether they are
|
|
60
|
+
* installed. Without a plugin, events fired from component files are never seen. Only an installed one is
|
|
61
|
+
* written to `plugins`: `sync` stops on a plugin it cannot load, and an `init` followed by `sync` must not.
|
|
62
|
+
*/
|
|
63
|
+
function detectPlugins(dependencyNames, installed) {
|
|
64
|
+
const found = Object.entries(FRAMEWORK_PLUGINS)
|
|
65
|
+
.filter(([dependency]) => dependencyNames.has(dependency))
|
|
66
|
+
.map(([, plugin]) => plugin);
|
|
67
|
+
return { enabled: found.filter(installed), missing: found.filter((plugin) => !installed(plugin)) };
|
|
68
|
+
}
|
|
69
|
+
const DEFAULT_ENDPOINT = "https://api.eventra.dev/api/v1/cli/events";
|
|
70
|
+
/**
|
|
71
|
+
* `yes` is the non-interactive mode (CI, scripts): defaults for every answer,
|
|
72
|
+
* and an existing eventra.json is never overwritten.
|
|
73
|
+
*/
|
|
74
|
+
async function init({ yes = false, isPluginInstalled = isInstalled } = {}) {
|
|
75
|
+
if (await fs_extra_1.default.pathExists(path_1.default.join(process.cwd(), config_1.CONFIG_NAME))) {
|
|
76
|
+
if (yes) {
|
|
77
|
+
console.log(chalk_1.default.yellow(`${config_1.CONFIG_NAME} already exists, keeping it`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: "confirm",
|
|
83
|
+
name: "overwrite",
|
|
84
|
+
message: `${config_1.CONFIG_NAME} already exists. Overwrite it? Its events, wrappers, plugins and sinks will be lost.`,
|
|
85
|
+
default: false,
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
if (!overwrite) {
|
|
89
|
+
console.log(chalk_1.default.gray(`Kept the existing ${config_1.CONFIG_NAME}`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
36
93
|
console.log(chalk_1.default.blue("Initializing Eventra..."));
|
|
37
|
-
const { apiKey, endpoint, } =
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
94
|
+
const { apiKey, endpoint, } = yes
|
|
95
|
+
? { apiKey: "", endpoint: DEFAULT_ENDPOINT }
|
|
96
|
+
: await inquirer_1.default.prompt([
|
|
97
|
+
{
|
|
98
|
+
type: "input",
|
|
99
|
+
name: "apiKey",
|
|
100
|
+
message: "API Key (optional):",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: "input",
|
|
104
|
+
name: "endpoint",
|
|
105
|
+
message: "Custom endpoint (optional):",
|
|
106
|
+
default: DEFAULT_ENDPOINT,
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
const dependencyNames = await (0, projectDependencies_1.collectDependencyNames)(process.cwd());
|
|
110
|
+
const sinks = await chooseSinks(yes, dependencyNames);
|
|
111
|
+
const { enabled: plugins, missing: missingPlugins } = detectPlugins(dependencyNames, isPluginInstalled);
|
|
51
112
|
console.log("");
|
|
52
113
|
console.log(chalk_1.default.gray("Eventra detects @eventra_dev/eventra-sdk usage:"));
|
|
53
114
|
console.log(chalk_1.default.gray('• tracker.track("event.name")'));
|
|
@@ -59,21 +120,24 @@ async function init() {
|
|
|
59
120
|
endpoint,
|
|
60
121
|
events: [],
|
|
61
122
|
functionWrappers: [],
|
|
123
|
+
...(plugins.length > 0 ? { plugins } : {}),
|
|
62
124
|
...(sinks.length > 0 ? { sinks } : {}),
|
|
63
125
|
sync: {
|
|
64
126
|
include: [
|
|
65
127
|
"**/*.{ts,tsx,js,jsx}",
|
|
66
128
|
],
|
|
67
|
-
exclude: [
|
|
68
|
-
"node_modules",
|
|
69
|
-
"dist",
|
|
70
|
-
".next",
|
|
71
|
-
".git",
|
|
72
|
-
],
|
|
129
|
+
exclude: [...config_1.INIT_EXCLUDE],
|
|
73
130
|
},
|
|
74
131
|
};
|
|
75
132
|
await (0, config_1.saveConfig)(config);
|
|
76
133
|
console.log(chalk_1.default.green("eventra.json created"));
|
|
134
|
+
if (plugins.length > 0) {
|
|
135
|
+
console.log(chalk_1.default.green(`plugins: ${plugins.join(", ")}`));
|
|
136
|
+
}
|
|
137
|
+
if (missingPlugins.length > 0) {
|
|
138
|
+
console.log(chalk_1.default.yellow(`Component files are not read yet: npm install -D ${missingPlugins.join(" ")}`));
|
|
139
|
+
console.log(chalk_1.default.yellow(`then add ${missingPlugins.map((p) => `"${p}"`).join(", ")} to "plugins" in ${config_1.CONFIG_NAME}`));
|
|
140
|
+
}
|
|
77
141
|
if (sinks.length > 0) {
|
|
78
142
|
console.log(chalk_1.default.green(`sinks: ${sinks.map((s) => s.preset).join(", ")}`));
|
|
79
143
|
}
|