@eventra_dev/eventra-cli 0.0.17 → 0.0.19

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
@@ -1,3 +1,4 @@
1
+
1
2
  <p align="center">
2
3
  <img src="./assets/eventra-icon-animated.svg" width="120">
3
4
  </p>
@@ -7,56 +8,22 @@
7
8
  [![npm version](https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
8
9
  [![npm downloads](https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
9
10
  [![TypeScript](https://img.shields.io/badge/typescript-ready-blue.svg)](https://www.typescriptlang.org/)
10
- [![Tests](https://github.com/and-1991/eventra-cli/actions/workflows/release.yml/badge.svg)]()
11
11
  [![License](https://img.shields.io/npm/l/@eventra_dev/eventra-cli)]()
12
12
 
13
13
  Eventra CLI automatically discovers feature usage events in your codebase and syncs them with Eventra.
14
14
 
15
- Eventra CLI helps you:
16
-
17
- - Discover feature usage automatically
18
- - Detect wrapper components
19
- - Detect wrapper functions
20
- - Keep events in sync
21
- - Register features in Eventra
22
- - Maintain consistent event naming
23
-
24
- It is designed to be:
25
-
26
- - framework-agnostic
27
- - static analysis based
28
- - zero runtime overhead
29
- - production-safe
30
- - backend + frontend compatible
31
-
32
15
  ---
33
16
 
34
17
  # Installation
35
18
 
36
- ### npm
37
-
38
19
  ```bash
39
20
  npm install -D @eventra_dev/eventra-cli
40
21
  ```
41
22
 
42
- ### pnpm
43
-
44
23
  ```bash
45
24
  pnpm add -D @eventra_dev/eventra-cli
46
25
  ```
47
26
 
48
- ### yarn
49
-
50
- ```bash
51
- yarn add -D @eventra_dev/eventra-cli
52
- ```
53
-
54
- ### npx
55
-
56
- ```bash
57
- npx eventra init
58
- ```
59
-
60
27
  ---
61
28
 
62
29
  # Quick Start
@@ -64,6 +31,7 @@ npx eventra init
64
31
  ```bash
65
32
  eventra init
66
33
  eventra sync
34
+ eventra check
67
35
  eventra send
68
36
  ```
69
37
 
@@ -75,75 +43,93 @@ eventra send
75
43
 
76
44
  Creates `eventra.json` configuration file.
77
45
 
78
- ```bash
79
- eventra init
80
- ```
81
-
82
46
  ---
83
47
 
84
48
  ## eventra sync
85
49
 
86
50
  Scans your project and discovers events automatically.
87
51
 
88
- ### track() detection
52
+ Supports:
89
53
 
90
- ```ts
91
- tracker.track("feature_created")
92
- ```
54
+ - track("event")
55
+ - analytics.track("event")
56
+ - function wrappers
57
+ - component wrappers
58
+ - aliases
93
59
 
94
- ```ts
95
- track("user_signup")
60
+ ---
61
+
62
+ ## eventra check
63
+
64
+ Validate events without modifying config.
65
+
66
+ ```bash
67
+ eventra check
96
68
  ```
97
69
 
98
- ---
70
+ ### Detects:
99
71
 
100
- ### Component wrappers
72
+ - New events
73
+ - Unresolved dynamic events
74
+
75
+ ### Output example:
101
76
 
102
- ```tsx
103
- <TrackedButton event="feature_created" />
104
77
  ```
78
+ New events:
79
+ + user_click (src/Button.tsx:42)
105
80
 
106
- ```tsx
107
- <MyComponent event="user_signup" />
81
+ Dynamic:
82
+ - eventName (src/App.tsx:10)
108
83
  ```
109
84
 
110
85
  ---
111
86
 
112
- ### Function wrappers
87
+ ## eventra check --fix
113
88
 
114
- ```ts
115
- trackFeature("feature_created")
116
- ```
89
+ Interactive auto-fix mode.
117
90
 
118
- ```ts
119
- analytics.trackFeature("user_signup")
91
+ ```bash
92
+ eventra check --fix
120
93
  ```
121
94
 
95
+ ### What it does:
96
+
97
+ - Adds new events to config
98
+ - Resolves dynamic events via alias
99
+ - Allows skipping dynamic values
100
+
122
101
  ---
123
102
 
124
- ## eventra send
103
+ ## eventra watch
125
104
 
126
- Send events to Eventra backend.
105
+ Real-time event detection.
127
106
 
128
107
  ```bash
129
- eventra send
108
+ eventra watch
130
109
  ```
131
110
 
132
- New events are queued for processing and will appear in dashboard shortly.
111
+ ### Features:
133
112
 
134
- Processing typically takes:
113
+ - Detects new events instantly
114
+ - Debounced updates
115
+ - No duplicates
116
+ - Ignores temp files
135
117
 
136
- ~1-2 minutes
118
+ ---
119
+
120
+ ## eventra send
137
121
 
138
- This delay ensures reliable event ingestion and aggregation.
122
+ Send events to backend.
123
+
124
+ ```bash
125
+ eventra send
126
+ ```
139
127
 
140
128
  ---
141
129
 
142
130
  # Configuration
143
131
 
144
- Eventra CLI creates `eventra.json`
145
-
146
- Example:
132
+ Example `eventra.json`:
147
133
 
148
134
  ```json
149
135
  {
@@ -151,28 +137,58 @@ Example:
151
137
  "events": [],
152
138
  "wrappers": [],
153
139
  "functionWrappers": [],
140
+ "aliases": {},
154
141
  "sync": {
155
- "include": ["**/*.{ts,tsx,js,jsx}"],
156
- "exclude": [
157
- "node_modules",
158
- "dist",
159
- ".next",
160
- ".git"
161
- ]
142
+ "include": ["**/*.{ts,tsx,js,jsx,vue,svelte,astro}"],
143
+ "exclude": ["node_modules", "dist", ".next", ".git"]
162
144
  }
163
145
  }
164
146
  ```
165
147
 
166
148
  ---
167
149
 
150
+ # Key Concepts
151
+
152
+ ### Aliases
153
+
154
+ ```ts
155
+ const EVENT = "user_click";
156
+ track(EVENT);
157
+ ```
158
+
159
+ Auto-resolved → `user_click`
160
+
161
+ ---
162
+
163
+ ### Dynamic events
164
+
165
+ ```ts
166
+ track(eventName);
167
+ ```
168
+
169
+ Must be:
170
+
171
+ - aliased
172
+ - or skipped (`__skip__`)
173
+
174
+ ---
175
+
176
+ ### File tracking
177
+
178
+ All events include:
179
+
180
+ ```
181
+ event_name (file:line)
182
+ ```
183
+
184
+ ---
185
+
168
186
  # Supported Frameworks
169
187
 
170
188
  Frontend:
171
189
 
172
190
  - React
173
- - Next.js
174
191
  - Vue
175
- - Nuxt
176
192
  - Svelte
177
193
  - Astro
178
194
 
@@ -182,31 +198,12 @@ Backend:
182
198
  - Express
183
199
  - NestJS
184
200
  - Fastify
185
- - Hono
186
- - Bun
187
- - Deno
188
-
189
- ---
190
-
191
- # How It Works
192
-
193
- Eventra CLI:
194
-
195
- 1. Scans your codebase
196
- 2. Detects tracking calls
197
- 3. Detects wrapper components
198
- 4. Detects wrapper functions
199
- 5. Syncs discovered events
200
- 6. Registers events in Eventra
201
-
202
- No runtime SDK required.
203
201
 
204
202
  ---
205
203
 
206
204
  # Requirements
207
205
 
208
206
  - Node.js 18+
209
- - JavaScript / TypeScript project
210
207
 
211
208
  ---
212
209
 
@@ -0,0 +1,153 @@
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.check = check;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const fast_glob_1 = __importDefault(require("fast-glob"));
9
+ const ts_morph_1 = require("ts-morph");
10
+ const promises_1 = __importDefault(require("fs/promises"));
11
+ const inquirer_1 = __importDefault(require("inquirer"));
12
+ const config_1 = require("../utils/config");
13
+ const router_1 = require("../utils/parsers/router");
14
+ const vue_1 = require("../utils/parsers/vue");
15
+ const svelte_1 = require("../utils/parsers/svelte");
16
+ const astro_1 = require("../utils/parsers/astro");
17
+ const track_1 = require("../utils/scanners/track");
18
+ const function_wrappers_1 = require("../utils/scanners/function-wrappers");
19
+ const component_wrappers_1 = require("../utils/scanners/component-wrappers");
20
+ async function check({ fix = false }) {
21
+ const config = await (0, config_1.loadConfig)();
22
+ if (!config) {
23
+ console.log(chalk_1.default.red("eventra.json not found"));
24
+ process.exit(1);
25
+ }
26
+ console.log(chalk_1.default.blue("Checking events..."));
27
+ const project = new ts_morph_1.Project();
28
+ const knownEvents = new Set(config.events ?? []);
29
+ const aliases = { ...(config.aliases ?? {}) };
30
+ const foundEvents = new Map();
31
+ const newEvents = new Map();
32
+ const unresolvedDynamic = new Map();
33
+ const files = await (0, fast_glob_1.default)(config.sync.include, {
34
+ ignore: config.sync.exclude
35
+ });
36
+ for (const file of files) {
37
+ try {
38
+ const stat = await promises_1.default.stat(file);
39
+ if (stat.size > 2 * 1024 * 1024) {
40
+ continue;
41
+ }
42
+ const parser = (0, router_1.detectParser)(file);
43
+ let content = await promises_1.default.readFile(file, "utf-8");
44
+ if (parser === "vue")
45
+ content = (0, vue_1.parseVue)(content);
46
+ if (parser === "svelte")
47
+ content = (0, svelte_1.parseSvelte)(content);
48
+ if (parser === "astro")
49
+ content = (0, astro_1.parseAstro)(content);
50
+ const source = project.createSourceFile(file, content, {
51
+ overwrite: true
52
+ });
53
+ const found = [
54
+ ...(0, track_1.scanTrack)(source, config, aliases),
55
+ ...(0, function_wrappers_1.scanFunctionWrappers)(source, config.functionWrappers ?? [], aliases),
56
+ ...(0, component_wrappers_1.scanComponentWrappers)(source, config.wrappers ?? [], aliases)
57
+ ];
58
+ for (const event of found) {
59
+ const { value, dynamic } = event;
60
+ if (!value)
61
+ continue;
62
+ // skip
63
+ if (aliases[value] === "__skip__")
64
+ continue;
65
+ // alias resolve
66
+ if (value in aliases) {
67
+ foundEvents.set(aliases[value], {
68
+ ...event,
69
+ value: aliases[value]
70
+ });
71
+ continue;
72
+ }
73
+ if (dynamic) {
74
+ unresolvedDynamic.set(value, event);
75
+ continue;
76
+ }
77
+ foundEvents.set(value, event);
78
+ }
79
+ project.removeSourceFile(source);
80
+ }
81
+ catch {
82
+ console.log(chalk_1.default.gray(`Skipped: ${file}`));
83
+ }
84
+ }
85
+ // find new
86
+ for (const [key, event] of foundEvents) {
87
+ if (!knownEvents.has(key)) {
88
+ newEvents.set(key, event);
89
+ }
90
+ }
91
+ // FIX MODE
92
+ if (fix) {
93
+ console.log(chalk_1.default.yellow("\nFix mode\n"));
94
+ // add new events
95
+ for (const [name] of newEvents) {
96
+ config.events.push(name);
97
+ console.log(chalk_1.default.green(`+ ${name}`));
98
+ }
99
+ // resolve dynamic
100
+ for (const [value] of unresolvedDynamic) {
101
+ const { action } = await inquirer_1.default.prompt([
102
+ {
103
+ type: "list",
104
+ name: "action",
105
+ message: `Dynamic: ${value}`,
106
+ choices: [
107
+ { name: "Alias", value: "alias" },
108
+ { name: "Skip", value: "skip" }
109
+ ]
110
+ }
111
+ ]);
112
+ if (action === "skip") {
113
+ aliases[value] = "__skip__";
114
+ continue;
115
+ }
116
+ const { name } = await inquirer_1.default.prompt([
117
+ {
118
+ type: "input",
119
+ name: "name",
120
+ message: "Event name:"
121
+ }
122
+ ]);
123
+ const normalized = name.trim().toLowerCase();
124
+ if (!normalized)
125
+ continue;
126
+ aliases[value] = normalized;
127
+ config.events.push(normalized);
128
+ console.log(chalk_1.default.green(`${value} → ${normalized}`));
129
+ }
130
+ config.aliases = Object.fromEntries(Object.entries(aliases).sort());
131
+ config.events = [...new Set(config.events)].sort();
132
+ await (0, config_1.saveConfig)(config);
133
+ console.log(chalk_1.default.green("\n✔ Fixed"));
134
+ return;
135
+ }
136
+ // NORMAL MODE
137
+ let hasError = false;
138
+ if (unresolvedDynamic.size) {
139
+ hasError = true;
140
+ console.log(chalk_1.default.red("\nDynamic events:"));
141
+ unresolvedDynamic.forEach((e) => console.log(chalk_1.default.red(`- ${e.value} (${e.file}:${e.line})`)));
142
+ }
143
+ if (newEvents.size) {
144
+ hasError = true;
145
+ console.log(chalk_1.default.red("\nNew events:"));
146
+ newEvents.forEach((e) => console.log(chalk_1.default.red(`+ ${e.value} (${e.file}:${e.line})`)));
147
+ }
148
+ if (hasError) {
149
+ console.log(chalk_1.default.red("\nEvent check failed"));
150
+ process.exit(1);
151
+ }
152
+ console.log(chalk_1.default.green("\nAll good"));
153
+ }
@@ -10,6 +10,8 @@ const ts_morph_1 = require("ts-morph");
10
10
  const promises_1 = __importDefault(require("fs/promises"));
11
11
  const inquirer_1 = __importDefault(require("inquirer"));
12
12
  const config_1 = require("../utils/config");
13
+ const detect_wrappers_1 = require("../utils/detect-wrappers");
14
+ const detect_aliases_1 = require("../utils/detect-aliases");
13
15
  const router_1 = require("../utils/parsers/router");
14
16
  const vue_1 = require("../utils/parsers/vue");
15
17
  const svelte_1 = require("../utils/parsers/svelte");
@@ -32,65 +34,103 @@ async function sync() {
32
34
  ignore: config.sync.exclude
33
35
  });
34
36
  for (const file of files) {
35
- const parser = (0, router_1.detectParser)(file);
36
- let content = await promises_1.default.readFile(file, "utf-8");
37
- if (parser === "vue")
38
- content = (0, vue_1.parseVue)(content);
39
- if (parser === "svelte")
40
- content = (0, svelte_1.parseSvelte)(content);
41
- if (parser === "astro")
42
- content = (0, astro_1.parseAstro)(content);
43
- const virtualFile = parser === "ts"
44
- ? file
45
- : file + ".tsx";
46
- const source = project.createSourceFile(virtualFile, content, { overwrite: true });
47
- const found = [
48
- ...(0, track_1.scanTrack)(source, config),
49
- ...(0, function_wrappers_1.scanFunctionWrappers)(source, config.functionWrappers ?? []),
50
- ...(0, component_wrappers_1.scanComponentWrappers)(source, config.wrappers ?? [])
51
- ];
52
- for (const event of found) {
53
- const { value, dynamic } = event;
54
- if (!value)
55
- continue;
56
- // alias
57
- if (aliases[value]) {
58
- console.log(chalk_1.default.gray(`alias: ${value} → ${aliases[value]}`));
59
- events.add(aliases[value]);
60
- continue;
37
+ try {
38
+ const parser = (0, router_1.detectParser)(file);
39
+ let content = await promises_1.default.readFile(file, "utf-8");
40
+ if (parser === "vue")
41
+ content = (0, vue_1.parseVue)(content);
42
+ if (parser === "svelte")
43
+ content = (0, svelte_1.parseSvelte)(content);
44
+ if (parser === "astro")
45
+ content = (0, astro_1.parseAstro)(content);
46
+ const virtualFile = parser === "ts" ? file : file + ".tsx";
47
+ const source = project.createSourceFile(virtualFile, content, { overwrite: true });
48
+ // AUTO ALIASES
49
+ const detectedAliases = (0, detect_aliases_1.detectAliases)(source);
50
+ for (const key in detectedAliases) {
51
+ if (!(key in aliases)) {
52
+ aliases[key] = detectedAliases[key];
53
+ console.log(chalk_1.default.green(`Detected alias: ${key} → ${detectedAliases[key]}`));
54
+ }
61
55
  }
62
- // static
63
- if (!dynamic) {
64
- events.add(value);
65
- continue;
56
+ // AUTO WRAPPERS
57
+ const detected = (0, detect_wrappers_1.detectWrappers)(source);
58
+ // function wrappers
59
+ for (const fn of detected.functions) {
60
+ if (!config.functionWrappers?.some((w) => w.name === fn.name)) {
61
+ config.functionWrappers.push(fn);
62
+ console.log(chalk_1.default.green(`Detected function wrapper: ${fn.name}`));
63
+ }
66
64
  }
67
- // already asked
68
- if (asked.has(value)) {
69
- continue;
65
+ // component wrappers
66
+ for (const comp of detected.components) {
67
+ if (!config.wrappers?.some((w) => w.name === comp.name)) {
68
+ config.wrappers.push(comp);
69
+ console.log(chalk_1.default.green(`Detected component wrapper: ${comp.name} (prop: ${comp.prop})`));
70
+ }
70
71
  }
71
- // ask user
72
- console.log(chalk_1.default.yellow("\nDynamic event detected:"));
73
- console.log(chalk_1.default.gray(value));
74
- const { name } = await inquirer_1.default.prompt([
75
- {
76
- type: "input",
77
- name: "name",
78
- message: "Enter event name (or type 'skip'):"
72
+ // SCAN EVENTS
73
+ const found = [
74
+ ...(0, track_1.scanTrack)(source, config, aliases),
75
+ ...(0, function_wrappers_1.scanFunctionWrappers)(source, config.functionWrappers ?? [], aliases),
76
+ ...(0, component_wrappers_1.scanComponentWrappers)(source, config.wrappers ?? [], aliases)
77
+ ];
78
+ for (const event of found) {
79
+ const { value, dynamic } = event;
80
+ if (!value)
81
+ continue;
82
+ // skip explicit ignore
83
+ if (value in aliases && aliases[value] === "__skip__") {
84
+ continue;
85
+ }
86
+ // resolve alias
87
+ if (value in aliases) {
88
+ events.add(aliases[value]);
89
+ continue;
79
90
  }
80
- ]);
81
- const normalized = name?.trim();
82
- asked.add(value);
83
- if (!normalized || normalized === "skip") {
84
- continue;
91
+ // static
92
+ if (!dynamic) {
93
+ events.add(value);
94
+ continue;
95
+ }
96
+ // already handled
97
+ if (asked.has(value))
98
+ continue;
99
+ // ASK ONLY IF NEEDED
100
+ console.log(chalk_1.default.yellow("\nDynamic event detected:"));
101
+ console.log(chalk_1.default.gray(value));
102
+ const { name } = await inquirer_1.default.prompt([
103
+ {
104
+ type: "input",
105
+ name: "name",
106
+ message: "Enter event name (or type 'skip'):"
107
+ }
108
+ ]);
109
+ const normalized = name?.trim();
110
+ asked.add(value);
111
+ if (!normalized || normalized === "skip") {
112
+ continue;
113
+ }
114
+ // save alias
115
+ aliases[value] = normalized;
116
+ console.log(chalk_1.default.green(`Saved alias: ${value} → ${normalized}`));
117
+ events.add(normalized);
85
118
  }
86
- // save alias
87
- aliases[value] = normalized;
88
- events.add(normalized);
119
+ // free memory
120
+ project.removeSourceFile(source);
121
+ }
122
+ catch {
123
+ console.log(chalk_1.default.gray(`Skipped file: ${file}`));
89
124
  }
90
125
  }
91
- config.aliases = aliases;
92
- const list = [...events].sort();
93
- config.events = list;
126
+ // SAVE CONFIG
127
+ config.aliases = Object.fromEntries(Object.entries(aliases).sort());
128
+ // MERGE
129
+ const merged = new Set([
130
+ ...config.events,
131
+ ...events
132
+ ]);
133
+ config.events = [...merged].sort();
94
134
  await (0, config_1.saveConfig)(config);
95
- console.log(chalk_1.default.green(`Found ${list.length} events`));
135
+ console.log(chalk_1.default.green(`\nFound ${config.events.length} events`));
96
136
  }