@eventra_dev/eventra-cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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-52%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
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 project files into an incremental TypeScript program (with SDK type shim).
189
- 2. **Phase 1** — find `Eventra` instances from `@eventra_dev/eventra-sdk` and register function wrappers that call `.track()`.
190
- 3. **Phase 2** — resolve static event names and wrapper propagation chains.
191
- 4. Write results to `eventra.json`.
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, **52 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios**.
278
+ Two test layers, **59 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios**.
207
279
 
208
- **Unit tests (vitest)** — 10 suites covering core modules:
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
- const typescript_1 = __importDefault(require("typescript"));
8
- const isEventraTrackCall_1 = require("../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 [
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 engine = new EventraEngine_1.EventraEngine(process.cwd());
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)(config.sync.include, {
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
- // processFile extracts import specifiers for chokidar tracking;
45
- // the engine itself receives raw file content.
46
- const parsed = await (0, processFile_1.processFile)(abs, raw);
47
- cache.set(abs, parsed);
48
- trackedFiles.add(abs);
49
- await engine.preloadFile((0, getVirtualFile_1.getVirtualFile)(abs), raw);
50
- fileDeps.set(abs, new Set(parsed.dependencies
51
- .filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
52
- .map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(abs), dep)))));
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([...trackedFiles, ...watchedDeps], { ignoreInitial: true });
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
- // processFile here is used ONLY to extract import specifiers for
74
- // chokidar's dependency tracking. The engine receives raw content
75
- // and parses it itself.
76
- const parsed = await (0, processFile_1.processFile)(file, raw);
77
- cache.set(file, parsed);
78
- trackedFiles.add(file);
79
- await engine.updateFile((0, getVirtualFile_1.getVirtualFile)(file), raw, config);
80
- const abs = normalize(file);
81
- const prevDeps = fileDeps.get(abs) ?? new Set();
82
- const nextDeps = new Set(parsed.dependencies
83
- .filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
84
- .map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(abs), dep))));
85
- for (const dep of nextDeps) {
86
- if (prevDeps.has(dep))
87
- continue;
88
- watcher.add(dep);
89
- watchedDeps.add(dep);
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
- trackedFiles.delete(abs);
119
- await engine.removeFile((0, getVirtualFile_1.getVirtualFile)(abs), config);
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
  });
@@ -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 files = await (0, fast_glob_1.default)(config.sync.include, {
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 parsed = await (0, processFile_1.processFile)(file, raw);
38
- cache.set(file, parsed);
39
- toScan.add(file);
40
- for (const dep of parsed.dependencies) {
41
- if (dep.startsWith(".") || dep.startsWith("/")) {
42
- toScan.add(path_1.default.resolve(path_1.default.dirname(file), dep));
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;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",