@eventra_dev/eventra-cli 2.0.10 → 2.2.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/LICENSE +14 -2
- package/README.md +95 -12
- package/dist/analysis/extractor/extractor.js +1 -1
- package/dist/analysis/extractor/propagationExtractor.js +56 -8
- package/dist/analysis/resolver/resolver.js +7 -4
- package/dist/analysis/scanner/analyzer/propagationAnalyzer.js +5 -0
- package/dist/analysis/scanner/scanner.js +10 -3
- package/dist/analysis/sdk/eventraSdk.js +6 -1
- package/dist/analysis/symbols/symbolUtils.js +12 -0
- package/dist/analysis/symbols/wrapperRegistry.js +14 -5
- package/dist/cli/check.js +25 -9
- package/dist/cli/init.js +71 -12
- package/dist/cli/projectDependencies.js +119 -0
- 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 +103 -5
- package/dist/compiler/documentRegistry.js +15 -1
- package/dist/compiler/importGraph.js +8 -0
- package/dist/config/config.js +52 -2
- package/dist/core/EventraEngine.js +129 -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 +8 -5
- package/dist/plugin/builtins/configuredSinks.js +253 -0
- package/dist/plugin/builtins/consoleDynamicEventReporter.js +18 -2
- package/dist/plugin/builtins/sinkPresets.js +99 -0
- package/dist/plugin/loadPlugins.js +22 -2
- package/dist/plugin/registry.js +50 -1
- package/package.json +11 -2
package/LICENSE
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 and-1991
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
7
7
|
in the Software without restriction, including without limitation the rights
|
|
8
8
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
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-
|
|
12
|
-
<img alt="e2e fixtures" src="https://img.shields.io/badge/e2e-
|
|
11
|
+
<img alt="unit tests" src="https://img.shields.io/badge/unit-783%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
|
|
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">
|
|
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>
|
|
@@ -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`, `.svelte`, `.astro`, Angular's `.html` templates) require an optional [plugin](#plugins).
|
|
27
|
+
It does **not** detect generic `track()`, Segment, Google Analytics, or other libraries out of the box - but you can opt in to a third-party library with [`sinks`](#third-party-analytics-sinks) (PostHog, Segment, Amplitude and Google Analytics 4 have built-in presets). Framework files (e.g. `.vue`, `.svelte`, `.astro`, Angular's `.html` templates) require an optional [plugin](#plugins).
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -121,12 +121,51 @@ trackFeature("purchase");
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
+
## Third-party analytics (`sinks`)
|
|
125
|
+
|
|
126
|
+
Already have analytics in place? Declare it in `eventra.json` and the CLI treats its calls as event sources too, with no SDK swap. Wrappers, constants, cross-file imports and JSX component props resolve exactly as they do for the Eventra SDK.
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"sinks": [
|
|
131
|
+
{ "preset": "posthog" },
|
|
132
|
+
{ "preset": "segment" },
|
|
133
|
+
{ "preset": "amplitude" },
|
|
134
|
+
{ "preset": "ga4" },
|
|
135
|
+
{ "callee": "analytics.track" },
|
|
136
|
+
{ "callee": "gtag", "nameArg": 1, "when": { "arg": 0, "equals": "event" } }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Field | Meaning |
|
|
142
|
+
|-------|---------|
|
|
143
|
+
| `preset` | A built-in rule set: `posthog`, `segment`, `amplitude` or `ga4` (see below) |
|
|
144
|
+
| `callee` | Dotted call path, matched as a suffix: `analytics.track` also matches `this.analytics.track`, but not `myanalytics.track` |
|
|
145
|
+
| `nameArg` | Zero-based index of the event-name argument (default `0`) |
|
|
146
|
+
| `nameProperty` | If that argument is an object literal, read the name from this property (e.g. `event`) |
|
|
147
|
+
| `modules` | Only match when the receiver comes from one of these npm packages (imported, re-exported, typed, or built from them) |
|
|
148
|
+
| `when` | Only match when the argument at `arg` is the string literal `equals`, e.g. `{ "arg": 0, "equals": "event" }` for `gtag("event", name)` |
|
|
149
|
+
|
|
150
|
+
| Preset | Detects |
|
|
151
|
+
|--------|---------|
|
|
152
|
+
| `posthog` | `posthog.capture("x")`, the `usePostHog()` hook (from `posthog-js/react` or `@posthog/react`) under any variable name (also through re-exports like `export { usePostHog as useAnalytics }`), and posthog-node's `client.capture({ distinctId, event: "x" })`, including typed class fields |
|
|
153
|
+
| `segment` | `analytics.track("x", props)` (analytics.js, `window.analytics`), analytics-node's `client.track({ userId, event: "x" })`, and clients from `@segment/analytics-next`, `@segment/analytics-node`, `@segment/analytics-react-native` or `analytics-node` under any variable name, including `const [analytics] = await AnalyticsBrowser.load(...)` |
|
|
154
|
+
| `amplitude` | `amplitude.track("x")`, `amplitude.track({ event_type: "x" })`, `amplitude.logEvent("x")`, `amplitude.getInstance().logEvent("x")`, and clients from the `@amplitude/*` browser, node, react-native and unified packages or `amplitude-js` |
|
|
155
|
+
| `ga4` | `gtag("event", "x", params)` (also `window.gtag` and nuxt-gtag's `const { gtag } = useGtag()`), `sendGAEvent("event", "x", params)` from `@next/third-parties`, nuxt-gtag's `useTrackEvent("x")`, vue-gtag's `this.$gtag.event("x")` and `event("x")` / `const { event } = useGtag()`, and react-ga4's `ReactGA.event("x")` / `ReactGA.event({ category, action: "x" })`. Other gtag commands (`gtag("config", id)`, `gtag("set", ...)`) are not events and are ignored |
|
|
156
|
+
|
|
157
|
+
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
|
+
|
|
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. Names with other characters, such as PostHog's `$`-prefixed system events (`$pageview`) or a name ending in `!`, are excluded from the scan result.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
124
163
|
## Event name rules (aligned with SDK)
|
|
125
164
|
|
|
126
165
|
| Rule | Limit |
|
|
127
166
|
|------|--------|
|
|
128
167
|
| Max length | **64 characters** (same as SDK) |
|
|
129
|
-
| Allowed characters | `a-zA-Z0-9:_./-` |
|
|
168
|
+
| Allowed characters | `a-zA-Z0-9:_./-` and the space (so `App: User Sign Up` works); tabs, newlines and any other character are rejected |
|
|
130
169
|
| Position | First argument of `.track(name, options?)` |
|
|
131
170
|
|
|
132
171
|
The second argument (`userId`, `properties`) is **not** used as the event name.
|
|
@@ -137,8 +176,10 @@ A literal name that violates either rule (too long, or a character outside the a
|
|
|
137
176
|
|
|
138
177
|
## What is ignored
|
|
139
178
|
|
|
179
|
+
By default only the Eventra SDK is read. Calls to other analytics are ignored unless you enable them with [`sinks`](#third-party-analytics-sinks).
|
|
180
|
+
|
|
140
181
|
```ts
|
|
141
|
-
// Not from @eventra_dev/eventra-sdk
|
|
182
|
+
// Not from @eventra_dev/eventra-sdk (and no matching `sinks` rule)
|
|
142
183
|
track("legacy");
|
|
143
184
|
analytics.track("ga");
|
|
144
185
|
segment.track("x");
|
|
@@ -151,6 +192,28 @@ tracker.track({ event: "click" });
|
|
|
151
192
|
|
|
152
193
|
## Commands
|
|
153
194
|
|
|
195
|
+
### `eventra init`
|
|
196
|
+
|
|
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:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
Found analytics in package.json: posthog-js; @segment/analytics-next
|
|
201
|
+
? Also read events from these analytics calls (sinks):
|
|
202
|
+
❯◉ posthog (posthog-js)
|
|
203
|
+
◉ segment (@segment/analytics-next)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Everything found is pre-selected; press Enter to accept or untick what you do not want. The chosen presets are written to `sinks` in `eventra.json`. Recognised packages:
|
|
207
|
+
|
|
208
|
+
| Preset | Packages |
|
|
209
|
+
|--------|----------|
|
|
210
|
+
| `posthog` | `posthog-js`, `@posthog/react`, `posthog-js-lite`, `posthog-node`, `posthog-react-native` |
|
|
211
|
+
| `segment` | `@segment/analytics-next`, `@segment/analytics-node`, `@segment/analytics-react-native`, `@segment/snippet`, and the older `analytics-node`, `@segment/analytics.js-core` |
|
|
212
|
+
| `amplitude` | `@amplitude/analytics-browser`, `@amplitude/analytics-node`, `@amplitude/analytics-react-native`, `@amplitude/unified`, `@amplitude/marketing-analytics-browser`, and the older `amplitude-js` |
|
|
213
|
+
| `ga4` | `react-ga4`, `vue-gtag`, `@next/third-parties`, `nuxt-gtag`, `@types/gtag.js`, and the older `vue-gtag-next` |
|
|
214
|
+
|
|
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. 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
|
+
|
|
154
217
|
### `eventra sync`
|
|
155
218
|
|
|
156
219
|
Full project scan → updates `eventra.json`:
|
|
@@ -158,9 +221,11 @@ Full project scan → updates `eventra.json`:
|
|
|
158
221
|
- `events` — discovered event names
|
|
159
222
|
- `functionWrappers` — detected wrapper functions
|
|
160
223
|
|
|
224
|
+
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.
|
|
225
|
+
|
|
161
226
|
### `eventra check`
|
|
162
227
|
|
|
163
|
-
Compares config with the current scan (events + wrappers). Exit code `1` on drift.
|
|
228
|
+
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.
|
|
164
229
|
|
|
165
230
|
### `eventra check --fix`
|
|
166
231
|
|
|
@@ -192,6 +257,14 @@ This records the endpoint in `eventra.local.json` (gitignored). If `eventra.json
|
|
|
192
257
|
|
|
193
258
|
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.
|
|
194
259
|
|
|
260
|
+
**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`.
|
|
261
|
+
|
|
262
|
+
- `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.
|
|
263
|
+
- `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.
|
|
264
|
+
- `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.
|
|
265
|
+
|
|
266
|
+
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`.
|
|
267
|
+
|
|
195
268
|
Network resilience:
|
|
196
269
|
|
|
197
270
|
- Up to 4 attempts with exponential backoff + jitter (capped at 8 s)
|
|
@@ -338,6 +411,7 @@ No dependency on `@eventra_dev/eventra-cli` is required — the host CLI loads t
|
|
|
338
411
|
"events": [],
|
|
339
412
|
"functionWrappers": [],
|
|
340
413
|
"plugins": [],
|
|
414
|
+
"sinks": [],
|
|
341
415
|
"sync": {
|
|
342
416
|
"include": ["**/*.{ts,tsx,js,jsx}"],
|
|
343
417
|
"exclude": ["node_modules", "dist", ".next", ".git"]
|
|
@@ -350,8 +424,9 @@ No dependency on `@eventra_dev/eventra-cli` is required — the host CLI loads t
|
|
|
350
424
|
| `apiKey` | Legacy inline API key — leave empty. Use `EVENTRA_API_KEY` or `eventra.local.json` instead (see [`eventra send`](#eventra-send)) so a real key never ends up in this committed file |
|
|
351
425
|
| `endpoint` | Custom `eventra send` target (e.g. self-hosted). Non-default values from this file need one-time local approval — see [`eventra send`](#eventra-send) |
|
|
352
426
|
| `plugins` | `@eventra_dev/cli-plugin-*` specifiers to `import()` at startup. Anything outside that scope is skipped (see [Plugin contract](#plugin-contract-for-authors)) |
|
|
427
|
+
| `sinks` | Third-party analytics calls to treat as event sources, e.g. `[{ "preset": "posthog" }]` (see [Third-party analytics](#third-party-analytics-sinks)) |
|
|
353
428
|
| `sync.include` | Base glob patterns; plugin `includeGlobs` are merged automatically |
|
|
354
|
-
| `sync.exclude` | Paths skipped during scan |
|
|
429
|
+
| `sync.exclude` | Paths skipped during scan. 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 |
|
|
355
430
|
|
|
356
431
|
`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`.
|
|
357
432
|
|
|
@@ -375,18 +450,18 @@ No runtime execution. No monkey-patching.
|
|
|
375
450
|
|
|
376
451
|
## Requirements
|
|
377
452
|
|
|
378
|
-
- Node.js 18+
|
|
453
|
+
- Node.js 18+ (`eventra watch` needs Node.js 20.19+, which its file-watching dependency requires; `init`, `sync`, `check` and `send` run on 18)
|
|
379
454
|
- TypeScript/JavaScript source using `@eventra_dev/eventra-sdk`
|
|
380
455
|
|
|
381
456
|
---
|
|
382
457
|
|
|
383
458
|
## Test Coverage
|
|
384
459
|
|
|
385
|
-
Two test layers, **
|
|
460
|
+
Two test layers, **783 unit tests + 14 e2e fixtures + 3 `check` exit-code scenarios + 1 `watch` scenario**.
|
|
386
461
|
|
|
387
462
|
**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.
|
|
388
463
|
|
|
389
|
-
**Unit tests (vitest)**
|
|
464
|
+
**Unit tests (vitest)** - 47 suites covering core modules:
|
|
390
465
|
|
|
391
466
|
| Module | Covers |
|
|
392
467
|
|---|---|
|
|
@@ -411,8 +486,14 @@ Two test layers, **445 unit tests + 12 e2e fixtures + 3 `check` exit-code scenar
|
|
|
411
486
|
| `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) |
|
|
412
487
|
| `send()` | Endpoint trust-on-first-use (TOFU): default-trusted, blocked-until-approved, approve-then-persists, re-block-on-change, env-var-bypasses-approval |
|
|
413
488
|
| `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 |
|
|
489
|
+
| `configured sinks` | `sinks` rules and the PostHog, Segment, Amplitude and GA4 presets: every call shape, receivers traced through imports, re-exports, hooks, typed fields and aliases, the `when` guard, spaced names, wrong-typed config, circular chains |
|
|
490
|
+
| `sink presets` | The package table behind the presets: detection per package, order, legacy and detect-only flags, rules vetted only against their own preset's packages |
|
|
491
|
+
| `project dependencies` | package.json and workspace reading for `eventra init` (`workspaces`, pnpm-workspace.yaml, negations, caps, patterns that leave the project, unreadable files) |
|
|
492
|
+
| `init` | `eventra init` prompts, pre-selected presets, written config, exact console output, API key kept out of `eventra.json` |
|
|
493
|
+
| `preset package check` | The registry verdicts behind `pnpm check:presets` (missing, deprecated, stale, legacy) |
|
|
494
|
+
| `license` | `LICENSE` is the complete MIT text, its holder is the package author, and both match `package.json` |
|
|
414
495
|
|
|
415
|
-
**End-to-end fixtures**
|
|
496
|
+
**End-to-end fixtures** - 14 isolated TS projects scanned via `eventra sync`:
|
|
416
497
|
|
|
417
498
|
| Fixture | Covers |
|
|
418
499
|
|---|---|
|
|
@@ -423,6 +504,8 @@ Two test layers, **445 unit tests + 12 e2e fixtures + 3 `check` exit-code scenar
|
|
|
423
504
|
| `wrappers/barrel` | `export * from "./tracker"` re-exports |
|
|
424
505
|
| `wrappers/default-export` | `export default function trackFeature` propagation |
|
|
425
506
|
| `wrappers/path-aliases` | `tsconfig.json` `paths` mapping (`@app/*`) |
|
|
507
|
+
| `sinks/presets` | PostHog, Segment, Amplitude and GA4 side by side, including `gtag("config")` that must not count |
|
|
508
|
+
| `sinks/spaced-names` | Names with spaces and colons through the SDK, PostHog (client and server shapes), a custom rule, wrappers and JSX, plus calls that must be ignored |
|
|
426
509
|
| `watch-incremental` | Engine state across sequential file updates (matches `sync` output) |
|
|
427
510
|
|
|
428
511
|
**`eventra check` exit-code scenarios:**
|
|
@@ -447,4 +530,4 @@ pnpm --filter @eventra_dev/eventra-cli test:e2e # fixtures + check exit codes
|
|
|
447
530
|
|
|
448
531
|
## License
|
|
449
532
|
|
|
450
|
-
MIT
|
|
533
|
+
MIT, copyright (c) 2026 [and-1991](https://github.com/and-1991). See [LICENSE](LICENSE).
|
|
@@ -30,7 +30,7 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
|
|
|
30
30
|
}
|
|
31
31
|
const propagationVisited = new Set();
|
|
32
32
|
function visit(node) {
|
|
33
|
-
if (typescript_1.default.isCallExpression(node)) {
|
|
33
|
+
if (typescript_1.default.isCallExpression(node) || typescript_1.default.isJsxOpeningElement(node) || typescript_1.default.isJsxSelfClosingElement(node)) {
|
|
34
34
|
(0, propagationExtractor_1.extractPropagationEvents)(node, checker, wrapperRegistry, resolvedCallCache, returnPropagationCache, evaluationCache, exportCache, events, propagationVisited, dynamicOccurrences);
|
|
35
35
|
}
|
|
36
36
|
typescript_1.default.forEachChild(node, visit);
|
|
@@ -8,6 +8,7 @@ const typescript_1 = __importDefault(require("typescript"));
|
|
|
8
8
|
const callResolver_1 = require("../resolver/callResolver");
|
|
9
9
|
const resolver_1 = require("../resolver/resolver");
|
|
10
10
|
const evaluationContext_1 = require("../shared/evaluationContext");
|
|
11
|
+
const symbolUtils_1 = require("../symbols/symbolUtils");
|
|
11
12
|
const eventValidation_1 = require("../shared/eventValidation");
|
|
12
13
|
const dynamicOccurrence_1 = require("../shared/dynamicOccurrence");
|
|
13
14
|
function resolveObjectPath(expression, path) {
|
|
@@ -35,8 +36,39 @@ function resolveObjectPath(expression, path) {
|
|
|
35
36
|
}
|
|
36
37
|
return current;
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* A JSX element is a call of its component with one argument: the props object. Rebuilt here as an
|
|
41
|
+
* object literal from the attributes so the existing call-site machinery (`resolveObjectPath`,
|
|
42
|
+
* parameter bindings) works unchanged. Spread attributes and children are ignored, and each
|
|
43
|
+
* attribute value is the original AST node, so symbol resolution still sees real code.
|
|
44
|
+
*/
|
|
45
|
+
function jsxPropsAsObjectLiteral(element) {
|
|
46
|
+
const properties = [];
|
|
47
|
+
for (const attribute of element.attributes.properties) {
|
|
48
|
+
if (!typescript_1.default.isJsxAttribute(attribute) || !typescript_1.default.isIdentifier(attribute.name) || !attribute.initializer) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const value = typescript_1.default.isJsxExpression(attribute.initializer) ? attribute.initializer.expression : attribute.initializer;
|
|
52
|
+
if (value && !typescript_1.default.isJsxElement(value) && !typescript_1.default.isJsxSelfClosingElement(value) && !typescript_1.default.isJsxFragment(value)) {
|
|
53
|
+
properties.push(typescript_1.default.factory.createPropertyAssignment(attribute.name, value));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return typescript_1.default.factory.createObjectLiteralExpression(properties);
|
|
57
|
+
}
|
|
58
|
+
/** The expression naming the called function/component; null for `<ns:tag>`, which is never a component. */
|
|
59
|
+
function callSiteExpression(site) {
|
|
60
|
+
if (typescript_1.default.isCallExpression(site)) {
|
|
61
|
+
return site.expression;
|
|
62
|
+
}
|
|
63
|
+
return typescript_1.default.isJsxNamespacedName(site.tagName) ? null : site.tagName;
|
|
64
|
+
}
|
|
65
|
+
function callSiteArgument(site, index) {
|
|
66
|
+
// A JSX site only reaches here for index 0 (the props object): see the guard in extractPropagationEvents.
|
|
67
|
+
return typescript_1.default.isCallExpression(site) ? site.arguments[index] : jsxPropsAsObjectLiteral(site);
|
|
68
|
+
}
|
|
38
69
|
function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCache, returnPropagationCache, evaluationCache, exportCache, events, visited, dynamicOccurrences) {
|
|
39
|
-
const
|
|
70
|
+
const callee = callSiteExpression(call);
|
|
71
|
+
const resolved = callee && (0, callResolver_1.resolveFunctionFromCall)(callee, checker, resolvedCallCache, exportCache);
|
|
40
72
|
if (!resolved) {
|
|
41
73
|
return;
|
|
42
74
|
}
|
|
@@ -60,6 +92,11 @@ function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCa
|
|
|
60
92
|
visited.add(signature);
|
|
61
93
|
try {
|
|
62
94
|
for (const propagation of semantic.propagations) {
|
|
95
|
+
// A component only ever receives props (an object), so a propagation that reads the
|
|
96
|
+
// parameter as a whole (`name` rather than `props.name`) can't be satisfied by a JSX site.
|
|
97
|
+
if (!typescript_1.default.isCallExpression(call) && (propagation.sourceParameterIndex !== 0 || propagation.propertyPath.length === 0)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
63
100
|
extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events, dynamicOccurrences);
|
|
64
101
|
}
|
|
65
102
|
}
|
|
@@ -71,11 +108,12 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
|
|
|
71
108
|
// Call site can omit the argument and rely on the parameter's own default
|
|
72
109
|
// value (`function f(event = "x") {}` called as `f()`) - that default is a
|
|
73
110
|
// real static literal, so fall back to it instead of dropping the call site.
|
|
74
|
-
const argument = call
|
|
111
|
+
const argument = callSiteArgument(call, propagation.sourceParameterIndex) ?? propagation.sourceParameter.initializer;
|
|
75
112
|
if (!argument) {
|
|
76
113
|
return;
|
|
77
114
|
}
|
|
78
115
|
const context = (0, evaluationContext_1.createEvaluationContext)();
|
|
116
|
+
let directTarget;
|
|
79
117
|
if (typescript_1.default.isIdentifier(propagation.sourceParameter.name)) {
|
|
80
118
|
// sourceParameter is a plain identifier (`function f(payload)`), so
|
|
81
119
|
// propagation.targetNode — `payload`, `payload.event`, or any expression
|
|
@@ -98,20 +136,30 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
|
|
|
98
136
|
// returns nothing. Narrow to the leaf value ourselves via propertyPath,
|
|
99
137
|
// then bind it to the destructured binding's own symbol (targetNode is
|
|
100
138
|
// that binding's reference, e.g. `event` in `sdk.track(event)`).
|
|
101
|
-
if (propagation.propertyPath.length === 0
|
|
139
|
+
if (propagation.propertyPath.length === 0) {
|
|
102
140
|
return;
|
|
103
141
|
}
|
|
104
142
|
const resolvedPath = resolveObjectPath(argument, propagation.propertyPath);
|
|
105
143
|
if (!resolvedPath) {
|
|
106
144
|
return;
|
|
107
145
|
}
|
|
108
|
-
|
|
109
|
-
|
|
146
|
+
if (typescript_1.default.isIdentifier(propagation.targetNode)) {
|
|
147
|
+
const bindingSymbol = (0, symbolUtils_1.getValueSymbol)(propagation.targetNode, checker);
|
|
148
|
+
if (!bindingSymbol) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
context.parameterBindings.set(bindingSymbol, resolvedPath);
|
|
152
|
+
}
|
|
153
|
+
else if (typescript_1.default.isPropertyAccessExpression(propagation.targetNode) || typescript_1.default.isElementAccessExpression(propagation.targetNode)) {
|
|
154
|
+
// `function f({ analytics }) { track(analytics.name) }`: propertyPath already walks the binding
|
|
155
|
+
// AND the trailing property access (["analytics", "name"]), so resolvedPath is the leaf value.
|
|
156
|
+
directTarget = resolvedPath;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
110
159
|
return;
|
|
111
160
|
}
|
|
112
|
-
context.parameterBindings.set(bindingSymbol, resolvedPath);
|
|
113
161
|
}
|
|
114
|
-
const resolved = (0, resolver_1.resolveNodeValue)(propagation.targetNode, checker, context, new Set(), evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
162
|
+
const resolved = (0, resolver_1.resolveNodeValue)(directTarget ?? propagation.targetNode, checker, context, new Set(), evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
115
163
|
for (const value of resolved.values) {
|
|
116
164
|
const normalized = (0, eventValidation_1.normalizeEventName)(value);
|
|
117
165
|
if (normalized) {
|
|
@@ -119,6 +167,6 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
|
|
|
119
167
|
}
|
|
120
168
|
}
|
|
121
169
|
if (resolved.dynamic) {
|
|
122
|
-
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call, call.
|
|
170
|
+
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call, callSiteExpression(call).getText(), resolved.values, dynamicOccurrences);
|
|
123
171
|
}
|
|
124
172
|
}
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.resolveNodeValue = resolveNodeValue;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const symbolUtils_1 = require("../symbols/symbolUtils");
|
|
8
9
|
const exportResolver_1 = require("./exportResolver");
|
|
9
10
|
const returnPropagationAnalyzer_1 = require("../scanner/analyzer/returnPropagationAnalyzer");
|
|
10
11
|
const callResolver_1 = require("./callResolver");
|
|
@@ -37,7 +38,7 @@ function concat(left, right) {
|
|
|
37
38
|
function resolveBoundObjectNode(rawNode, checker, context) {
|
|
38
39
|
const node = (0, utils_1.unwrapExpression)(rawNode);
|
|
39
40
|
if (typescript_1.default.isIdentifier(node)) {
|
|
40
|
-
const symbol =
|
|
41
|
+
const symbol = (0, symbolUtils_1.getValueSymbol)(node, checker);
|
|
41
42
|
if (!symbol || !context.parameterBindings.has(symbol)) {
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
@@ -220,10 +221,12 @@ function resolvePropertyAccess(node, checker, context, visited, evaluationCache,
|
|
|
220
221
|
/* v8 ignore next */
|
|
221
222
|
const declarations = symbol.getDeclarations() ?? [];
|
|
222
223
|
for (const declaration of declarations) {
|
|
223
|
-
|
|
224
|
+
// `{ ... } as const` / `satisfies Record<...>` is still the same object literal.
|
|
225
|
+
const objectLiteral = typescript_1.default.isVariableDeclaration(declaration) && declaration.initializer ? (0, utils_1.unwrapExpression)(declaration.initializer) : null;
|
|
226
|
+
if (!objectLiteral || !typescript_1.default.isObjectLiteralExpression(objectLiteral)) {
|
|
224
227
|
continue;
|
|
225
228
|
}
|
|
226
|
-
for (const property of
|
|
229
|
+
for (const property of objectLiteral.properties) {
|
|
227
230
|
if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
|
|
228
231
|
continue;
|
|
229
232
|
}
|
|
@@ -297,7 +300,7 @@ function collectReassignments(symbol, checker) {
|
|
|
297
300
|
return { values, hasUnresolvedReassignment };
|
|
298
301
|
}
|
|
299
302
|
function resolveIdentifier(node, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache) {
|
|
300
|
-
const boundSymbol =
|
|
303
|
+
const boundSymbol = (0, symbolUtils_1.getValueSymbol)(node, checker);
|
|
301
304
|
if (boundSymbol && context.parameterBindings.has(boundSymbol)) {
|
|
302
305
|
const bound = context.parameterBindings.get(boundSymbol);
|
|
303
306
|
// `has(boundSymbol)` above already guarantees a Map entry, and every
|
|
@@ -138,6 +138,11 @@ function collectPropagations(fn, sinks) {
|
|
|
138
138
|
if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
|
+
// The sink only reads one property (e.g. `event`) as the name; `{ distinctId, event }`
|
|
142
|
+
// must not propagate `distinctId` as an event name too.
|
|
143
|
+
if (tracked.propertyPath.length > 0 && property.name.getText() !== tracked.propertyPath[0]) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
141
146
|
const initializer = typescript_1.default.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
|
|
142
147
|
if (!typescript_1.default.isIdentifier(initializer)) {
|
|
143
148
|
continue;
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.scanSource = scanSource;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const utils_1 = require("../shared/utils");
|
|
8
9
|
const sinkDetector_1 = require("./sinkDetector");
|
|
9
10
|
const jsxEventAttribute_1 = require("./jsxEventAttribute");
|
|
10
11
|
const propagationAnalyzer_1 = require("./analyzer/propagationAnalyzer");
|
|
@@ -35,13 +36,19 @@ function scanSource(source, checker, wrapperRegistry, plugins) {
|
|
|
35
36
|
// track({
|
|
36
37
|
// event: name
|
|
37
38
|
// })
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const argumentLiteral = (0, utils_1.unwrapExpression)(argument);
|
|
40
|
+
if (tracked.propertyPath.length > 0 && typescript_1.default.isObjectLiteralExpression(argumentLiteral)) {
|
|
41
|
+
const property = argumentLiteral.properties.find(prop => {
|
|
42
|
+
return ((typescript_1.default.isPropertyAssignment(prop) || typescript_1.default.isShorthandPropertyAssignment(prop)) &&
|
|
43
|
+
prop.name.getText() === tracked.propertyPath[0]);
|
|
41
44
|
});
|
|
42
45
|
if (property && typescript_1.default.isPropertyAssignment(property)) {
|
|
43
46
|
return property.initializer;
|
|
44
47
|
}
|
|
48
|
+
// track({ event })
|
|
49
|
+
if (property && typescript_1.default.isShorthandPropertyAssignment(property)) {
|
|
50
|
+
return property.name;
|
|
51
|
+
}
|
|
45
52
|
}
|
|
46
53
|
// track(name)
|
|
47
54
|
return argument;
|
|
@@ -12,12 +12,17 @@ function isEventraSdkModuleSpecifier(specifier) {
|
|
|
12
12
|
return (normalized === exports.EVENTRA_SDK_PACKAGE ||
|
|
13
13
|
normalized.startsWith(`${exports.EVENTRA_SDK_PACKAGE}/`));
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Letters, digits and `: _ . / -`, plus the plain space - so third-party names such as
|
|
17
|
+
* `App: User Sign Up` survive. Tabs, newlines and every other character stay rejected.
|
|
18
|
+
*/
|
|
19
|
+
const EVENT_NAME_PATTERN = /^[a-zA-Z0-9:_./ -]+$/;
|
|
15
20
|
function normalizeEventName(value) {
|
|
16
21
|
const trimmed = value.trim();
|
|
17
22
|
if (!trimmed || trimmed.length > exports.MAX_EVENT_NAME_LENGTH) {
|
|
18
23
|
return null;
|
|
19
24
|
}
|
|
20
|
-
if (
|
|
25
|
+
if (!EVENT_NAME_PATTERN.test(trimmed)) {
|
|
21
26
|
return null;
|
|
22
27
|
}
|
|
23
28
|
return trimmed;
|
|
@@ -3,8 +3,20 @@ 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.getValueSymbol = getValueSymbol;
|
|
6
7
|
exports.getFunctionSymbol = getFunctionSymbol;
|
|
7
8
|
const typescript_1 = __importDefault(require("typescript"));
|
|
9
|
+
/**
|
|
10
|
+
* Symbol an identifier *refers to*. For the name in a shorthand property (`{ event }`) a plain
|
|
11
|
+
* `getSymbolAtLocation` yields the property's own symbol, not the variable/parameter it reads.
|
|
12
|
+
*/
|
|
13
|
+
function getValueSymbol(node, checker) {
|
|
14
|
+
const parent = node.parent;
|
|
15
|
+
if (parent && typescript_1.default.isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
16
|
+
return checker.getShorthandAssignmentValueSymbol(parent);
|
|
17
|
+
}
|
|
18
|
+
return checker.getSymbolAtLocation(node);
|
|
19
|
+
}
|
|
8
20
|
function getFunctionSymbol(node, checker) {
|
|
9
21
|
// function foo() {}
|
|
10
22
|
if (typescript_1.default.isFunctionDeclaration(node) && node.name) {
|
|
@@ -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"));
|