@eventra_dev/eventra-cli 0.1.31 → 0.1.33

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.
@@ -0,0 +1,88 @@
1
+ # Eventra CLI Architecture
2
+
3
+ ## High-Level Flow
4
+
5
+ FILES → parseUniversal → TSService → scanSource → Engine → CLI
6
+
7
+ ---
8
+
9
+ ## Diagram
10
+
11
+ ```
12
+ +-------------------+
13
+ | Source Files |
14
+ +---------+---------+
15
+ |
16
+ v
17
+ +-------------------+
18
+ | parseUniversal |
19
+ | (normalize input) |
20
+ +---------+---------+
21
+ |
22
+ v
23
+ +-------------------+
24
+ | TSService |
25
+ | (AST + Types) |
26
+ +---------+---------+
27
+ |
28
+ v
29
+ +-------------------+
30
+ | scanSource |
31
+ | (core analyzer) |
32
+ +----+------+-------+
33
+ | |
34
+ | |
35
+ v v
36
+ +----------------+----------------+
37
+ | |
38
+ +-------------------+ +-----------------------+
39
+ | Function Wrappers | | Component Wrappers |
40
+ +-------------------+ +-----------------------+
41
+ | |
42
+ +---------------+-----------------+
43
+ |
44
+ v
45
+ +-------------------+
46
+ | resolveNodeValue |
47
+ | (extract values) |
48
+ +---------+---------+
49
+ |
50
+ v
51
+ +-------------------+
52
+ | EventraEngine |
53
+ | (orchestration) |
54
+ +---------+---------+
55
+ |
56
+ v
57
+ +-------------------+
58
+ | CLI (sync/check) |
59
+ +-------------------+
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Modules
65
+
66
+ ### Parser
67
+ Extracts JS from any framework (Vue, Svelte, Astro, HTML).
68
+
69
+ ### TSService
70
+ Provides TypeScript AST and type resolution.
71
+
72
+ ### Scanner
73
+ Finds:
74
+ - track() calls
75
+ - wrappers
76
+ - values
77
+
78
+ ### Engine
79
+ Handles:
80
+ - caching
81
+ - dependency graph
82
+ - incremental updates
83
+
84
+ ---
85
+
86
+ ## Goal
87
+
88
+ Static analytics extraction without runtime.
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveFunctionFromCall = resolveFunctionFromCall;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
+ // HELPERS
8
9
  function isFunctionLike(node) {
9
10
  return (typescript_1.default.isFunctionDeclaration(node) ||
10
11
  typescript_1.default.isMethodDeclaration(node) ||
@@ -12,32 +13,33 @@ function isFunctionLike(node) {
12
13
  typescript_1.default.isFunctionExpression(node) ||
13
14
  typescript_1.default.isArrowFunction(node));
14
15
  }
16
+ // symbol → function
15
17
  function resolveFromSymbol(symbol, checker, visited = new Set(), depth = 0) {
18
+ if (!symbol)
19
+ return null;
16
20
  if (visited.has(symbol))
17
21
  return null;
18
- if (depth > 5)
22
+ if (depth > 10)
19
23
  return null;
20
24
  visited.add(symbol);
21
- // unwrap alias
25
+ // Alias unwrap (import/export)
22
26
  if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
23
- const aliased = checker.getAliasedSymbol(symbol);
24
- return resolveFromSymbol(aliased, checker, visited, depth + 1);
25
- }
26
- // re-export support
27
- if (symbol.exports) {
28
- for (const exp of symbol.exports.values()) {
29
- const resolved = resolveFromSymbol(exp, checker, visited, depth + 1);
30
- if (resolved)
31
- return resolved;
27
+ try {
28
+ const aliased = checker.getAliasedSymbol(symbol);
29
+ return resolveFromSymbol(aliased, checker, visited, depth + 1);
32
30
  }
31
+ catch { }
33
32
  }
33
+ // Declarations
34
34
  const decls = symbol.getDeclarations() ?? [];
35
35
  for (const decl of decls) {
36
- // direct function
36
+ /**
37
+ * direct function
38
+ */
37
39
  if (isFunctionLike(decl)) {
38
40
  return decl;
39
41
  }
40
- // VARIABLE → FUNCTION
42
+ // VARIABLE → FUNCTION const fn = () => {}
41
43
  if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
42
44
  if (isFunctionLike(decl.initializer)) {
43
45
  return decl.initializer;
@@ -49,17 +51,18 @@ function resolveFromSymbol(symbol, checker, visited = new Set(), depth = 0) {
49
51
  return resolved;
50
52
  }
51
53
  }
52
- // MethodSignature (interface / type)
54
+ // CLASS METHOD
55
+ if (typescript_1.default.isMethodDeclaration(decl)) {
56
+ return decl;
57
+ }
58
+ // interface / type method
53
59
  if (typescript_1.default.isMethodSignature(decl)) {
54
60
  const parent = decl.parent;
55
61
  if (typescript_1.default.isInterfaceDeclaration(parent) ||
56
62
  typescript_1.default.isTypeLiteralNode(parent)) {
57
- const name = decl.name.getText();
58
63
  const type = checker.getTypeAtLocation(parent);
59
64
  const props = type.getProperties();
60
65
  for (const prop of props) {
61
- if (prop.name !== name)
62
- continue;
63
66
  const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
64
67
  if (resolved)
65
68
  return resolved;
@@ -67,21 +70,34 @@ function resolveFromSymbol(symbol, checker, visited = new Set(), depth = 0) {
67
70
  }
68
71
  }
69
72
  // fallback recursion
70
- const s = checker.getSymbolAtLocation(decl);
71
- if (s && s !== symbol) {
72
- const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
73
+ const inner = checker.getSymbolAtLocation(decl);
74
+ if (inner && inner !== symbol) {
75
+ const resolved = resolveFromSymbol(inner, checker, visited, depth + 1);
76
+ if (resolved)
77
+ return resolved;
78
+ }
79
+ }
80
+ // Exports (re-export)
81
+ const exports = symbol.exports;
82
+ if (exports) {
83
+ for (const exp of exports.values()) {
84
+ const resolved = resolveFromSymbol(exp, checker, visited, depth + 1);
73
85
  if (resolved)
74
86
  return resolved;
75
87
  }
76
88
  }
77
89
  return null;
78
90
  }
91
+ // MAIN FUNCTION
79
92
  function resolveFunctionFromCall(expr, checker) {
80
- // tracker.track
93
+ // obj.method()
81
94
  if (typescript_1.default.isPropertyAccessExpression(expr)) {
82
95
  let symbol = checker.getSymbolAtLocation(expr.name);
83
96
  if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
84
- symbol = checker.getAliasedSymbol(symbol);
97
+ try {
98
+ symbol = checker.getAliasedSymbol(symbol);
99
+ }
100
+ catch { }
85
101
  }
86
102
  if (symbol) {
87
103
  const resolved = resolveFromSymbol(symbol, checker);
@@ -89,7 +105,7 @@ function resolveFunctionFromCall(expr, checker) {
89
105
  return resolved;
90
106
  }
91
107
  }
92
- // tracker["track"]
108
+ // obj["method"]()
93
109
  if (typescript_1.default.isElementAccessExpression(expr)) {
94
110
  const symbol = checker.getSymbolAtLocation(expr.argumentExpression);
95
111
  if (symbol) {
@@ -98,7 +114,7 @@ function resolveFunctionFromCall(expr, checker) {
98
114
  return resolved;
99
115
  }
100
116
  }
101
- // normal call signatures
117
+ // Call signatures
102
118
  const type = checker.getTypeAtLocation(expr);
103
119
  const signatures = type.getCallSignatures();
104
120
  for (const sig of signatures) {
@@ -117,7 +133,22 @@ function resolveFunctionFromCall(expr, checker) {
117
133
  }
118
134
  }
119
135
  }
120
- // fallback
136
+ // Identifier fallback
137
+ if (typescript_1.default.isIdentifier(expr)) {
138
+ let symbol = checker.getSymbolAtLocation(expr);
139
+ if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
140
+ try {
141
+ symbol = checker.getAliasedSymbol(symbol);
142
+ }
143
+ catch { }
144
+ }
145
+ if (symbol) {
146
+ const resolved = resolveFromSymbol(symbol, checker);
147
+ if (resolved)
148
+ return resolved;
149
+ }
150
+ }
151
+ // LAST fallback
121
152
  const symbol = checker.getSymbolAtLocation(expr);
122
153
  if (symbol) {
123
154
  return resolveFromSymbol(symbol, checker);
@@ -23,6 +23,7 @@ class TSService {
23
23
  const configPath = typescript_1.default.findConfigFile(root, typescript_1.default.sys.fileExists, "tsconfig.json");
24
24
  let compilerOptions = {
25
25
  allowJs: true,
26
+ allowNonTsExtensions: true,
26
27
  jsx: typescript_1.default.JsxEmit.React,
27
28
  jsxImportSource: "react",
28
29
  moduleResolution: typescript_1.default.ModuleResolutionKind.Node16,
@@ -49,7 +50,12 @@ class TSService {
49
50
  this.baseUrl = baseUrl;
50
51
  this.paths = paths;
51
52
  const host = {
52
- getScriptFileNames: () => Array.from(this.fileNames),
53
+ getScriptFileNames: () => {
54
+ if (this.fileNames.size === 0) {
55
+ return typescript_1.default.sys.readDirectory(this.root, [".ts", ".tsx", ".js", ".jsx"], undefined, undefined);
56
+ }
57
+ return Array.from(this.fileNames);
58
+ },
53
59
  getScriptVersion: (file) => {
54
60
  file = this.normalize(file);
55
61
  return this.files.get(file)?.version.toString() ?? "0";
@@ -61,6 +67,20 @@ class TSService {
61
67
  this.touch(file);
62
68
  return typescript_1.default.ScriptSnapshot.fromString(f.content);
63
69
  }
70
+ if (file.endsWith(".tsx")) {
71
+ const original = file.replace(/\.tsx$/, "");
72
+ if (fs_1.default.existsSync(original)) {
73
+ const content = fs_1.default.readFileSync(original, "utf-8");
74
+ this.files.set(file, {
75
+ content,
76
+ version: 1,
77
+ });
78
+ this.fileNames.add(file);
79
+ this.touch(file);
80
+ this.evictIfNeeded();
81
+ return typescript_1.default.ScriptSnapshot.fromString(content);
82
+ }
83
+ }
64
84
  try {
65
85
  if (fs_1.default.existsSync(file)) {
66
86
  const content = fs_1.default.readFileSync(file, "utf-8");
@@ -75,14 +95,19 @@ class TSService {
75
95
  }
76
96
  }
77
97
  catch { }
78
- // fallback
79
98
  return typescript_1.default.ScriptSnapshot.fromString("");
80
99
  },
81
100
  getCurrentDirectory: () => this.root,
82
101
  getCompilationSettings: () => this.compilerOptions,
83
102
  getDefaultLibFileName: (opts) => typescript_1.default.getDefaultLibFilePath(opts),
84
- fileExists: typescript_1.default.sys.fileExists,
85
- readFile: typescript_1.default.sys.readFile,
103
+ fileExists: (file) => {
104
+ file = this.normalize(file);
105
+ return this.files.has(file) || typescript_1.default.sys.fileExists(file);
106
+ },
107
+ readFile: (file) => {
108
+ file = this.normalize(file);
109
+ return this.files.get(file)?.content ?? typescript_1.default.sys.readFile(file);
110
+ },
86
111
  readDirectory: typescript_1.default.sys.readDirectory,
87
112
  };
88
113
  this.service = typescript_1.default.createLanguageService(host);
@@ -98,24 +123,24 @@ class TSService {
98
123
  evictIfNeeded() {
99
124
  if (this.fileNames.size <= this.MAX_FILES)
100
125
  return;
101
- let oldestFile = null;
126
+ let oldest = null;
102
127
  let oldestTick = Infinity;
103
128
  for (const f of this.fileNames) {
104
129
  const t = this.usage.get(f) ?? 0;
105
130
  if (t < oldestTick) {
106
131
  oldestTick = t;
107
- oldestFile = f;
132
+ oldest = f;
108
133
  }
109
134
  }
110
- if (oldestFile) {
111
- this.files.delete(oldestFile);
112
- this.fileNames.delete(oldestFile);
113
- this.usage.delete(oldestFile);
135
+ if (oldest) {
136
+ this.files.delete(oldest);
137
+ this.fileNames.delete(oldest);
138
+ this.usage.delete(oldest);
114
139
  }
115
140
  }
116
141
  ensureProgram() {
117
142
  try {
118
- this.service.getProgram();
143
+ this.service.getProgram()?.getTypeChecker();
119
144
  }
120
145
  catch { }
121
146
  }
@@ -9,7 +9,6 @@ const resolver_1 = require("./resolver");
9
9
  const callResolver_1 = require("./callResolver");
10
10
  const functionAnalyzer_1 = require("./functionAnalyzer");
11
11
  const exportResolver_1 = require("./exportResolver");
12
- const boundary_1 = require("./boundary");
13
12
  // UTILS
14
13
  function getJsxAttrName(name) {
15
14
  if (typescript_1.default.isIdentifier(name))
@@ -136,67 +135,58 @@ function findTrackedPropName(fn, checker) {
136
135
  return result;
137
136
  }
138
137
  // MAIN SCANNER
139
- function scanSource(source, checker, config, visited = new Set(), inheritedParamMap, wrapperCache) {
140
- wrapperCache = wrapperCache ?? new Map();
138
+ function scanSource(source, checker, config, visited = new Set(), inheritedParamMap, wrapperCache = new Map()) {
141
139
  const events = new Set();
142
140
  const detectedFunctionWrappers = new Set();
143
141
  const detectedComponentWrappers = new Map();
144
- if (visited.size > 200 || visited.has(source.fileName)) {
142
+ if (visited.has(source.fileName) || visited.size > 200) {
145
143
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
146
144
  }
147
145
  visited.add(source.fileName);
148
146
  function visit(node) {
149
147
  // JSX WRAPPERS
150
- if (typescript_1.default.isJsxSelfClosingElement(node) ||
151
- typescript_1.default.isJsxOpeningElement(node)) {
148
+ if (typescript_1.default.isJsxSelfClosingElement(node) || typescript_1.default.isJsxOpeningElement(node)) {
152
149
  const tagName = getJsxTagName(node.tagName);
153
150
  // skip html
154
- if (!tagName || tagName[0] === tagName[0].toLowerCase()) {
151
+ if (tagName[0] === tagName[0].toLowerCase()) {
155
152
  typescript_1.default.forEachChild(node, visit);
156
153
  return;
157
154
  }
158
- let wrapper = config.wrappers.find((w) => w.name === tagName);
155
+ let wrapper = config.wrappers.find(w => w.name === tagName);
156
+ // AUTO DETECT
159
157
  if (!wrapper) {
160
- let symbol = checker.getSymbolAtLocation(node.tagName);
161
- let cacheKey = tagName;
162
- if (symbol) {
163
- if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
164
- symbol = checker.getAliasedSymbol(symbol);
165
- }
166
- symbol =
167
- (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
168
- cacheKey = checker.getFullyQualifiedName(symbol);
169
- }
170
- if (wrapperCache?.has(cacheKey)) {
171
- const cached = wrapperCache.get(cacheKey);
172
- if (cached) {
158
+ if (wrapperCache.has(tagName)) {
159
+ const cached = wrapperCache.get(tagName);
160
+ if (cached)
173
161
  wrapper = { name: tagName, prop: cached };
174
- }
175
162
  }
176
- else if (symbol) {
177
- let detected = null;
178
- for (const decl of symbol.getDeclarations() ?? []) {
179
- let fn = null;
180
- if (typescript_1.default.isFunctionDeclaration(decl))
181
- fn = decl;
182
- if (typescript_1.default.isVariableDeclaration(decl) &&
183
- decl.initializer &&
184
- (typescript_1.default.isArrowFunction(decl.initializer) ||
185
- typescript_1.default.isFunctionExpression(decl.initializer))) {
186
- fn = decl.initializer;
187
- }
188
- if (!fn)
189
- continue;
190
- const prop = findTrackedPropName(fn, checker);
191
- if (prop) {
192
- detected = prop;
193
- break;
194
- }
163
+ else {
164
+ let symbol = checker.getSymbolAtLocation(node.tagName);
165
+ if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
166
+ symbol = checker.getAliasedSymbol(symbol);
195
167
  }
196
- wrapperCache?.set(cacheKey, detected);
197
- if (detected) {
198
- wrapper = { name: tagName, prop: detected };
199
- detectedComponentWrappers.set(tagName, detected);
168
+ if (symbol) {
169
+ symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
170
+ for (const decl of symbol.getDeclarations() ?? []) {
171
+ let fn = null;
172
+ if (typescript_1.default.isFunctionDeclaration(decl))
173
+ fn = decl;
174
+ if (typescript_1.default.isVariableDeclaration(decl) &&
175
+ decl.initializer &&
176
+ (typescript_1.default.isArrowFunction(decl.initializer) ||
177
+ typescript_1.default.isFunctionExpression(decl.initializer))) {
178
+ fn = decl.initializer;
179
+ }
180
+ if (!fn)
181
+ continue;
182
+ const prop = findTrackedPropName(fn, checker);
183
+ wrapperCache.set(tagName, prop ?? null);
184
+ if (prop) {
185
+ wrapper = { name: tagName, prop };
186
+ detectedComponentWrappers.set(tagName, prop);
187
+ break;
188
+ }
189
+ }
200
190
  }
201
191
  }
202
192
  }
@@ -210,16 +200,16 @@ function scanSource(source, checker, config, visited = new Set(), inheritedParam
210
200
  if (!attr.initializer)
211
201
  continue;
212
202
  if (typescript_1.default.isStringLiteral(attr.initializer)) {
213
- if (isValidEvent(attr.initializer.text)) {
214
- events.add(attr.initializer.text);
215
- }
203
+ const v = attr.initializer.text;
204
+ if (isValidEvent(v))
205
+ events.add(v);
216
206
  }
217
207
  if (typescript_1.default.isJsxExpression(attr.initializer)) {
218
208
  const expr = attr.initializer.expression;
219
209
  if (!expr)
220
210
  continue;
221
211
  const res = (0, resolver_1.resolveNodeValue)(expr, checker, inheritedParamMap);
222
- res.values.forEach((v) => {
212
+ res.values.forEach(v => {
223
213
  if (isValidEvent(v))
224
214
  events.add(v);
225
215
  });
@@ -229,24 +219,39 @@ function scanSource(source, checker, config, visited = new Set(), inheritedParam
229
219
  typescript_1.default.forEachChild(node, visit);
230
220
  return;
231
221
  }
232
- // FUNCTION TRACKING
222
+ // FUNCTION CALLS
233
223
  if (typescript_1.default.isCallExpression(node)) {
234
- const name = getCallName(node.expression);
235
- const isKnownWrapper = config.functionWrappers.some((w) => w.name === name);
224
+ let trackParamIndex = null;
225
+ let isTracking = false;
226
+ const callName = getCallName(node.expression);
227
+ // direct .track()
228
+ if (typescript_1.default.isPropertyAccessExpression(node.expression) &&
229
+ node.expression.name.text === "track") {
230
+ isTracking = true;
231
+ trackParamIndex = 0;
232
+ }
233
+ // config wrapper
234
+ const manualWrapper = config.functionWrappers.find(w => w.name === callName);
235
+ if (manualWrapper) {
236
+ isTracking = true;
237
+ trackParamIndex = 0;
238
+ }
239
+ // resolve function → check body
236
240
  const resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
237
- const isAutoWrapper = !!resolvedFn && (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn) !== null;
238
- const isTracking = name === "track" ||
239
- isKnownWrapper ||
240
- (isAutoWrapper &&
241
- resolvedFn &&
242
- !(0, boundary_1.isExternalFile)(resolvedFn.getSourceFile().fileName));
241
+ if (resolvedFn) {
242
+ const idx = (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn);
243
+ if (idx !== null) {
244
+ isTracking = true;
245
+ trackParamIndex = idx;
246
+ if (!manualWrapper && callName !== "track") {
247
+ detectedFunctionWrappers.add(callName);
248
+ }
249
+ }
250
+ }
243
251
  if (!isTracking) {
244
252
  typescript_1.default.forEachChild(node, visit);
245
253
  return;
246
254
  }
247
- if (!isKnownWrapper && isAutoWrapper && name !== "track") {
248
- detectedFunctionWrappers.add(name);
249
- }
250
255
  const paramMap = new Map(inheritedParamMap ?? []);
251
256
  if (resolvedFn && typescript_1.default.isFunctionLike(resolvedFn)) {
252
257
  resolvedFn.parameters.forEach((param, i) => {
@@ -258,21 +263,19 @@ function scanSource(source, checker, config, visited = new Set(), inheritedParam
258
263
  }
259
264
  });
260
265
  }
261
- if (resolvedFn) {
262
- const idx = (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn);
263
- if (idx !== null && node.arguments[idx]) {
264
- const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
265
- res.values.forEach((v) => {
266
- if (isValidEvent(v))
267
- events.add(v);
268
- });
269
- }
266
+ if (trackParamIndex !== null && node.arguments[trackParamIndex]) {
267
+ const res = (0, resolver_1.resolveNodeValue)(node.arguments[trackParamIndex], checker, paramMap);
268
+ res.values.forEach(v => {
269
+ if (isValidEvent(v))
270
+ events.add(v);
271
+ });
270
272
  }
273
+ // CROSS FILE
271
274
  if (resolvedFn) {
272
275
  const sf = resolvedFn.getSourceFile();
273
276
  if (!visited.has(sf.fileName)) {
274
277
  const res = scanSource(sf, checker, config, visited, paramMap, wrapperCache);
275
- res.events.forEach((e) => events.add(e));
278
+ res.events.forEach(e => events.add(e));
276
279
  }
277
280
  }
278
281
  }