@eventra_dev/eventra-cli 0.1.2 → 0.1.4

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,4 +1,3 @@
1
-
2
1
  <p align="center">
3
2
  <img src="./assets/eventra-icon-animated.svg" width="120">
4
3
  </p>
@@ -49,13 +48,54 @@ Creates `eventra.json` configuration file.
49
48
 
50
49
  Scans your project and discovers events automatically.
51
50
 
52
- Supports:
51
+ ### Supports:
53
52
 
54
53
  - track("event")
55
54
  - analytics.track("event")
56
- - function wrappers
57
- - component wrappers
58
- - aliases
55
+
56
+ - Variables
57
+ ```ts
58
+ const EVENT = "click"
59
+ track(EVENT)
60
+ ```
61
+
62
+ - Enums
63
+ ```ts
64
+ track(EVENTS.CLICK)
65
+ ```
66
+
67
+ - Template strings
68
+ ```ts
69
+ track(`click_${type}`)
70
+ ```
71
+
72
+ - Conditionals
73
+ ```ts
74
+ track(condition ? "a" : "b")
75
+ ```
76
+
77
+ - Object format
78
+ ```ts
79
+ track({ event: "click" })
80
+ ```
81
+
82
+ - Function wrappers
83
+ ```ts
84
+ const trackFeature = (name: string) => {
85
+ track(name)
86
+ }
87
+
88
+ trackFeature("click")
89
+ ```
90
+
91
+ - Multi-argument wrappers
92
+ ```ts
93
+ trackFeature("test", "click")
94
+ ```
95
+
96
+ - Cross-file tracking
97
+
98
+ - Vue / Svelte / Astro support
59
99
 
60
100
  ---
61
101
 
@@ -70,34 +110,19 @@ eventra check
70
110
  ### Detects:
71
111
 
72
112
  - New events
73
- - Unresolved dynamic events
74
-
75
- ### Output example:
76
-
77
- ```
78
- New events:
79
- + user_click (src/Button.tsx:42)
80
-
81
- Dynamic:
82
- - eventName (src/App.tsx:10)
83
- ```
113
+ - Removed events
114
+ - Dynamic values
84
115
 
85
116
  ---
86
117
 
87
118
  ## eventra check --fix
88
119
 
89
- Interactive auto-fix mode.
120
+ Auto-fix mode.
90
121
 
91
122
  ```bash
92
123
  eventra check --fix
93
124
  ```
94
125
 
95
- ### What it does:
96
-
97
- - Adds new events to config
98
- - Resolves dynamic events via alias
99
- - Allows skipping dynamic values
100
-
101
126
  ---
102
127
 
103
128
  ## eventra watch
@@ -108,13 +133,6 @@ Real-time event detection.
108
133
  eventra watch
109
134
  ```
110
135
 
111
- ### Features:
112
-
113
- - Detects new events instantly
114
- - Debounced updates
115
- - No duplicates
116
- - Ignores temp files
117
-
118
136
  ---
119
137
 
120
138
  ## eventra send
@@ -129,8 +147,6 @@ eventra send
129
147
 
130
148
  # Configuration
131
149
 
132
- Example `eventra.json`:
133
-
134
150
  ```json
