@eventra_dev/eventra-cli 0.1.31 → 0.1.32

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.
@@ -1,5 +1,6 @@
1
1
  import ts from "typescript";
2
2
 
3
+ // HELPERS
3
4
  function isFunctionLike(
4
5
  node: ts.Node
5
6
  ): node is ts.FunctionLikeDeclaration {
@@ -12,40 +13,39 @@ function isFunctionLike(
12
13
  );
13
14
  }
14
15
 
16
+ // symbol → function
15
17
  function resolveFromSymbol(
16
18
  symbol: ts.Symbol,
17
19
  checker: ts.TypeChecker,
18
20
  visited = new Set<ts.Symbol>(),
19
21
  depth = 0
20
22
  ): ts.FunctionLikeDeclaration | null {
23
+ if (!symbol) return null;
21
24
  if (visited.has(symbol)) return null;
22
- if (depth > 5) return null;
25
+ if (depth > 10) return null;
23
26
 
24
27
  visited.add(symbol);
25
28
 
26
- // unwrap alias
29
+ // Alias unwrap (import/export)
27
30
  if (symbol.flags & ts.SymbolFlags.Alias) {
28
- const aliased = checker.getAliasedSymbol(symbol);
29
- return resolveFromSymbol(aliased, checker, visited, depth + 1);
30
- }
31
-
32
- // re-export support
33
- if ((symbol as any).exports) {
34
- for (const exp of (symbol as any).exports.values()) {
35
- const resolved = resolveFromSymbol(exp, checker, visited, depth + 1);
36
- if (resolved) return resolved;
37
- }
31
+ try {
32
+ const aliased = checker.getAliasedSymbol(symbol);
33
+ return resolveFromSymbol(aliased, checker, visited, depth + 1);
34
+ } catch {}
38
35
  }
39
36
 
37
+ // Declarations
40
38
  const decls = symbol.getDeclarations() ?? [];
41
39
 
42
40
  for (const decl of decls) {
43
- // direct function
41
+ /**
42
+ * direct function
43
+ */
44
44
  if (isFunctionLike(decl)) {
45
45
  return decl;
46
46
  }
47
47
 
48
- // VARIABLE → FUNCTION
48
+ // VARIABLE → FUNCTION const fn = () => {}
49
49
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
50
50
  if (isFunctionLike(decl.initializer)) {
51
51
  return decl.initializer;
@@ -63,7 +63,12 @@ function resolveFromSymbol(
63
63
  }
64
64
  }
65
65
 
66
- // MethodSignature (interface / type)
66
+ // CLASS METHOD
67
+ if (ts.isMethodDeclaration(decl)) {
68
+ return decl;
69
+ }
70
+
71
+ // interface / type method
67
72
  if (ts.isMethodSignature(decl)) {
68
73
  const parent = decl.parent;
69
74
 
@@ -71,14 +76,10 @@ function resolveFromSymbol(
71
76
  ts.isInterfaceDeclaration(parent) ||
72
77
  ts.isTypeLiteralNode(parent)
73
78
  ) {
74
- const name = decl.name.getText();
75
-
76
79
  const type = checker.getTypeAtLocation(parent);
77
80
  const props = type.getProperties();
78
81
 
79
82
  for (const prop of props) {
80
- if (prop.name !== name) continue;
81
-
82
83
  const resolved = resolveFromSymbol(
83
84
  prop,
84
85
  checker,
@@ -91,10 +92,24 @@ function resolveFromSymbol(
91
92
  }
92
93
 
93
94
  // fallback recursion
94
- const s = checker.getSymbolAtLocation(decl);
95
- if (s && s !== symbol) {
95
+ const inner = checker.getSymbolAtLocation(decl);
96
+ if (inner && inner !== symbol) {
96
97
  const resolved = resolveFromSymbol(
97
- s,
98
+ inner,
99
+ checker,
100
+ visited,
101
+ depth + 1
102
+ );
103
+ if (resolved) return resolved;
104
+ }
105
+ }
106
+
107
+ // Exports (re-export)
108
+ const exports = (symbol as any).exports;
109
+ if (exports) {
110
+ for (const exp of exports.values()) {
111
+ const resolved = resolveFromSymbol(
112
+ exp,
98
113
  checker,
99
114
  visited,
100
115
  depth + 1
@@ -106,17 +121,19 @@ function resolveFromSymbol(
106
121
  return null;
107
122
  }
108
123
 
124
+ // MAIN FUNCTION
109
125
  export function resolveFunctionFromCall(
110
126
  expr: ts.Expression,
111
127
  checker: ts.TypeChecker
112
128
  ): ts.FunctionLikeDeclaration | null {
113
-
114
- // tracker.track
129
+ // obj.method()
115
130
  if (ts.isPropertyAccessExpression(expr)) {
116
131
  let symbol = checker.getSymbolAtLocation(expr.name);
117
132
 
118
133
  if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
119
- symbol = checker.getAliasedSymbol(symbol);
134
+ try {
135
+ symbol = checker.getAliasedSymbol(symbol);
136
+ } catch {}
120
137
  }
121
138
 
122
139
  if (symbol) {
@@ -125,7 +142,7 @@ export function resolveFunctionFromCall(
125
142
  }
126
143
  }
127
144
 
128
- // tracker["track"]
145
+ // obj["method"]()
129
146
  if (ts.isElementAccessExpression(expr)) {
130
147
  const symbol = checker.getSymbolAtLocation(expr.argumentExpression);
131
148
 
@@ -135,7 +152,7 @@ export function resolveFunctionFromCall(
135
152
  }
136
153
  }
137
154
 
138
- // normal call signatures
155
+ // Call signatures
139
156
  const type = checker.getTypeAtLocation(expr);
140
157
  const signatures = type.getCallSignatures();
141
158
 
@@ -157,7 +174,23 @@ export function resolveFunctionFromCall(
157
174
  }
158
175
  }
159
176
 
160
- // fallback
177
+ // Identifier fallback
178
+ if (ts.isIdentifier(expr)) {
179
+ let symbol = checker.getSymbolAtLocation(expr);
180
+
181
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
182
+ try {
183
+ symbol = checker.getAliasedSymbol(symbol);
184
+ } catch {}
185
+ }
186
+
187
+ if (symbol) {
188
+ const resolved = resolveFromSymbol(symbol, checker);
189
+ if (resolved) return resolved;
190
+ }
191
+ }
192
+
193
+ // LAST fallback
161
194
  const symbol = checker.getSymbolAtLocation(expr);
162
195
  if (symbol) {
163
196
  return resolveFromSymbol(symbol, checker);
@@ -7,9 +7,10 @@ export class TSService {
7
7
  private fileNames = new Set<string>();
8
8
  private service: ts.LanguageService;
9
9
 
10
- private compilerOptions: ts.CompilerOptions;
11
- private baseUrl: string;
12
- private paths: Record<string, string[]>;
10
+ private readonly compilerOptions: ts.CompilerOptions;
11
+ private readonly baseUrl: string;
12
+ private readonly paths: Record<string, string[]>;
13
+
13
14
  private usage = new Map<string, number>();
14
15
  private tick = 0;
15
16
  private MAX_FILES = 2000;
@@ -23,6 +24,7 @@ export class TSService {
23
24
 
24
25
  let compilerOptions: ts.CompilerOptions = {
25
26
  allowJs: true,
27
+ allowNonTsExtensions: true,
26
28
  jsx: ts.JsxEmit.React,
27
29
  jsxImportSource: "react",
28
30
  moduleResolution: ts.ModuleResolutionKind.Node16,
@@ -60,7 +62,17 @@ export class TSService {
60
62
  this.paths = paths;
61
63
 
62
64
  const host: ts.LanguageServiceHost = {
63
- getScriptFileNames: () => Array.from(this.fileNames),
65
+ getScriptFileNames: () => {
66
+ if (this.fileNames.size === 0) {
67
+ return ts.sys.readDirectory(
68
+ this.root,
69
+ [".ts", ".tsx", ".js", ".jsx"],
70
+ undefined,
71
+ undefined
72
+ );
73
+ }
74
+ return Array.from(this.fileNames);
75
+ },
64
76
 
65
77
  getScriptVersion: (file) => {
66
78
  file = this.normalize(file);
@@ -69,13 +81,29 @@ export class TSService {
69
81
 
70
82
  getScriptSnapshot: (file) => {
71
83
  file = this.normalize(file);
72
-
73
84
  const f = this.files.get(file);
74
85
  if (f) {
75
86
  this.touch(file);
76
87
  return ts.ScriptSnapshot.fromString(f.content);
77
88
  }
89
+ if (file.endsWith(".tsx")) {
90
+ const original = file.replace(/\.tsx$/, "");
91
+
92
+ if (fs.existsSync(original)) {
93
+ const content = fs.readFileSync(original, "utf-8");
94
+
95
+ this.files.set(file, {
96
+ content,
97
+ version: 1,
98
+ });
99
+
100
+ this.fileNames.add(file);
101
+ this.touch(file);
102
+ this.evictIfNeeded();
78
103
 
104
+ return ts.ScriptSnapshot.fromString(content);
105
+ }
106
+ }
79
107
  try {
80
108
  if (fs.existsSync(file)) {
81
109
  const content = fs.readFileSync(file, "utf-8");
@@ -93,16 +121,22 @@ export class TSService {
93
121
  }
94
122
  } catch {}
95
123
 
96
- // fallback
97
124
  return ts.ScriptSnapshot.fromString("");
98
125
  },
99
-
100
126
  getCurrentDirectory: () => this.root,
101
127
  getCompilationSettings: () => this.compilerOptions,
102
128
  getDefaultLibFileName: (opts) =>
103
129
  ts.getDefaultLibFilePath(opts),
104
- fileExists: ts.sys.fileExists,
105
- readFile: ts.sys.readFile,
130
+ fileExists: (file) => {
131
+ file = this.normalize(file);
132
+ return this.files.has(file) || ts.sys.fileExists(file);
133
+ },
134
+
135
+ readFile: (file) => {
136
+ file = this.normalize(file);
137
+ return this.files.get(file)?.content ?? ts.sys.readFile(file);
138
+ },
139
+
106
140
  readDirectory: ts.sys.readDirectory,
107
141
  };
108
142
 
@@ -122,7 +156,7 @@ export class TSService {
122
156
  private evictIfNeeded() {
123
157
  if (this.fileNames.size <= this.MAX_FILES) return;
124
158
 
125
- let oldestFile: string | null = null;
159
+ let oldest: string | null = null;
126
160
  let oldestTick = Infinity;
127
161
 
128
162
  for (const f of this.fileNames) {
@@ -130,20 +164,20 @@ export class TSService {
130
164
 
131
165
  if (t < oldestTick) {
132
166
  oldestTick = t;
133
- oldestFile = f;
167
+ oldest = f;
134
168
  }
135
169
  }
136
170
 
137
- if (oldestFile) {
138
- this.files.delete(oldestFile);
139
- this.fileNames.delete(oldestFile);
140
- this.usage.delete(oldestFile);
171
+ if (oldest) {
172
+ this.files.delete(oldest);
173
+ this.fileNames.delete(oldest);
174
+ this.usage.delete(oldest);
141
175
  }
142
176
  }
143
177
 
144
178
  private ensureProgram() {
145
179
  try {
146
- this.service.getProgram();
180
+ this.service.getProgram()?.getTypeChecker();
147
181
  } catch {}
148
182
  }
149
183
 
@@ -173,15 +173,13 @@ export function scanSource(
173
173
  config: EventraConfig,
174
174
  visited = new Set<string>(),
175
175
  inheritedParamMap?: Map<string, ts.Expression>,
176
- wrapperCache?: Map<string, string | null>
176
+ wrapperCache = new Map<string, string | null>()
177
177
  ): ScanResult {
178
- wrapperCache = wrapperCache ?? new Map();
179
-
180
178
  const events = new Set<string>();
181
179
  const detectedFunctionWrappers = new Set<string>();
182
180
  const detectedComponentWrappers = new Map<string, string>();
183
181
 
184
- if (visited.size > 200 || visited.has(source.fileName)) {
182
+ if (visited.has(source.fileName) || visited.size > 200) {
185
183
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
186
184
  }
187
185
 
@@ -189,76 +187,58 @@ export function scanSource(
189
187
 
190
188
  function visit(node: ts.Node) {
191
189
  // JSX WRAPPERS
192
- if (
193
- ts.isJsxSelfClosingElement(node) ||
194
- ts.isJsxOpeningElement(node)
195
- ) {
190
+ if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
196
191
  const tagName = getJsxTagName(node.tagName);
197
192
 
198
193
  // skip html
199
- if (!tagName || tagName[0] === tagName[0].toLowerCase()) {
194
+ if (tagName[0] === tagName[0].toLowerCase()) {
200
195
  ts.forEachChild(node, visit);
201
196
  return;
202
197
  }
203
198
 
204
- let wrapper = config.wrappers.find(
205
- (w) => w.name === tagName
206
- );
199
+ let wrapper = config.wrappers.find(w => w.name === tagName);
207
200
 
201
+ // AUTO DETECT
208
202
  if (!wrapper) {
209
- let symbol = checker.getSymbolAtLocation(node.tagName);
210
-
211
- let cacheKey = tagName;
203
+ if (wrapperCache.has(tagName)) {
204
+ const cached = wrapperCache.get(tagName);
205
+ if (cached) wrapper = { name: tagName, prop: cached };
206
+ } else {
207
+ let symbol = checker.getSymbolAtLocation(node.tagName);
212
208
 
213
- if (symbol) {
214
- if (symbol.flags & ts.SymbolFlags.Alias) {
209
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
215
210
  symbol = checker.getAliasedSymbol(symbol);
216
211
  }
217
212
 
218
- symbol =
219
- resolveExportedSymbol(symbol, checker) ?? symbol;
220
-
221
- cacheKey = checker.getFullyQualifiedName(symbol);
222
- }
223
-
224
- if (wrapperCache?.has(cacheKey)) {
225
- const cached = wrapperCache.get(cacheKey);
226
- if (cached) {
227
- wrapper = { name: tagName, prop: cached };
228
- }
229
- } else if (symbol) {
230
- let detected: string | null = null;
213
+ if (symbol) {
214
+ symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
231
215
 
232
- for (const decl of symbol.getDeclarations() ?? []) {
233
- let fn: ts.FunctionLikeDeclaration | null = null;
216
+ for (const decl of symbol.getDeclarations() ?? []) {
217
+ let fn: ts.FunctionLikeDeclaration | null = null;
234
218
 
235
- if (ts.isFunctionDeclaration(decl)) fn = decl;
219
+ if (ts.isFunctionDeclaration(decl)) fn = decl;
236
220
 
237
- if (
238
- ts.isVariableDeclaration(decl) &&
239
- decl.initializer &&
240
- (ts.isArrowFunction(decl.initializer) ||
241
- ts.isFunctionExpression(decl.initializer))
242
- ) {
243
- fn = decl.initializer;
244
- }
221
+ if (
222
+ ts.isVariableDeclaration(decl) &&
223
+ decl.initializer &&
224
+ (ts.isArrowFunction(decl.initializer) ||
225
+ ts.isFunctionExpression(decl.initializer))
226
+ ) {
227
+ fn = decl.initializer;
228
+ }
245
229
 
246
- if (!fn) continue;
230
+ if (!fn) continue;
247
231
 
248
- const prop = findTrackedPropName(fn, checker);
232
+ const prop = findTrackedPropName(fn, checker);
233
+ wrapperCache.set(tagName, prop ?? null);
249
234
 
250
- if (prop) {
251
- detected = prop;
252
- break;
235
+ if (prop) {
236
+ wrapper = { name: tagName, prop };
237
+ detectedComponentWrappers.set(tagName, prop);
238
+ break;
239
+ }
253
240
  }
254
241
  }
255
-
256
- wrapperCache?.set(cacheKey, detected);
257
-
258
- if (detected) {
259
- wrapper = { name: tagName, prop: detected };
260
- detectedComponentWrappers.set(tagName, detected);
261
- }
262
242
  }
263
243
  }
264
244
 
@@ -268,25 +248,21 @@ export function scanSource(
268
248
 
269
249
  const name = getJsxAttrName(attr.name);
270
250
  if (name !== wrapper.prop) continue;
251
+
271
252
  if (!attr.initializer) continue;
272
253
 
273
254
  if (ts.isStringLiteral(attr.initializer)) {
274
- if (isValidEvent(attr.initializer.text)) {
275
- events.add(attr.initializer.text);
276
- }
255
+ const v = attr.initializer.text;
256
+ if (isValidEvent(v)) events.add(v);
277
257
  }
278
258
 
279
259
  if (ts.isJsxExpression(attr.initializer)) {
280
260
  const expr = attr.initializer.expression;
281
261
  if (!expr) continue;
282
262
 
283
- const res = resolveNodeValue(
284
- expr,
285
- checker,
286
- inheritedParamMap
287
- );
263
+ const res = resolveNodeValue(expr, checker, inheritedParamMap);
288
264
 
289
- res.values.forEach((v) => {
265
+ res.values.forEach(v => {
290
266
  if (isValidEvent(v)) events.add(v);
291
267
  });
292
268
  }
@@ -297,40 +273,56 @@ export function scanSource(
297
273
  return;
298
274
  }
299
275
 
300
- // FUNCTION TRACKING
276
+ // FUNCTION CALLS
301
277
  if (ts.isCallExpression(node)) {
302
- const name = getCallName(node.expression);
278
+ let trackParamIndex: number | null = null;
279
+ let isTracking = false;
280
+
281
+ const callName = getCallName(node.expression);
282
+
283
+ // direct .track()
284
+ if (
285
+ ts.isPropertyAccessExpression(node.expression) &&
286
+ node.expression.name.text === "track"
287
+ ) {
288
+ isTracking = true;
289
+ trackParamIndex = 0;
290
+ }
303
291
 
304
- const isKnownWrapper = config.functionWrappers.some(
305
- (w) => w.name === name
292
+ // config wrapper
293
+ const manualWrapper = config.functionWrappers.find(
294
+ w => w.name === callName
306
295
  );
307
296
 
297
+ if (manualWrapper) {
298
+ isTracking = true;
299
+ trackParamIndex = 0;
300
+ }
301
+
302
+ // resolve function → check body
308
303
  const resolvedFn = resolveFunctionFromCall(
309
304
  node.expression,
310
305
  checker
311
306
  );
312
307
 
313
- const isAutoWrapper =
314
- !!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
308
+ if (resolvedFn) {
309
+ const idx = findTrackParamIndex(resolvedFn);
315
310
 
316
- const isTracking =
317
- name === "track" ||
318
- isKnownWrapper ||
319
- (isAutoWrapper &&
320
- resolvedFn &&
321
- !isExternalFile(
322
- resolvedFn.getSourceFile().fileName
323
- ));
311
+ if (idx !== null) {
312
+ isTracking = true;
313
+ trackParamIndex = idx;
314
+
315
+ if (!manualWrapper && callName !== "track") {
316
+ detectedFunctionWrappers.add(callName);
317
+ }
318
+ }
319
+ }
324
320
 
325
321
  if (!isTracking) {
326
322
  ts.forEachChild(node, visit);
327
323
  return;
328
324
  }
329
325
 
330
- if (!isKnownWrapper && isAutoWrapper && name !== "track") {
331
- detectedFunctionWrappers.add(name);
332
- }
333
-
334
326
  const paramMap = new Map(inheritedParamMap ?? []);
335
327
 
336
328
  if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
@@ -344,22 +336,19 @@ export function scanSource(
344
336
  });
345
337
  }
346
338
 
347
- if (resolvedFn) {
348
- const idx = findTrackParamIndex(resolvedFn);
339
+ if (trackParamIndex !== null && node.arguments[trackParamIndex]) {
340
+ const res = resolveNodeValue(
341
+ node.arguments[trackParamIndex],
342
+ checker,
343
+ paramMap
344
+ );
349
345
 
350
- if (idx !== null && node.arguments[idx]) {
351
- const res = resolveNodeValue(
352
- node.arguments[idx],
353
- checker,
354
- paramMap
355
- );
356
-
357
- res.values.forEach((v) => {
358
- if (isValidEvent(v)) events.add(v);
359
- });
360
- }
346
+ res.values.forEach(v => {
347
+ if (isValidEvent(v)) events.add(v);
348
+ });
361
349
  }
362
350
 
351
+ // CROSS FILE
363
352
  if (resolvedFn) {
364
353
  const sf = resolvedFn.getSourceFile();
365
354
 
@@ -373,7 +362,7 @@ export function scanSource(
373
362
  wrapperCache
374
363
  );
375
364
 
376
- res.events.forEach((e) => events.add(e));
365
+ res.events.forEach(e => events.add(e));
377
366
  }
378
367
  }
379
368
  }