@eventra_dev/eventra-cli 0.1.25 → 0.1.27

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.
@@ -29,18 +29,48 @@ function resolveFromSymbol(
29
29
  return resolveFromSymbol(aliased, checker, visited, depth + 1);
30
30
  }
31
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
+ }
38
+ }
39
+
32
40
  const decls = symbol.getDeclarations() ?? [];
33
41
 
34
42
  for (const decl of decls) {
43
+ // direct function
35
44
  if (isFunctionLike(decl)) {
36
45
  return decl;
37
46
  }
38
47
 
39
- // MethodSignature
48
+ // VARIABLE → FUNCTION
49
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
50
+ if (isFunctionLike(decl.initializer)) {
51
+ return decl.initializer;
52
+ }
53
+
54
+ const innerSymbol = checker.getSymbolAtLocation(decl.initializer);
55
+ if (innerSymbol && innerSymbol !== symbol) {
56
+ const resolved = resolveFromSymbol(
57
+ innerSymbol,
58
+ checker,
59
+ visited,
60
+ depth + 1
61
+ );
62
+ if (resolved) return resolved;
63
+ }
64
+ }
65
+
66
+ // MethodSignature (interface / type)
40
67
  if (ts.isMethodSignature(decl)) {
41
68
  const parent = decl.parent;
42
69
 
43
- if (ts.isInterfaceDeclaration(parent) || ts.isTypeLiteralNode(parent)) {
70
+ if (
71
+ ts.isInterfaceDeclaration(parent) ||
72
+ ts.isTypeLiteralNode(parent)
73
+ ) {
44
74
  const name = decl.name.getText();
45
75
 
46
76
  const type = checker.getTypeAtLocation(parent);
@@ -49,16 +79,26 @@ function resolveFromSymbol(
49
79
  for (const prop of props) {
50
80
  if (prop.name !== name) continue;
51
81
 
52
- const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
82
+ const resolved = resolveFromSymbol(
83
+ prop,
84
+ checker,
85
+ visited,
86
+ depth + 1
87
+ );
53
88
  if (resolved) return resolved;
54
89
  }
55
90
  }
56
91
  }
57
92
 
58
- // SAFE fallback
93
+ // fallback recursion
59
94
  const s = checker.getSymbolAtLocation(decl);
60
95
  if (s && s !== symbol) {
61
- const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
96
+ const resolved = resolveFromSymbol(
97
+ s,
98
+ checker,
99
+ visited,
100
+ depth + 1
101
+ );
62
102
  if (resolved) return resolved;
63
103
  }
64
104
  }
@@ -70,6 +110,32 @@ export function resolveFunctionFromCall(
70
110
  expr: ts.Expression,
71
111
  checker: ts.TypeChecker
72
112
  ): ts.FunctionLikeDeclaration | null {
113
+
114
+ // tracker.track
115
+ if (ts.isPropertyAccessExpression(expr)) {
116
+ let symbol = checker.getSymbolAtLocation(expr.name);
117
+
118
+ if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
119
+ symbol = checker.getAliasedSymbol(symbol);
120
+ }
121
+
122
+ if (symbol) {
123
+ const resolved = resolveFromSymbol(symbol, checker);
124
+ if (resolved) return resolved;
125
+ }
126
+ }
127
+
128
+ // tracker["track"]
129
+ if (ts.isElementAccessExpression(expr)) {
130
+ const symbol = checker.getSymbolAtLocation(expr.argumentExpression);
131
+
132
+ if (symbol) {
133
+ const resolved = resolveFromSymbol(symbol, checker);
134
+ if (resolved) return resolved;
135
+ }
136
+ }
137
+
138
+ // normal call signatures
73
139
  const type = checker.getTypeAtLocation(expr);
74
140
  const signatures = type.getCallSignatures();
75
141
 
@@ -81,7 +147,6 @@ export function resolveFunctionFromCall(
81
147
  return decl;
82
148
  }
83
149
 
84
- // interface method
85
150
  if (ts.isMethodSignature(decl)) {
86
151
  const symbol = checker.getSymbolAtLocation(decl.name);
87
152
 
@@ -92,5 +157,11 @@ export function resolveFunctionFromCall(
92
157
  }
93
158
  }
94
159
 
160
+ // fallback
161
+ const symbol = checker.getSymbolAtLocation(expr);
162
+ if (symbol) {
163
+ return resolveFromSymbol(symbol, checker);
164
+ }
165
+
95
166
  return null;
96
167
  }