135
151
  {
136
152
  "apiKey": "",
@@ -147,53 +163,12 @@ Example `eventra.json`:
147
163
 
148
164
  ---
149
165
 
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
-
186
166
  # Supported Frameworks
187
167
 
188
- Frontend:
189
-
190
168
  - React
191
169
  - Vue
192
170
  - Svelte
193
171
  - Astro
194
-
195
- Backend:
196
-
197
172
  - Node.js
198
173
  - Express
199
174
  - NestJS
@@ -27,26 +27,37 @@ async function check({ fix = false }) {
27
27
  const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
28
28
  engine.scanFile(virtualFile, content);
29
29
  }
30
- catch {
31
- console.log(chalk_1.default.gray(`Skipped: ${file}`));
30
+ catch (err) {
31
+ console.log(chalk_1.default.yellow(`Skipped: ${file}`));
32
+ if (err instanceof Error) {
33
+ console.log(chalk_1.default.gray(err.message));
34
+ }
32
35
  }
33
36
  }
34
37
  const found = new Set(engine.getAllEvents());
35
38
  const known = new Set(config.events ?? []);
36
39
  const newEvents = [...found].filter(e => !known.has(e));
40
+ const removed = [...known].filter(e => !found.has(e));
37
41
  if (fix) {
42
+ const merged = new Set(config.events ?? []);
38
43
  newEvents.forEach(e => {
39
- config.events.push(e);
44
+ merged.add(e);
40
45
  console.log(chalk_1.default.green(`+ ${e}`));
41
46
  });
42
- config.events = [...new Set(config.events)].sort();
47
+ config.events = [...merged].sort();
43
48
  await (0, config_1.saveConfig)(config);
44
49
  console.log(chalk_1.default.green("\nFixed"));
45
50
  return;
46
51
  }
47
- if (newEvents.length) {
48
- console.log(chalk_1.default.red("\nNew events:"));
49
- newEvents.forEach(e => console.log(chalk_1.default.red(`+ ${e}`)));
52
+ if (newEvents.length || removed.length) {
53
+ if (newEvents.length) {
54
+ console.log(chalk_1.default.red("\nNew events:"));
55
+ newEvents.forEach(e => console.log(chalk_1.default.red(`+ ${e}`)));
56
+ }
57
+ if (removed.length) {
58
+ console.log(chalk_1.default.yellow("\nRemoved events:"));
59
+ removed.forEach(e => console.log(chalk_1.default.yellow(`- ${e}`)));
60
+ }
50
61
  process.exit(1);
51
62
  }
52
63
  console.log(chalk_1.default.green("\nAll good"));
@@ -47,6 +47,8 @@ async function send() {
47
47
  }
48
48
  console.log("");
49
49
  console.log(chalk_1.default.blue(`Sending ${config.events.length} events...`));
50
+ const controller = new AbortController();
51
+ const timeout = setTimeout(() => controller.abort(), 10000);
50
52
  try {
51
53
  const res = await fetch(endpoint, {
52
54
  method: "POST",
@@ -61,7 +63,8 @@ async function send() {
61
63
  version: CLI_VERSION,
62
64
  runtime: "node"
63
65
  }
64
- })
66
+ }),
67
+ signal: controller.signal
65
68
  });
66
69
  if (!res.ok) {
67
70
  console.log(chalk_1.default.red(`Request failed (${res.status})`));
@@ -99,4 +102,7 @@ async function send() {
99
102
  console.log(chalk_1.default.gray(err.message));
100
103
  }
101
104
  }
105
+ finally {
106
+ clearTimeout(timeout);
107
+ }
102
108
  }
@@ -29,9 +29,11 @@ async function sync() {
29
29
  const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
30
30
  engine.scanFile(virtualFile, content);
31
31
  }
32
- catch (e) {
33
- console.log(chalk_1.default.red(`ERROR: ${file}`));
34
- console.error(e);
32
+ catch (err) {
33
+ console.log(chalk_1.default.yellow(`Skipped: ${file}`));
34
+ if (err instanceof Error) {
35
+ console.log(chalk_1.default.gray(err.message));
36
+ }
35
37
  }
36
38
  }
37
39
  const events = engine.getAllEvents().sort();
@@ -10,28 +10,7 @@ const promises_1 = __importDefault(require("fs/promises"));
10
10
  const fast_glob_1 = __importDefault(require("fast-glob"));
11
11
  const config_1 = require("../utils/config");
