@eventra_dev/eventra-cli 1.0.0 → 1.0.2
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 +84 -9
- package/dist/analysis/engine/analyzeFile.js +2 -2
- package/dist/analysis/scanner/scanner.js +2 -2
- package/dist/analysis/scanner/sinkDetector.js +2 -33
- package/dist/cli/watch.js +49 -38
- package/dist/config/config.js +1 -0
- package/dist/core/EventraEngine.js +5 -2
- package/dist/core/projectScanner.js +15 -8
- package/dist/plugin/adapters/external.js +103 -0
- package/dist/plugin/builtins/eventra-sdk.js +36 -0
- package/dist/plugin/loadPlugins.js +88 -0
- package/dist/plugin/registry.js +78 -0
- package/dist/plugin/types.js +2 -0
- package/package.json +4 -1
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-59%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
13
|
<img alt="node" src="https://img.shields.io/node/v/@eventra_dev/eventra-cli?style=flat-square&color=darkgreen&logo=node.js&logoColor=white">
|
|
14
14
|
<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>
|
|
@@ -23,7 +23,7 @@ Eventra CLI statically discovers analytics events from [**@eventra_dev/eventra-s
|
|
|
23
23
|
|
|
24
24
|
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`.
|
|
25
25
|
|
|
26
|
-
It does **not** detect generic `track()`, Segment, Google Analytics, or other libraries.
|
|
26
|
+
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
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -166,6 +166,66 @@ Network resilience:
|
|
|
166
166
|
|
|
167
167
|
---
|
|
168
168
|
|
|
169
|
+
## Plugins
|
|
170
|
+
|
|
171
|
+
The CLI core is framework-agnostic. Extensions are **separate npm packages** with their own types — the CLI loads them from `eventra.json` and adapts their output internally.
|
|
172
|
+
|
|
173
|
+
### Vue (`.vue` SFC)
|
|
174
|
+
|
|
175
|
+
Install the official Vue plugin and enable it in config:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pnpm add -D @eventra_dev/cli-plugin-vue
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"plugins": ["@eventra_dev/cli-plugin-vue"],
|
|
184
|
+
"sync": {
|
|
185
|
+
"include": ["**/*.{ts,tsx,js,jsx}"],
|
|
186
|
+
"exclude": ["node_modules", "dist", ".next", ".git"]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The plugin:
|
|
192
|
+
|
|
193
|
+
- splits each `.vue` file into virtual TypeScript modules (`App.vue.ts`, `App.vue.template.ts`)
|
|
194
|
+
- extracts `track()` calls from `<script>` blocks
|
|
195
|
+
- detects static `event="feature_name"` attributes in `<template>` (literals only)
|
|
196
|
+
|
|
197
|
+
`sync.include` does **not** need `**/*.vue` manually — the plugin registers `**/*.vue` via `includeGlobs`.
|
|
198
|
+
|
|
199
|
+
### Before publishing / local development
|
|
200
|
+
|
|
201
|
+
Plugins are resolved from your project's `node_modules` (same as any dependency):
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"devDependencies": {
|
|
206
|
+
"@eventra_dev/cli-plugin-vue": "file:../cli-plugin-vue"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The plugin package must be built (`dist/`) before use. Unpublished plugins work the same way — only the install source differs.
|
|
212
|
+
|
|
213
|
+
### Plugin contract (for authors)
|
|
214
|
+
|
|
215
|
+
External plugins export an object (or factory) with:
|
|
216
|
+
|
|
217
|
+
| Field | Purpose |
|
|
218
|
+
|-------|---------|
|
|
219
|
+
| `id` | Unique preprocessor name |
|
|
220
|
+
| `includeGlobs` | Extra glob patterns merged into the scan |
|
|
221
|
+
| `match(path)` | Whether this plugin handles a file |
|
|
222
|
+
| `transform({ path, source })` | Returns `{ modules: [{ path, content }] }` |
|
|
223
|
+
| `staticSinks?` | Declarative callee-based sink rules (CLI converts to internal detectors) |
|
|
224
|
+
|
|
225
|
+
No dependency on `@eventra_dev/eventra-cli` is required. See [`ARCHITECTURE.md`](./ARCHITECTURE.md#plugin-kernel-phase-15--foundation-shipped) for details.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
169
229
|
## Configuration
|
|
170
230
|
|
|
171
231
|
```json
|
|
@@ -174,6 +234,7 @@ Network resilience:
|
|
|
174
234
|
"endpoint": "",
|
|
175
235
|
"events": [],
|
|
176
236
|
"functionWrappers": [],
|
|
237
|
+
"plugins": [],
|
|
177
238
|
"sync": {
|
|
178
239
|
"include": ["**/*.{ts,tsx,js,jsx}"],
|
|
179
240
|
"exclude": ["node_modules", "dist", ".next", ".git"]
|
|
@@ -181,14 +242,25 @@ Network resilience:
|
|
|
181
242
|
}
|
|
182
243
|
```
|
|
183
244
|
|
|
245
|
+
| Field | Description |
|
|
246
|
+
|-------|-------------|
|
|
247
|
+
| `plugins` | Module specifiers to `import()` at startup (e.g. `@eventra_dev/cli-plugin-vue`) |
|
|
248
|
+
| `sync.include` | Base glob patterns; plugin `includeGlobs` are merged automatically |
|
|
249
|
+
| `sync.exclude` | Paths skipped during scan |
|
|
250
|
+
|
|
184
251
|
---
|
|
185
252
|
|
|
186
253
|
## How it works
|
|
187
254
|
|
|
188
|
-
1. Load
|
|
189
|
-
2.
|
|
190
|
-
3.
|
|
191
|
-
4.
|
|
255
|
+
1. Load built-in plugins (`eventra-sdk` sink detector) and any packages listed in `plugins`.
|
|
256
|
+
2. Glob project files (`sync.include` + plugin `includeGlobs`).
|
|
257
|
+
3. Run file preprocessors (e.g. `.vue` → virtual `.ts` modules).
|
|
258
|
+
4. Load sources into an incremental TypeScript program (with SDK type shim).
|
|
259
|
+
5. **Phase 1** — find `Eventra` instances from `@eventra_dev/eventra-sdk` and register function wrappers that call `.track()`.
|
|
260
|
+
6. **Phase 2** — resolve static event names and wrapper propagation chains (sink detector chain includes plugin sinks).
|
|
261
|
+
7. Write results to `eventra.json`.
|
|
262
|
+
|
|
263
|
+
`watch` tracks **disk source files** (including `.vue`), re-runs preprocessors on change, and incrementally updates the engine.
|
|
192
264
|
|
|
193
265
|
No runtime execution. No monkey-patching.
|
|
194
266
|
|
|
@@ -203,9 +275,9 @@ No runtime execution. No monkey-patching.
|
|
|
203
275
|
|
|
204
276
|
## Test Coverage
|
|
205
277
|
|
|
206
|
-
Two test layers, **
|
|
278
|
+
Two test layers, **59 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios**.
|
|
207
279
|
|
|
208
|
-
**Unit tests (vitest)** —
|
|
280
|
+
**Unit tests (vitest)** — 13 suites covering core modules:
|
|
209
281
|
|
|
210
282
|
| Module | Covers |
|
|
211
283
|
|---|---|
|
|
@@ -214,9 +286,12 @@ Two test layers, **52 unit tests + 12 e2e fixtures + 3 `check` exit-code scenari
|
|
|
214
286
|
| `DocumentRegistry` | Path normalization, version bumping, no-op on identical content, `ensure()` from disk |
|
|
215
287
|
| `CompilerContext` | Stage/update/remove files, `resolveModule` with `tsconfig.json` `paths`, source-file enumeration |
|
|
216
288
|
| `EventraEngine` | Direct calls, SDK isolation, cross-file wrappers, file updates, file removal, wrapper filtering |
|
|
289
|
+
| `PluginRegistry` | Built-in SDK sink, preprocessors, virtual-path mapping, include-pattern dedup |
|
|
290
|
+
| `external plugin adapter` | Transform output mapping, static sink registration, invalid result rejection |
|
|
291
|
+
| `vue-shaped external plugin` | Adapter path for script + template virtual modules and `staticSinks` |
|
|
217
292
|
| `processFile` | Script-kind detection, import/export specifier extraction |
|
|
218
293
|
| `extractTemplateExpressions` | Vue/Svelte/Astro attribute patterns |
|
|
219
|
-
| `normalizeConfig` | Sort events, dedupe wrappers, defaults, preserve `apiKey` / `endpoint` / `sync` |
|
|
294
|
+
| `normalizeConfig` | Sort events, dedupe wrappers, defaults, preserve `apiKey` / `endpoint` / `sync` / `plugins` |
|
|
220
295
|
| `buildConfigFromScan` | Replace events + wrappers, preserve everything else |
|
|
221
296
|
| `hash` | Stability and uniqueness |
|
|
222
297
|
|
|
@@ -4,8 +4,8 @@ exports.indexSourceFile = indexSourceFile;
|
|
|
4
4
|
exports.extractFromIndex = extractFromIndex;
|
|
5
5
|
const scanner_1 = require("../scanner/scanner");
|
|
6
6
|
const extractor_1 = require("../extractor/extractor");
|
|
7
|
-
function indexSourceFile(source, checker, wrapperRegistry) {
|
|
8
|
-
return (0, scanner_1.scanSource)(source, checker, wrapperRegistry);
|
|
7
|
+
function indexSourceFile(source, checker, wrapperRegistry, plugins) {
|
|
8
|
+
return (0, scanner_1.scanSource)(source, checker, wrapperRegistry, plugins);
|
|
9
9
|
}
|
|
10
10
|
function extractFromIndex(index, config, checker, wrapperRegistry, evaluationCache, resolvedCallCache, returnPropagationCache, resolvedExportCache) {
|
|
11
11
|
return (0, extractor_1.extractEvents)(index, checker, config, evaluationCache, resolvedExportCache, resolvedCallCache, returnPropagationCache, wrapperRegistry);
|
|
@@ -7,7 +7,7 @@ exports.scanSource = scanSource;
|
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
8
|
const sinkDetector_1 = require("./sinkDetector");
|
|
9
9
|
const propagationAnalyzer_1 = require("./analyzer/propagationAnalyzer");
|
|
10
|
-
function scanSource(source, checker, wrapperRegistry) {
|
|
10
|
+
function scanSource(source, checker, wrapperRegistry, plugins) {
|
|
11
11
|
const sinks = [];
|
|
12
12
|
const wrappers = [];
|
|
13
13
|
const trackCalls = [];
|
|
@@ -23,7 +23,7 @@ function scanSource(source, checker, wrapperRegistry) {
|
|
|
23
23
|
}
|
|
24
24
|
// sink detection
|
|
25
25
|
if (typescript_1.default.isCallExpression(node)) {
|
|
26
|
-
const sink = (0, sinkDetector_1.detectTrackSink)(node, checker);
|
|
26
|
+
const sink = (0, sinkDetector_1.detectTrackSink)(node, checker, plugins);
|
|
27
27
|
if (sink) {
|
|
28
28
|
sinks.push(sink);
|
|
29
29
|
const trackedArguments = sink.trackedArguments.map(tracked => {
|
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.detectTrackSink = detectTrackSink;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** SDK: track(name: string, options?) — event name is always the first argument. */
|
|
10
|
-
function extractTrackedArguments(call) {
|
|
11
|
-
if (call.arguments.length === 0) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
const first = call.arguments[0];
|
|
15
|
-
if (typescript_1.default.isObjectLiteralExpression(first)) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
return [
|
|
19
|
-
{
|
|
20
|
-
index: 0,
|
|
21
|
-
propertyPath: [],
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
}
|
|
25
|
-
function detectTrackSink(call, checker) {
|
|
26
|
-
if (!(0, isEventraTrackCall_1.isEventraSdkTrackCall)(call, checker)) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
const trackedArguments = extractTrackedArguments(call);
|
|
30
|
-
if (trackedArguments.length === 0) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
call,
|
|
35
|
-
trackedArguments,
|
|
36
|
-
};
|
|
4
|
+
function detectTrackSink(call, checker, plugins) {
|
|
5
|
+
return plugins.detectSink({ call, checker });
|
|
37
6
|
}
|
package/dist/cli/watch.js
CHANGED
|
@@ -13,6 +13,7 @@ const config_1 = require("../config/config");
|
|
|
13
13
|
const eventraSdk_1 = require("../analysis/sdk/eventraSdk");
|
|
14
14
|
const EventraEngine_1 = require("../core/EventraEngine");
|
|
15
15
|
const scanResults_1 = require("../core/scanResults");
|
|
16
|
+
const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
16
17
|
const SDK_TYPES_FILE = "__eventra_sdk_types__.d.ts";
|
|
17
18
|
const processFile_1 = require("../filesystem/processFile");
|
|
18
19
|
const getVirtualFile_1 = require("../filesystem/getVirtualFile");
|
|
@@ -23,15 +24,18 @@ async function watch() {
|
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
console.log(chalk_1.default.blue("Watching...\n"));
|
|
26
|
-
const
|
|
27
|
+
const plugins = await (0, loadPlugins_1.createPluginRegistry)(config);
|
|
28
|
+
const include = [...config.sync.include, ...plugins.getIncludePatterns()];
|
|
29
|
+
const engine = new EventraEngine_1.EventraEngine(process.cwd(), plugins);
|
|
27
30
|
const fileDeps = new Map();
|
|
28
31
|
const cache = new Map();
|
|
29
32
|
const watchedDeps = new Set();
|
|
30
33
|
const trackedFiles = new Set();
|
|
34
|
+
const sourceFiles = new Set();
|
|
31
35
|
const normalize = (file) => {
|
|
32
36
|
return path_1.default.resolve(file).replace(/\\/g, "/");
|
|
33
37
|
};
|
|
34
|
-
const files = await (0, fast_glob_1.default)(
|
|
38
|
+
const files = await (0, fast_glob_1.default)(include, {
|
|
35
39
|
ignore: config.sync.exclude,
|
|
36
40
|
absolute: true,
|
|
37
41
|
});
|
|
@@ -40,16 +44,19 @@ async function watch() {
|
|
|
40
44
|
for (const file of files) {
|
|
41
45
|
try {
|
|
42
46
|
const abs = normalize(file);
|
|
47
|
+
sourceFiles.add(abs);
|
|
43
48
|
const raw = await promises_1.default.readFile(abs, "utf-8");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
49
|
+
const virtualFiles = await plugins.preprocessFile({ fileName: abs, content: raw });
|
|
50
|
+
for (const virtualFile of virtualFiles) {
|
|
51
|
+
const scanPath = normalize(virtualFile.fileName);
|
|
52
|
+
const parsed = await (0, processFile_1.processFile)(scanPath, virtualFile.content);
|
|
53
|
+
cache.set(scanPath, parsed);
|
|
54
|
+
trackedFiles.add(scanPath);
|
|
55
|
+
await engine.preloadFile((0, getVirtualFile_1.getVirtualFile)(scanPath), virtualFile.content);
|
|
56
|
+
fileDeps.set(scanPath, new Set(parsed.dependencies
|
|
57
|
+
.filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
|
|
58
|
+
.map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(scanPath), dep)))));
|
|
59
|
+
}
|
|
53
60
|
}
|
|
54
61
|
catch {
|
|
55
62
|
// ignore invalid files
|
|
@@ -61,7 +68,7 @@ async function watch() {
|
|
|
61
68
|
const initialEvents = engine.getAllEvents();
|
|
62
69
|
const initialWrappers = engine.getAllFunctionWrappers();
|
|
63
70
|
console.log(chalk_1.default.gray(`Initial: ${initialEvents.length} events, ${initialWrappers.length} wrappers\n`));
|
|
64
|
-
const watcher = chokidar_1.default.watch([...
|
|
71
|
+
const watcher = chokidar_1.default.watch([...sourceFiles, ...watchedDeps], { ignoreInitial: true });
|
|
65
72
|
const queue = new Map();
|
|
66
73
|
let timer = null;
|
|
67
74
|
const run = async () => {
|
|
@@ -70,31 +77,31 @@ async function watch() {
|
|
|
70
77
|
for (const file of batch) {
|
|
71
78
|
try {
|
|
72
79
|
const raw = await promises_1.default.readFile(file, "utf-8");
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
const virtualFiles = await plugins.preprocessFile({ fileName: file, content: raw });
|
|
81
|
+
for (const virtualFile of virtualFiles) {
|
|
82
|
+
const scanPath = normalize(virtualFile.fileName);
|
|
83
|
+
const parsed = await (0, processFile_1.processFile)(scanPath, virtualFile.content);
|
|
84
|
+
cache.set(scanPath, parsed);
|
|
85
|
+
trackedFiles.add(scanPath);
|
|
86
|
+
await engine.updateFile((0, getVirtualFile_1.getVirtualFile)(scanPath), virtualFile.content, config);
|
|
87
|
+
const prevDeps = fileDeps.get(scanPath) ?? new Set();
|
|
88
|
+
const nextDeps = new Set(parsed.dependencies
|
|
89
|
+
.filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
|
|
90
|
+
.map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(scanPath), dep))));
|
|
91
|
+
for (const dep of nextDeps) {
|
|
92
|
+
if (prevDeps.has(dep))
|
|
93
|
+
continue;
|
|
94
|
+
watcher.add(dep);
|
|
95
|
+
watchedDeps.add(dep);
|
|
96
|
+
}
|
|
97
|
+
for (const dep of prevDeps) {
|
|
98
|
+
if (nextDeps.has(dep))
|
|
99
|
+
continue;
|
|
100
|
+
watcher.unwatch(dep);
|
|
101
|
+
watchedDeps.delete(dep);
|
|
102
|
+
}
|
|
103
|
+
fileDeps.set(scanPath, nextDeps);
|
|
90
104
|
}
|
|
91
|
-
for (const dep of prevDeps) {
|
|
92
|
-
if (nextDeps.has(dep))
|
|
93
|
-
continue;
|
|
94
|
-
watcher.unwatch(dep);
|
|
95
|
-
watchedDeps.delete(dep);
|
|
96
|
-
}
|
|
97
|
-
fileDeps.set(abs, nextDeps);
|
|
98
105
|
}
|
|
99
106
|
catch {
|
|
100
107
|
console.log(chalk_1.default.gray(`skip: ${file}`));
|
|
@@ -115,8 +122,12 @@ async function watch() {
|
|
|
115
122
|
.on("add", schedule)
|
|
116
123
|
.on("unlink", async (file) => {
|
|
117
124
|
const abs = normalize(file);
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
sourceFiles.delete(abs);
|
|
126
|
+
const virtualPaths = plugins.getVirtualPathsForSource(abs);
|
|
127
|
+
for (const virtualPath of virtualPaths) {
|
|
128
|
+
trackedFiles.delete(virtualPath);
|
|
129
|
+
await engine.removeFile((0, getVirtualFile_1.getVirtualFile)(virtualPath), config);
|
|
130
|
+
}
|
|
120
131
|
await (0, scanResults_1.persistScanResults)(config, engine);
|
|
121
132
|
console.log(chalk_1.default.red(`Removed → ${engine.getAllEvents().length} events, ${engine.getAllFunctionWrappers().length} wrappers`));
|
|
122
133
|
});
|
package/dist/config/config.js
CHANGED
|
@@ -28,6 +28,7 @@ function normalizeConfig(config) {
|
|
|
28
28
|
endpoint: config.endpoint ?? "",
|
|
29
29
|
events: [...(config.events ?? [])].sort(),
|
|
30
30
|
functionWrappers: uniqueWrappers.sort((a, b) => a.name.localeCompare(b.name)),
|
|
31
|
+
plugins: [...(config.plugins ?? [])],
|
|
31
32
|
sync: {
|
|
32
33
|
include: config.sync?.include ?? DEFAULT_INCLUDE,
|
|
33
34
|
exclude: config.sync?.exclude ?? DEFAULT_EXCLUDE,
|
|
@@ -16,6 +16,7 @@ const returnPropagationCache_1 = require("../analysis/cache/returnPropagationCac
|
|
|
16
16
|
const analyzeFile_1 = require("../analysis/engine/analyzeFile");
|
|
17
17
|
const wrapperRegistry_1 = require("../analysis/symbols/wrapperRegistry");
|
|
18
18
|
const processFile_1 = require("../filesystem/processFile");
|
|
19
|
+
const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
19
20
|
const EMPTY_RESULT = () => ({
|
|
20
21
|
events: new Set(),
|
|
21
22
|
detectedFunctionWrappers: new Set(),
|
|
@@ -32,6 +33,7 @@ class EventraEngine {
|
|
|
32
33
|
fileIndices = new Map();
|
|
33
34
|
fileResults = new Map();
|
|
34
35
|
normalizedPathCache = new Map();
|
|
36
|
+
plugins;
|
|
35
37
|
isPreloading = false;
|
|
36
38
|
lastConfig = {
|
|
37
39
|
events: [],
|
|
@@ -41,7 +43,8 @@ class EventraEngine {
|
|
|
41
43
|
exclude: [],
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
|
-
constructor(rootDir) {
|
|
46
|
+
constructor(rootDir, plugins = (0, loadPlugins_1.createBuiltinPluginRegistry)()) {
|
|
47
|
+
this.plugins = plugins;
|
|
45
48
|
this.compiler = new compilerContext_1.CompilerContext(rootDir);
|
|
46
49
|
this.wrapperRegistry = new wrapperRegistry_1.WrapperRegistry(this.compiler.getChecker(), this.resolvedExportCache);
|
|
47
50
|
this.scheduler = new scheduler_1.Scheduler(async (updates) => {
|
|
@@ -184,7 +187,7 @@ class EventraEngine {
|
|
|
184
187
|
this.fileIndices.delete(normalized);
|
|
185
188
|
return;
|
|
186
189
|
}
|
|
187
|
-
const index = (0, analyzeFile_1.indexSourceFile)(source, this.compiler.getChecker(), this.wrapperRegistry);
|
|
190
|
+
const index = (0, analyzeFile_1.indexSourceFile)(source, this.compiler.getChecker(), this.wrapperRegistry, this.plugins);
|
|
188
191
|
this.fileIndices.set(normalized, index);
|
|
189
192
|
}
|
|
190
193
|
extractFile(fileName, config) {
|
|
@@ -11,6 +11,7 @@ const eventraSdk_1 = require("../analysis/sdk/eventraSdk");
|
|
|
11
11
|
const EventraEngine_1 = require("./EventraEngine");
|
|
12
12
|
const processFile_1 = require("../filesystem/processFile");
|
|
13
13
|
const getVirtualFile_1 = require("../filesystem/getVirtualFile");
|
|
14
|
+
const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
14
15
|
const SDK_TYPES_FILE = "__eventra_sdk_types__.d.ts";
|
|
15
16
|
async function getParsedFile(file, cache) {
|
|
16
17
|
const existing = cache.get(file);
|
|
@@ -22,11 +23,13 @@ async function getParsedFile(file, cache) {
|
|
|
22
23
|
return parsed;
|
|
23
24
|
}
|
|
24
25
|
async function scanProject(config) {
|
|
25
|
-
const
|
|
26
|
+
const plugins = await (0, loadPlugins_1.createPluginRegistry)(config);
|
|
27
|
+
const include = [...config.sync.include, ...plugins.getIncludePatterns()];
|
|
28
|
+
const files = await (0, fast_glob_1.default)(include, {
|
|
26
29
|
ignore: config.sync.exclude,
|
|
27
30
|
absolute: true,
|
|
28
31
|
});
|
|
29
|
-
const engine = new EventraEngine_1.EventraEngine(process.cwd());
|
|
32
|
+
const engine = new EventraEngine_1.EventraEngine(process.cwd(), plugins);
|
|
30
33
|
const cache = new Map();
|
|
31
34
|
const toScan = new Set();
|
|
32
35
|
for (const file of files) {
|
|
@@ -34,12 +37,16 @@ async function scanProject(config) {
|
|
|
34
37
|
const raw = await promises_1.default.readFile(file, "utf8");
|
|
35
38
|
if (raw.length > 2_000_000)
|
|
36
39
|
continue;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const virtualFiles = await plugins.preprocessFile({ fileName: file, content: raw });
|
|
41
|
+
for (const virtualFile of virtualFiles) {
|
|
42
|
+
const parsed = await (0, processFile_1.processFile)(virtualFile.fileName, virtualFile.content);
|
|
43
|
+
const scanPath = path_1.default.resolve(virtualFile.fileName);
|
|
44
|
+
cache.set(scanPath, parsed);
|
|
45
|
+
toScan.add(scanPath);
|
|
46
|
+
for (const dep of parsed.dependencies) {
|
|
47
|
+
if (dep.startsWith(".") || dep.startsWith("/")) {
|
|
48
|
+
toScan.add(path_1.default.resolve(path_1.default.dirname(scanPath), dep));
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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.isExternalCliPlugin = isExternalCliPlugin;
|
|
7
|
+
exports.registerExternalCliPlugin = registerExternalCliPlugin;
|
|
8
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
9
|
+
function isStaticSink(value) {
|
|
10
|
+
if (!value || typeof value !== "object") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const sink = value;
|
|
14
|
+
return (typeof sink.id === "string" &&
|
|
15
|
+
sink.id.trim().length > 0 &&
|
|
16
|
+
typeof sink.callee === "string" &&
|
|
17
|
+
sink.callee.trim().length > 0 &&
|
|
18
|
+
Number.isInteger(sink.eventNameArgumentIndex) &&
|
|
19
|
+
sink.eventNameArgumentIndex >= 0);
|
|
20
|
+
}
|
|
21
|
+
function isExternalCliPlugin(value) {
|
|
22
|
+
if (!value || typeof value !== "object") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const plugin = value;
|
|
26
|
+
if (typeof plugin.id !== "string" || plugin.id.trim().length === 0) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(plugin.includeGlobs) || plugin.includeGlobs.some((g) => typeof g !== "string")) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (plugin.staticSinks !== undefined) {
|
|
33
|
+
if (!Array.isArray(plugin.staticSinks) || plugin.staticSinks.some((s) => !isStaticSink(s))) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return typeof plugin.match === "function" && typeof plugin.transform === "function";
|
|
38
|
+
}
|
|
39
|
+
function createStaticCalleeSinkDetector(sink) {
|
|
40
|
+
return {
|
|
41
|
+
name: sink.id,
|
|
42
|
+
detect({ call }) {
|
|
43
|
+
const expr = call.expression;
|
|
44
|
+
if (!typescript_1.default.isIdentifier(expr) || expr.text !== sink.callee) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const argIndex = sink.eventNameArgumentIndex;
|
|
48
|
+
if (call.arguments.length <= argIndex) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const arg = call.arguments[argIndex];
|
|
52
|
+
if (!typescript_1.default.isStringLiteral(arg)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
call,
|
|
57
|
+
trackedArguments: [{ index: argIndex, propertyPath: [] }],
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function mapTransformModules(plugin, fileName, modules) {
|
|
63
|
+
if (!Array.isArray(modules)) {
|
|
64
|
+
throw new Error(`Plugin "${plugin.id}" returned invalid transform result for ${fileName}`);
|
|
65
|
+
}
|
|
66
|
+
return modules.map((module, index) => {
|
|
67
|
+
if (!module || typeof module !== "object") {
|
|
68
|
+
throw new Error(`Plugin "${plugin.id}" module at index ${index} is invalid`);
|
|
69
|
+
}
|
|
70
|
+
const { path: modulePath, content } = module;
|
|
71
|
+
if (typeof modulePath !== "string" || typeof content !== "string") {
|
|
72
|
+
throw new Error(`Plugin "${plugin.id}" module at index ${index} must have string path and content`);
|
|
73
|
+
}
|
|
74
|
+
return { fileName: modulePath, content };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Maps an external plugin contract into the internal PluginRegistry hooks. */
|
|
78
|
+
function registerExternalCliPlugin(plugin, registry) {
|
|
79
|
+
for (const glob of plugin.includeGlobs) {
|
|
80
|
+
registry.registerIncludePattern(glob);
|
|
81
|
+
}
|
|
82
|
+
registry.registerFilePreprocessor({
|
|
83
|
+
name: plugin.id,
|
|
84
|
+
test: (fileName) => plugin.match(fileName),
|
|
85
|
+
process: async ({ fileName, content }) => {
|
|
86
|
+
let result;
|
|
87
|
+
try {
|
|
88
|
+
result = await plugin.transform({ path: fileName, source: content });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
92
|
+
throw new Error(`Plugin "${plugin.id}" failed to transform ${fileName}: ${message}`);
|
|
93
|
+
}
|
|
94
|
+
return mapTransformModules(plugin, fileName, result?.modules);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
for (const sink of plugin.staticSinks ?? []) {
|
|
98
|
+
if (!isStaticSink(sink)) {
|
|
99
|
+
throw new Error(`Plugin "${plugin.id}" has invalid staticSink (id, callee, eventNameArgumentIndex)`);
|
|
100
|
+
}
|
|
101
|
+
registry.registerSinkDetector(createStaticCalleeSinkDetector(sink));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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.registerEventraSdkPlugin = registerEventraSdkPlugin;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const isEventraTrackCall_1 = require("../../analysis/sdk/isEventraTrackCall");
|
|
9
|
+
/** SDK: track(name: string, options?) — event name is always the first argument. */
|
|
10
|
+
function extractTrackedArguments(call) {
|
|
11
|
+
if (call.arguments.length === 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const first = call.arguments[0];
|
|
15
|
+
if (typescript_1.default.isObjectLiteralExpression(first)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return [{ index: 0, propertyPath: [] }];
|
|
19
|
+
}
|
|
20
|
+
const eventraSdkSinkDetector = {
|
|
21
|
+
name: "eventra-sdk",
|
|
22
|
+
detect({ call, checker }) {
|
|
23
|
+
if (!(0, isEventraTrackCall_1.isEventraSdkTrackCall)(call, checker)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const trackedArguments = extractTrackedArguments(call);
|
|
27
|
+
if (trackedArguments.length === 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return { call, trackedArguments };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
/** Built-in sink detection for @eventra_dev/eventra-sdk (always enabled). */
|
|
34
|
+
function registerEventraSdkPlugin(registry) {
|
|
35
|
+
registry.registerSinkDetector(eventraSdkSinkDetector);
|
|
36
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createBuiltinPluginRegistry = createBuiltinPluginRegistry;
|
|
37
|
+
exports.createPluginRegistry = createPluginRegistry;
|
|
38
|
+
const external_1 = require("./adapters/external");
|
|
39
|
+
const eventra_sdk_1 = require("./builtins/eventra-sdk");
|
|
40
|
+
const registry_1 = require("./registry");
|
|
41
|
+
async function resolvePluginExport(mod) {
|
|
42
|
+
let candidate = mod.default ?? mod;
|
|
43
|
+
if (typeof candidate === "function") {
|
|
44
|
+
candidate = candidate();
|
|
45
|
+
}
|
|
46
|
+
if (candidate && typeof candidate.then === "function") {
|
|
47
|
+
candidate = await candidate;
|
|
48
|
+
}
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
function pluginLoadHint(spec) {
|
|
52
|
+
if (spec.includes("plugin-vue") && !spec.includes("cli-plugin-vue")) {
|
|
53
|
+
return " Did you mean @eventra_dev/cli-plugin-vue?";
|
|
54
|
+
}
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
async function loadExternalPlugin(registry, spec) {
|
|
58
|
+
const trimmed = spec.trim();
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
return;
|
|
61
|
+
let mod;
|
|
62
|
+
try {
|
|
63
|
+
mod = await Promise.resolve(`${trimmed}`).then(s => __importStar(require(s)));
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
throw new Error(`Failed to load plugin "${trimmed}": ${message}.${pluginLoadHint(trimmed)}`);
|
|
68
|
+
}
|
|
69
|
+
const candidate = await resolvePluginExport(mod);
|
|
70
|
+
if (!(0, external_1.isExternalCliPlugin)(candidate)) {
|
|
71
|
+
throw new Error(`Plugin "${trimmed}" must export a CLI plugin ({ id, includeGlobs, match, transform }).${pluginLoadHint(trimmed)}`);
|
|
72
|
+
}
|
|
73
|
+
(0, external_1.registerExternalCliPlugin)(candidate, registry);
|
|
74
|
+
}
|
|
75
|
+
/** Built-in plugins only (sync). Used in tests and as engine default. */
|
|
76
|
+
function createBuiltinPluginRegistry() {
|
|
77
|
+
const registry = new registry_1.PluginRegistry();
|
|
78
|
+
(0, eventra_sdk_1.registerEventraSdkPlugin)(registry);
|
|
79
|
+
return registry;
|
|
80
|
+
}
|
|
81
|
+
/** Built-ins + plugins listed in eventra.json `plugins` array. */
|
|
82
|
+
async function createPluginRegistry(config) {
|
|
83
|
+
const registry = createBuiltinPluginRegistry();
|
|
84
|
+
for (const spec of config.plugins ?? []) {
|
|
85
|
+
await loadExternalPlugin(registry, spec);
|
|
86
|
+
}
|
|
87
|
+
return registry;
|
|
88
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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.PluginRegistry = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
class PluginRegistry {
|
|
9
|
+
preprocessors = [];
|
|
10
|
+
sinkDetectors = [];
|
|
11
|
+
includePatterns = [];
|
|
12
|
+
sourceToVirtuals = new Map();
|
|
13
|
+
registerFilePreprocessor(preprocessor) {
|
|
14
|
+
if (this.preprocessors.some((p) => p.name === preprocessor.name)) {
|
|
15
|
+
throw new Error(`File preprocessor already registered: ${preprocessor.name}`);
|
|
16
|
+
}
|
|
17
|
+
this.preprocessors.push(preprocessor);
|
|
18
|
+
}
|
|
19
|
+
registerSinkDetector(detector) {
|
|
20
|
+
if (this.sinkDetectors.some((d) => d.name === detector.name)) {
|
|
21
|
+
throw new Error(`Sink detector already registered: ${detector.name}`);
|
|
22
|
+
}
|
|
23
|
+
this.sinkDetectors.push(detector);
|
|
24
|
+
}
|
|
25
|
+
registerIncludePattern(pattern) {
|
|
26
|
+
const trimmed = pattern.trim();
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
return;
|
|
29
|
+
if (this.includePatterns.includes(trimmed))
|
|
30
|
+
return;
|
|
31
|
+
this.includePatterns.push(trimmed);
|
|
32
|
+
}
|
|
33
|
+
getIncludePatterns() {
|
|
34
|
+
return this.includePatterns;
|
|
35
|
+
}
|
|
36
|
+
getSinkDetectors() {
|
|
37
|
+
return this.sinkDetectors;
|
|
38
|
+
}
|
|
39
|
+
getFilePreprocessors() {
|
|
40
|
+
return this.preprocessors;
|
|
41
|
+
}
|
|
42
|
+
/** Disk paths produced by the last preprocess run for a source file (for watch unlink). */
|
|
43
|
+
getVirtualPathsForSource(fileName) {
|
|
44
|
+
const mapped = this.sourceToVirtuals.get(this.normalizePath(fileName));
|
|
45
|
+
if (mapped && mapped.length > 0) {
|
|
46
|
+
return mapped;
|
|
47
|
+
}
|
|
48
|
+
return [this.normalizePath(fileName)];
|
|
49
|
+
}
|
|
50
|
+
async preprocessFile(input) {
|
|
51
|
+
const sourceKey = this.normalizePath(input.fileName);
|
|
52
|
+
for (const preprocessor of this.preprocessors) {
|
|
53
|
+
if (!preprocessor.test(input.fileName)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const files = await preprocessor.process(input);
|
|
57
|
+
if (files.length > 0) {
|
|
58
|
+
this.sourceToVirtuals.set(sourceKey, files.map((file) => this.normalizePath(file.fileName)));
|
|
59
|
+
return files;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.sourceToVirtuals.delete(sourceKey);
|
|
63
|
+
return [{ fileName: input.fileName, content: input.content }];
|
|
64
|
+
}
|
|
65
|
+
detectSink(context) {
|
|
66
|
+
for (const detector of this.sinkDetectors) {
|
|
67
|
+
const sink = detector.detect(context);
|
|
68
|
+
if (sink) {
|
|
69
|
+
return sink;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
normalizePath(fileName) {
|
|
75
|
+
return path_1.default.resolve(fileName).replace(/\\/g, "/");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.PluginRegistry = PluginRegistry;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventra_dev/eventra-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"eventra": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js"
|
|
13
|
+
},
|
|
11
14
|
"files": [
|
|
12
15
|
"dist",
|
|
13
16
|
"README.md",
|