@@ -1,3 +1,6 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+
1
4
  import { TSService } from "./languageService";
2
5
  import { ScanResult, scanSource } from "./scanner";
3
6
  import { DependencyGraph } from "./graph";
@@ -18,8 +21,29 @@ export class EventraEngine {
18
21
  const { baseUrl, paths } = this.ts.getModuleResolutionConfig();
19
22
  this.graph = new DependencyGraph(baseUrl, paths);
20
23
  }
24
+
25
+ private normalize(file: string) {
26
+ return path.resolve(file).replace(/\\/g, "/");
27
+ }
28
+
29
+ private empty(): ScanResult {
30
+ return {
31
+ events: new Set(),
32
+ detectedFunctionWrappers: new Set(),
33
+ detectedComponentWrappers: new Map(),
34
+ };
35
+ }
36
+
37
+ // PRELOAD
21
38
  preloadFile(file: string, content: string) {
39
+ file = this.normalize(file);
40
+
22
41
  this.ts.updateFile(file, content);
42
+
43
+ const source = this.ts.getSourceFile(file);
44
+ if (source) {
45
+ this.graph.update(file, source);
46
+ }
23
47
  }
24
48
 
25
49
  // SCAN FILE
@@ -28,33 +52,26 @@ export class EventraEngine {
28
52
  content: string,
29
53
  config: EventraConfig
30
54
  ): ScanResult {
55
+ file = this.normalize(file);
56
+
31
57
  const h = hash(content);
32
58
 
33
- // skip unchanged
34
59
  if (this.fileHash.get(file) === h) {
35
- return (
36
- this.fileResults.get(file) ?? {
37
- events: new Set(),
38
- detectedFunctionWrappers: new Set(),
39
- detectedComponentWrappers: new Map(),
40
- }
41
- );
60
+ return this.fileResults.get(file) ?? this.empty();
42
61
  }
43
62
 
44
63
  this.fileHash.set(file, h);
64
+
45
65
  this.ts.updateFile(file, content);
46
66
 
47
67
  const source = this.ts.getSourceFile(file);
48
68
  const checker = this.ts.getChecker();
49
69
 
50
70
  if (!source || !checker) {
51
- return {
52
- events: new Set(),
53
- detectedFunctionWrappers: new Set(),
54
- detectedComponentWrappers: new Map(),
55
- };
71
+ return this.empty();
56
72
  }
57
73
 
74
+ // обновляем graph
58
75
  this.graph.update(file, source);
59
76
 
60
77
  const res = scanSource(source, checker, config);
@@ -69,32 +86,46 @@ export class EventraEngine {
69
86
  content: string,
70
87
  config: EventraConfig
71
88
  ) {
89
+ file = this.normalize(file);
90
+
72
91
  const res = this.scanFile(file, content, config);
73
92
 
74
93
  const dependents = this.graph.getAllDependentsDeep(file);
75
94
 
76
95
  for (const dep of dependents) {
77
- if (isExternalFile(dep)) continue;
96
+ const abs = this.normalize(dep);
78
97
 
79
- const depContent = this.ts.getFileContent(dep);
80
- if (!depContent) continue;
98
+ if (isExternalFile(abs)) continue;
99
+
100
+ let depContent = this.ts.getFileContent(abs);
101
+
102
+ if (!depContent) {
103
+ try {
104
+ depContent = fs.readFileSync(abs, "utf-8");
105
+ } catch {
106
+ continue;
107
+ }
108
+ }
81
109
 
82
110
  const h = hash(depContent);
83
111
 
84
- if (this.fileHash.get(dep) === h) continue;
112
+ if (this.fileHash.get(abs) === h) {
113
+ continue;
114
+ }
115
+
116
+ this.fileHash.set(abs, h);
85
117
 
86
- this.fileHash.set(dep, h);
87
- this.ts.updateFile(dep, depContent);
118
+ this.ts.updateFile(abs, depContent);
88
119
 
89
- const source = this.ts.getSourceFile(dep);
120
+ const source = this.ts.getSourceFile(abs);
90
121
  const checker = this.ts.getChecker();
91
122
 
92
123
  if (!source || !checker) continue;
93
124
 
94
- this.graph.update(dep, source);
125
+ this.graph.update(abs, source);
95
126
 
96
127
  const r = scanSource(source, checker, config);
97
- this.fileResults.set(dep, r);
128
+ this.fileResults.set(abs, r);
98
129
  }
99
130
 
100
131
  return res;
@@ -113,31 +144,45 @@ export class EventraEngine {
113
144
 
114
145
  // REMOVE FILE
115
146
  removeFile(file: string, config?: EventraConfig) {
147
+ file = this.normalize(file);
148
+
149
+ const dependents = this.graph.getAllDependentsDeep(file);
150
+
116
151
  this.ts.removeFile(file);
117
152
  this.fileResults.delete(file);
118
153
  this.fileHash.delete(file);
119
154
  this.graph.remove(file);
120
155
 
121
- const dependents = this.graph.getAllDependentsDeep(file);
122
-
123
156
  for (const dep of dependents) {
124
- if (isExternalFile(dep)) continue;
157
+ const abs = this.normalize(dep);
125
158
 
126
- const content = this.ts.getFileContent(dep);
127
- if (!content) continue;
159
+ if (isExternalFile(abs)) continue;
160
+
161
+ let content = this.ts.getFileContent(abs);
162
+
163
+ if (!content) {
164
+ try {
165
+ content = fs.readFileSync(abs, "utf-8");
166
+ } catch {
167
+ continue;
168
+ }
169
+ }
128
170
 
129
171
  const h = hash(content);
130
172
 
131
- if (this.fileHash.get(dep) === h) continue;
173
+ if (this.fileHash.get(abs) === h) continue;
174
+
175
+ this.fileHash.set(abs, h);
132
176
 
133
- this.fileHash.set(dep, h);
134
- this.ts.updateFile(dep, content);
177
+ this.ts.updateFile(abs, content);
135
178
 
136
- const source = this.ts.getSourceFile(dep);
179
+ const source = this.ts.getSourceFile(abs);
137
180
  const checker = this.ts.getChecker();
138
181
 
139
182
  if (!source || !checker) continue;
140
183
 
184
+ this.graph.update(abs, source);
185
+
141
186
  const r = scanSource(
142
187
  source,
143
188
  checker,
@@ -149,7 +194,7 @@ export class EventraEngine {
149
194
  }
150
195
  );
151
196
 
152
- this.fileResults.set(dep, r);
197
+ this.fileResults.set(abs, r);
153
198
  }
154
199
  }
155
200
  }
@@ -1,5 +1,13 @@
1
1
  import ts from "typescript";
2
2
 
3
+ function isRealDeclaration(node: ts.Node) {
4
+ return (
5
+ ts.isFunctionDeclaration(node) ||
6
+ ts.isVariableDeclaration(node) ||
7
+ ts.isClassDeclaration(node)
8
+ );
9
+ }
10
+
3
11
  export function resolveExportedSymbol(
4
12
  symbol: ts.Symbol,
5
13
  checker: ts.TypeChecker,
@@ -8,20 +16,29 @@ export function resolveExportedSymbol(
8
16
 
9
17
  if (visited.has(symbol)) return null;
10
18
  visited.add(symbol);
19
+
20
+ // unwrap alias
11
21
  if (symbol.flags & ts.SymbolFlags.Alias) {
12
22
  const aliased = checker.getAliasedSymbol(symbol);
13
23
  return resolveExportedSymbol(aliased, checker, visited);
14
24
  }
15
25
 
16
- if (symbol.getDeclarations()?.length) {
17
- return symbol;
26
+ // export symbol
27
+ const exportSymbol = checker.getExportSymbolOfSymbol(symbol);
28
+ if (exportSymbol && exportSymbol !== symbol) {
29
+ return resolveExportedSymbol(exportSymbol, checker, visited);
18
30
  }
19
31
 
20
- const declarations = symbol.getDeclarations() ?? [];
32
+ const decls = symbol.getDeclarations() ?? [];
33
+
34
+ if (decls.some(isRealDeclaration)) {
35
+ return symbol;
36
+ }
21
37
 
22
- for (const decl of declarations) {
38
+ // exports (re-export)
39
+ for (const decl of decls) {
23
40
  const source = decl.getSourceFile();
24
- const moduleSymbol = checker.getSymbolAtLocation(source);
41
+ const moduleSymbol = (source as any).symbol;
25
42
 
26
43
  if (!moduleSymbol) continue;
27
44
 
@@ -8,24 +8,95 @@ export function findTrackParamIndex(
8
8
  fn.parameters.forEach((p, i) => {
9
9
  if (ts.isIdentifier(p.name)) {
10
10
  paramIndex.set(p.name.text, i);
11
+
12
+ // default param: (name = "x")
13
+ if (p.initializer && ts.isStringLiteral(p.initializer)) {
14
+ if (i === 0) return 0;
15
+ }
11
16
  }
12
17
  });
13
18
 
14
19
  let result: number | null = null;
15
20
 
21
+ function unwrap(node: ts.Expression): ts.Expression {
22
+ // as / parentheses
23
+ while (
24
+ ts.isAsExpression(node) ||
25
+ ts.isParenthesizedExpression(node)
26
+ ) {
27
+ node = node.expression;
28
+ }
29
+ return node;
30
+ }
31
+
32
+ function checkArg(arg: ts.Expression) {
33
+ arg = unwrap(arg);
34
+
35
+ // identifier → param
36
+ if (ts.isIdentifier(arg)) {
37
+ const idx = paramIndex.get(arg.text);
38
+ if (idx !== undefined) {
39
+ result = idx;
40
+ return true;
41
+ }
42
+ }
43
+
44
+ // string literal
45
+ if (ts.isStringLiteral(arg)) {
46
+ result = 0;
47
+ return true;
48
+ }
49
+
50
+ // template `${name}`
51
+ if (ts.isTemplateExpression(arg)) {
52
+ for (const span of arg.templateSpans) {
53
+ if (ts.isIdentifier(span.expression)) {
54
+ const idx = paramIndex.get(span.expression.text);
55
+ if (idx !== undefined) {
56
+ result = idx;
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+ result = 0;
62
+ return true;
63
+ }
64
+
65
+ // object { event: name }
66
+ if (ts.isObjectLiteralExpression(arg)) {
67
+ for (const prop of arg.properties) {
68
+ if (!ts.isPropertyAssignment(prop)) continue;
69
+
70
+ const name = prop.name.getText();
71
+
72
+ if (["event", "name", "type"].includes(name)) {
73
+ return checkArg(prop.initializer);
74
+ }
75
+ }
76
+ }
77
+
78
+ // call: String(name)
79
+ if (ts.isCallExpression(arg)) {
80
+ for (const inner of arg.arguments) {
81
+ if (checkArg(inner)) return true;
82
+ }
83
+ }
84
+
85
+ return false;
86
+ }
87
+
16
88
  function visit(node: ts.Node) {
89
+ if (result !== null) return;
90
+
17
91
  if (ts.isCallExpression(node)) {
18
92
  let isTrackCall = false;
19
93
 
20
94
  if (ts.isPropertyAccessExpression(node.expression)) {
21
- const method = node.expression.name.text;
22
-
23
- if (method === "track") {
95
+ if (node.expression.name.text === "track") {
24
96
  isTrackCall = true;
25
97
  }
26
98
  }
27
99
 
28
- // fallback
29
100
  if (ts.isIdentifier(node.expression)) {
30
101
  if (node.expression.text === "track") {
31
102
  isTrackCall = true;
@@ -34,18 +105,7 @@ export function findTrackParamIndex(
34
105
 
35
106
  if (isTrackCall) {
36
107
  for (const arg of node.arguments) {
37
- if (ts.isIdentifier(arg)) {
38
- const idx = paramIndex.get(arg.text);
39
- if (idx !== undefined) {
40
- result = idx;
41
- return;
42
- }
43
- }
44
-
45
- if (ts.isStringLiteral(arg)) {
46
- result = 0;
47
- return;
48
- }
108
+ if (checkArg(arg)) return;
49
109
  }
50
110
  }
51
111
  }
@@ -13,15 +13,18 @@ export class DependencyGraph {
13
13
  ) {}
14
14
 
15
15
  private normalize(file: string) {
16
- return path.resolve(file);
16
+ return path.resolve(file).replace(/\\/g, "/");
17
17
  }
18
18
 
19
+ // MODULE RESOLVE
19
20
  private resolveModule(module: string, from: string): string | null {
21
+ // relative import
20
22
  if (module.startsWith(".")) {
21
23
  const abs = path.resolve(path.dirname(from), module);
22
24
  return this.resolveFile(abs);
23
25
  }
24
26
 
27
+ // tsconfig paths
25
28
  for (const key in this.paths) {
26
29
  const pattern = key.replace(/\*/g, "(.*)");
27
30
  const regex = new RegExp("^" + pattern + "$");
@@ -44,10 +47,21 @@ export class DependencyGraph {
44
47
  }
45
48
  }
46
49
 
50
+ // node resolution (safe)
51
+ try {
52
+ const resolved = require.resolve(module, {
53
+ paths: [path.dirname(from)],
54
+ });
55
+
56
+ return this.normalize(resolved);
57
+ } catch {}
58
+
59
+ // fallback
47
60
  const abs = path.resolve(this.baseUrl, module);
48
61
  return this.resolveFile(abs);
49
62
  }
50
63
 
64
+ // FILE RESOLVE
51
65
  private resolveFile(base: string): string | null {
52
66
  if (this.fileCache.has(base)) {
53
67
  return this.fileCache.get(base)!;
@@ -59,6 +73,12 @@ export class DependencyGraph {
59
73
  base + ".tsx",
60
74
  base + ".js",
61
75
  base + ".jsx",
76
+ base + ".mjs",
77
+ base + ".cjs",
78
+ base + ".d.ts",
79
+ base + ".vue",
80
+ base + ".svelte",
81
+ base + ".astro",
62
82
  path.join(base, "index.ts"),
63
83
  path.join(base, "index.tsx"),
64
84
  path.join(base, "index.js"),
@@ -76,16 +96,28 @@ export class DependencyGraph {
76
96
  return null;
77
97
  }
78
98
 
99
+ // UPDATE GRAPH
79
100
  update(file: string, source: ts.SourceFile) {
80
101
  const normalizedFile = this.normalize(file);
81
102
 
103
+ for (const deps of this.reverse.values()) {
104
+ deps.delete(normalizedFile);
105
+ }
106
+
82
107
  this.exports.set(normalizedFile, new Set());
83
108
 
84
109
  for (const stmt of source.statements) {
110
+
111
+ // IMPORT
85
112
  if (ts.isImportDeclaration(stmt)) {
86
- const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
87
- const resolved = this.resolveModule(module, normalizedFile);
113
+ // ignore import type
114
+ if ((stmt.importClause as any)?.importKind === "type") continue;
115
+
116
+ const module = stmt.moduleSpecifier
117
+ .getText()
118
+ .replace(/['"]/g, "");
88
119
 
120
+ const resolved = this.resolveModule(module, normalizedFile);
89
121
  if (!resolved) continue;
90
122
 
91
123
  if (!this.reverse.has(resolved)) {
@@ -95,10 +127,15 @@ export class DependencyGraph {
95
127
  this.reverse.get(resolved)!.add(normalizedFile);
96
128
  }
97
129
 
98
- if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
99
- const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
100
- const resolved = this.resolveModule(module, normalizedFile);
130
+ // EXPORT (re-export)
131
+ if (ts.isExportDeclaration(stmt)) {
132
+ if (!stmt.moduleSpecifier) continue;
133
+
134
+ const module = stmt.moduleSpecifier
135
+ .getText()
136
+ .replace(/['"]/g, "");
101
137
 
138
+ const resolved = this.resolveModule(module, normalizedFile);
102
139
  if (!resolved) continue;
103
140
 
104
141
  this.exports.get(normalizedFile)?.add(resolved);
@@ -112,6 +149,7 @@ export class DependencyGraph {
112
149
  }
113
150
  }
114
151
 
152
+ // DEPENDENTS
115
153
  getAllDependentsDeep(file: string): string[] {
116
154
  const start = this.normalize(file);
117
155
  const visited = new Set<string>();
@@ -136,6 +174,7 @@ export class DependencyGraph {
136
174
  return [...result];
137
175
  }
138
176
 
177
+ // REMOVE
139
178
  remove(file: string) {
140
179
  const normalized = this.normalize(file);
141
180