12
12
  const engine_1 = require("../engine/engine");
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
- // PREPROCESS (CONTENT)
18
- function preprocess(file, content) {
19
- const parser = (0, router_1.detectParser)(file);
20
- if (parser === "vue")
21
- return (0, vue_1.parseVue)(content);
22
- if (parser === "svelte")
23
- return (0, svelte_1.parseSvelte)(content);
24
- if (parser === "astro")
25
- return (0, astro_1.parseAstro)(content);
26
- return content;
27
- }
28
- // VIRTUAL FILE (PATH)
29
- function toVirtualFile(file) {
30
- return file.endsWith(".vue")
31
- ? file + ".tsx"
32
- : file;
33
- }
34
- // WATCH
13
+ const processFile_1 = require("../utils/processFile");
35
14
  async function watch() {
36
15
  const config = await (0, config_1.loadConfig)();
37
16
  if (!config)
@@ -40,17 +19,11 @@ async function watch() {
40
19
  const files = await (0, fast_glob_1.default)(config.sync.include, {
41
20
  ignore: config.sync.exclude,
42
21
  });
43
- if (!files.length) {
44
- console.log(chalk_1.default.yellow("No files found"));
45
- return;
46
- }
47
22
  const engine = new engine_1.EventraEngine(process.cwd());
48
- // INITIAL SCAN
49
23
  for (const file of files) {
50
24
  try {
51
25
  const raw = await promises_1.default.readFile(file, "utf-8");
52
- const content = preprocess(file, raw);
53
- const virtualFile = toVirtualFile(file);
26
+ const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
54
27
  engine.scanFile(virtualFile, content);
55
28
  }
56
29
  catch {
@@ -60,9 +33,9 @@ async function watch() {
60
33
  config.events = engine.getAllEvents().sort();
61
34
  await (0, config_1.saveConfig)(config);
62
35
  console.log(chalk_1.default.gray(`Initial: ${config.events.length} events\n`));
63
- // STATE
64
36
  let locked = false;
65
37
  let queued = new Set();
38
+ let timer = null;
66
39
  const run = async () => {
67
40
  if (locked)
68
41
  return;
@@ -72,8 +45,7 @@ async function watch() {
72
45
  for (const file of batch) {
73
46
  try {
74
47
  const raw = await promises_1.default.readFile(file, "utf-8");
75
- const content = preprocess(file, raw);
76
- const virtualFile = toVirtualFile(file);
48
+ const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
77
49
  engine.updateFile(virtualFile, content);
78
50
  }
79
51
  catch {
@@ -94,39 +66,25 @@ async function watch() {
94
66
  };
95
67
  const schedule = (file) => {
96
68
  queued.add(file);
97
- setTimeout(run, 50);
69
+ if (timer)
70
+ clearTimeout(timer);
71
+ timer = setTimeout(run, 100);
98
72
  };
99
- // WATCHER
100
73
  const watcher = chokidar_1.default.watch(config.sync.include, {
101
74
  ignored: config.sync.exclude,
102
75
  ignoreInitial: true,
103
- awaitWriteFinish: {
104
- stabilityThreshold: 50,
105
- pollInterval: 10,
106
- },
107
76
  });
108
77
  watcher
109
- .on("add", (file) => {
110
- console.log(chalk_1.default.gray(`add: ${file}`));
111
- schedule(file);
112
- })
113
- .on("change", (file) => {
114
- console.log(chalk_1.default.gray(`change: ${file}`));
115
- schedule(file);
116
- })
78
+ .on("add", schedule)
79
+ .on("change", schedule)
117
80
  .on("unlink", async (file) => {
118
81
  console.log(chalk_1.default.gray(`remove: ${file}`));
119
- const virtualFile = toVirtualFile(file);
120
- engine.removeFile?.(virtualFile);
82
+ const { virtualFile } = (0, processFile_1.processFile)(file, "");
83
+ engine.removeFile(virtualFile);
121
84
  const next = engine.getAllEvents();
122
- const removed = config.events.filter(e => !next.includes(e));
123
- if (removed.length) {
124
- removed.forEach(e => console.log(chalk_1.default.red(`- ${e}`)));
125
- config.events = next.sort();
126
- await (0, config_1.saveConfig)(config);
127
- }
85
+ config.events = next.sort();
86
+ await (0, config_1.saveConfig)(config);
128
87
  });
129
- // EXIT
130
88
  process.on("SIGINT", async () => {
131
89
  console.log(chalk_1.default.yellow("\nStopping watcher..."));
132
90
  await watcher.close();
@@ -6,29 +6,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveFunctionFromCall = resolveFunctionFromCall;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  function resolveFunctionFromCall(expr, checker) {
9
- let symbol = checker.getSymbolAtLocation(expr);
10
- if (!symbol && typescript_1.default.isPropertyAccessExpression(expr)) {
11
- symbol = checker.getSymbolAtLocation(expr.name);
12
- }
13
- if (!symbol)
14
- return null;
15
- if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
16
- symbol = checker.getAliasedSymbol(symbol);
17
- }
18
- for (const decl of symbol.getDeclarations() ?? []) {
19
- if (typescript_1.default.isFunctionDeclaration(decl) ||
20
- typescript_1.default.isMethodDeclaration(decl) ||
21
- typescript_1.default.isFunctionExpression(decl) ||
22
- typescript_1.default.isArrowFunction(decl)) {
9
+ const type = checker.getTypeAtLocation(expr);
10
+ const signatures = type.getCallSignatures();
11
+ for (const sig of signatures) {
12
+ const decl = sig.getDeclaration();
13
+ if (decl &&
14
+ (typescript_1.default.isFunctionDeclaration(decl) ||
15
+ typescript_1.default.isMethodDeclaration(decl) ||
16
+ typescript_1.default.isFunctionExpression(decl) ||
17
+ typescript_1.default.isArrowFunction(decl))) {
23
18
  return decl;
24
19
  }
25
- if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
26
- const init = decl.initializer;
27
- if (typescript_1.default.isArrowFunction(init) ||
28
- typescript_1.default.isFunctionExpression(init)) {
29
- return init;
30
- }
31
- }
32
20
  }
33
21
  return null;
34
22
  }
@@ -30,7 +30,17 @@ class EventraEngine {
30
30
  return events;
31
31
  }
32
32
  updateFile(file, content) {
33
- return this.scanFile(file, content);
33
+ const events = this.scanFile(file, content);
34
+ const dependents = this.graph.getDependents(file);
35
+ for (const dep of dependents) {
36
+ const source = this.ts.getSourceFile(dep);
37
+ const checker = this.ts.getChecker();
38
+ if (!source || !checker)
39
+ continue;
40
+ const ev = (0, scanner_1.scanSource)(source, checker);
41
+ this.fileEvents.set(dep, ev);
42
+ }
43
+ return events;
34
44
  }
35
45
  getAllEvents() {
36
46
  const all = new Set();
@@ -42,6 +52,7 @@ class EventraEngine {
42
52
  removeFile(file) {
43
53
  this.fileEvents.delete(file);
44
54
  this.fileHash.delete(file);
55
+ this.graph.remove(file);
45
56
  }
46
57
  }
47
58
  exports.EventraEngine = EventraEngine;
@@ -0,0 +1,45 @@
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.findTrackParamIndex = findTrackParamIndex;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ function findTrackParamIndex(fn) {
9
+ const paramIndex = new Map();
10
+ fn.parameters.forEach((p, i) => {
11
+ if (typescript_1.default.isIdentifier(p.name)) {
12
+ paramIndex.set(p.name.text, i);
13
+ }
14
+ });
15
+ let result = null;
16
+ function visit(node) {
17
+ if (typescript_1.default.isCallExpression(node)) {
18
+ const name = getCallName(node.expression);
19
+ if (/(track|event|capture|send)/i.test(name)) {
20
+ for (const arg of node.arguments) {
21
+ if (typescript_1.default.isIdentifier(arg)) {
22
+ const idx = paramIndex.get(arg.text);
23
+ if (idx !== undefined) {
24
+ result = idx;
25
+ return;
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ typescript_1.default.forEachChild(node, visit);
32
+ }
33
+ if (fn.body) {
34
+ visit(fn.body);
35
+ }
36
+ return result;
37
+ }
38
+ function getCallName(expr) {
39
+ if (typescript_1.default.isIdentifier(expr))
40
+ return expr.text;
41
+ if (typescript_1.default.isPropertyAccessExpression(expr)) {
42
+ return expr.name.text;
43
+ }
44
+ return "";
45
+ }
@@ -5,21 +5,89 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DependencyGraph = void 0;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
8
10
  class DependencyGraph {
9
11
  reverse = new Map();
12
+ exports = new Map();
13
+ normalize(file) {
14
+ return path_1.default.resolve(file);
15
+ }
16
+ resolveImport(from, module) {
17
+ if (!module.startsWith("."))
18
+ return null;
19
+ const base = path_1.default.resolve(path_1.default.dirname(from), module);
20
+ const tryFiles = [
21
+ base,
22
+ base + ".ts",
23
+ base + ".tsx",
24
+ base + ".js",
25
+ base + ".jsx",
26
+ path_1.default.join(base, "index.ts"),
27
+ path_1.default.join(base, "index.tsx"),
28
+ path_1.default.join(base, "index.js"),
29
+ ];
30
+ for (const f of tryFiles) {
31
+ if (fs_1.default.existsSync(f)) {
32
+ return this.normalize(f);
33
+ }
34
+ }
35
+ return this.normalize(base);
36
+ }
10
37
  update(file, source) {
38
+ const normalizedFile = this.normalize(file);
39
+ this.exports.set(normalizedFile, new Set());
11
40
  for (const stmt of source.statements) {
12
- if (!typescript_1.default.isImportDeclaration(stmt))
13
- continue;
14
- const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
15
- if (!this.reverse.has(module)) {
16
- this.reverse.set(module, new Set());
41
+ // IMPORT
42
+ if (typescript_1.default.isImportDeclaration(stmt)) {
43
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
44
+ const resolved = this.resolveImport(normalizedFile, module);
45
+ if (!resolved)
46
+ continue;
47
+ if (!this.reverse.has(resolved)) {
48
+ this.reverse.set(resolved, new Set());
49
+ }
50
+ this.reverse.get(resolved).add(normalizedFile);
51
+ }
52
+ // EXPORT
53
+ if (typescript_1.default.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
54
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
55
+ const resolved = this.resolveImport(normalizedFile, module);
56
+ if (!resolved)
57
+ continue;
58
+ if (!this.exports.has(normalizedFile)) {
59
+ this.exports.set(normalizedFile, new Set());
60
+ }
61
+ this.exports.get(normalizedFile).add(resolved);
62
+ if (!this.reverse.has(resolved)) {
63
+ this.reverse.set(resolved, new Set());
64
+ }
65
+ this.reverse.get(resolved).add(normalizedFile);
17
66
  }
18
- this.reverse.get(module).add(file);
19
67
  }
20
68
  }
69
+ getAllRelated(file, visited = new Set()) {
70
+ const normalized = this.normalize(file);
71
+ if (visited.has(normalized))
72
+ return visited;
73
+ visited.add(normalized);
74
+ const deps = this.exports.get(normalized);
75
+ if (!deps)
76
+ return visited;
77
+ for (const dep of deps) {
78
+ this.getAllRelated(dep, visited);
79
+ }
80
+ return visited;
81
+ }
21
82
  getDependents(file) {
22
- return [...(this.reverse.get(file) ?? [])];
83
+ return [...(this.reverse.get(this.normalize(file)) ?? [])];
84
+ }
85
+ remove(file) {
86
+ const normalized = this.normalize(file);
87
+ this.exports.delete(normalized);
88
+ for (const set of this.reverse.values()) {
89
+ set.delete(normalized);
90
+ }
23
91
  }
24
92
  }
25
93
  exports.DependencyGraph = DependencyGraph;
@@ -9,11 +9,29 @@ const fs_1 = __importDefault(require("fs"));
9
9
  class TSService {
10
10
  root;
11
11
  files = new Map();
12
+ fileNames = new Set();
12
13
  service;
13
14
  constructor(root) {
14
15
  this.root = root;
16
+ const configPath = typescript_1.default.findConfigFile(root, typescript_1.default.sys.fileExists, "tsconfig.json");
17
+ let compilerOptions = {
18
+ allowJs: true,
19
+ jsx: typescript_1.default.JsxEmit.Preserve,
20
+ moduleResolution: typescript_1.default.ModuleResolutionKind.Node16,
21
+ target: typescript_1.default.ScriptTarget.ESNext,
22
+ esModuleInterop: true,
23
+ allowSyntheticDefaultImports: true,
24
+ };
25
+ if (configPath) {
26
+ const configFile = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
27
+ const parsed = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, root);
28
+ compilerOptions = {
29
+ ...compilerOptions,
30
+ ...parsed.options,
31
+ };
32
+ }
15
33
  const host = {
16
- getScriptFileNames: () => [...this.files.keys()],
34
+ getScriptFileNames: () => [...this.fileNames],
17
35
  getScriptVersion: (file) => this.files.get(file)?.version.toString() ?? "0",
18
36
  getScriptSnapshot: (file) => {
19
37
  const f = this.files.get(file);
@@ -26,24 +44,11 @@ class TSService {
26
44
  return undefined;
27
45
  },
28
46
  getCurrentDirectory: () => this.root,
29
- getCompilationSettings: () => ({
30
- allowJs: true,
31
- jsx: typescript_1.default.JsxEmit.Preserve,
32
- moduleResolution: typescript_1.default.ModuleResolutionKind.Node16,
33
- target: typescript_1.default.ScriptTarget.ESNext,
34
- esModuleInterop: true,
35
- allowSyntheticDefaultImports: true,
36
- }),
47
+ getCompilationSettings: () => compilerOptions,
37
48
  getDefaultLibFileName: (opts) => typescript_1.default.getDefaultLibFilePath(opts),
38
- readFile: (fileName, encoding) => {
39
- try {
40
- return fs_1.default.readFileSync(fileName, encoding);
41
- }
42
- catch (err) {
43
- return undefined;
44
- }
45
- },
46
- fileExists: fs_1.default.existsSync,
49
+ readFile: typescript_1.default.sys.readFile,
50
+ fileExists: typescript_1.default.sys.fileExists,
51
+ readDirectory: typescript_1.default.sys.readDirectory,
47
52
  };
48
53
  this.service = typescript_1.default.createLanguageService(host);
49
54
  }
@@ -53,6 +58,7 @@ class TSService {
53
58
  content,
54
59
  version: prev ? prev.version + 1 : 1,
55
60
  });
61
+ this.fileNames.add(file);
56
62
  }
57
63
  getProgram() {
58
64
  return this.service.getProgram();