@eventra_dev/eventra-cli 0.1.5 → 0.1.7

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.
@@ -7,94 +7,195 @@ exports.scanSource = scanSource;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const resolver_1 = require("./resolver");
9
9
  const callResolver_1 = require("./callResolver");
10
+ const exportResolver_1 = require("./exportResolver");
10
11
  const functionAnalyzer_1 = require("./functionAnalyzer");
12
+ const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
11
13
  function getCallName(expr) {
12
14
  if (typescript_1.default.isIdentifier(expr))
13
15
  return expr.text;
14
- if (typescript_1.default.isPropertyAccessExpression(expr)) {
16
+ if (typescript_1.default.isPropertyAccessExpression(expr))
15
17
  return expr.name.text;
16
- }
17
18
  return "";
18
19
  }
19
- function pickBestArg(args, checker, paramMap) {
20
- let best = null;
21
- for (const arg of args) {
22
- const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
23
- if (!res?.values?.length)
24
- continue;
25
- let score = 0;
26
- if (!res.dynamic)
27
- score += 3;
28
- if (res.values.every(v => v.length < 50))
29
- score += 2;
30
- if (res.values.some(v => v.includes("*")))
31
- score -= 2;
32
- if (!best || score > best.score) {
33
- best = { score, node: arg };
20
+ function getJsxAttrName(name) {
21
+ if (typescript_1.default.isIdentifier(name))
22
+ return name.text;
23
+ if (typescript_1.default.isJsxNamespacedName(name))
24
+ return name.name.text;
25
+ return null;
26
+ }
27
+ function isValidEvent(v) {
28
+ return (v &&
29
+ v.length > 1 &&
30
+ v.length < 100 &&
31
+ !v.includes(" ") &&
32
+ /^[a-zA-Z0-9._:-]+$/.test(v));
33
+ }
34
+ function isPropUsedInTracking(fn, prop) {
35
+ let found = false;
36
+ function visit(node) {
37
+ if (typescript_1.default.isCallExpression(node)) {
38
+ const name = getCallName(node.expression);
39
+ if (!TRACKING_NAMES.has(name))
40
+ return;
41
+ for (const arg of node.arguments) {
42
+ if ((typescript_1.default.isIdentifier(arg) && arg.text === prop) ||
43
+ (typescript_1.default.isPropertyAccessExpression(arg) &&
44
+ arg.name.text === prop)) {
45
+ found = true;
46
+ }
47
+ }
34
48
  }
49
+ typescript_1.default.forEachChild(node, visit);
35
50
  }
36
- return best?.node;
51
+ visit(fn);
52
+ return found;
37
53
  }
38
- function scanSource(source, checker, visited = new Set(), depth = 0) {
39
- if (depth > 5)
40
- return new Set();
54
+ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
41
55
  const events = new Set();
42
- if (visited.has(source.fileName))
43
- return events;
56
+ const detectedFunctionWrappers = new Set();
57
+ const detectedComponentWrappers = new Map();
58
+ if (depth > 5 || visited.has(source.fileName)) {
59
+ return { events, detectedFunctionWrappers, detectedComponentWrappers };
60
+ }
44
61
  visited.add(source.fileName);
45
62
  function visit(node) {
63
+ // CALL EXPRESSIONS
46
64
  if (typescript_1.default.isCallExpression(node)) {
47
65
  const name = getCallName(node.expression);
48
- if (/(track|event|capture|send)/i.test(name)) {
49
- const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
50
- const paramMap = new Map();
51
- if (fn && typescript_1.default.isFunctionLike(fn)) {
52
- fn.parameters.forEach((param, index) => {
53
- const arg = node.arguments[index];
54
- if (!arg)
55
- return;
56
- if (typescript_1.default.isIdentifier(param.name)) {
57
- paramMap.set(param.name.text, arg);
58
- }
66
+ const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
67
+ const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
68
+ const isAutoWrapper = fn && (0, functionAnalyzer_1.findTrackParamIndex)(fn) !== null;
69
+ const isTracking = isKnownWrapper ||
70
+ TRACKING_NAMES.has(name) ||
71
+ isAutoWrapper;
72
+ if (!isTracking)
73
+ return;
74
+ // AUTO DETECT WRAPPER
75
+ if (!isKnownWrapper &&
76
+ fn &&
77
+ !TRACKING_NAMES.has(name)) {
78
+ detectedFunctionWrappers.add(name);
79
+ }
80
+ const paramMap = new Map();
81
+ if (fn && typescript_1.default.isFunctionLike(fn)) {
82
+ fn.parameters.forEach((param, i) => {
83
+ const arg = node.arguments[i];
84
+ if (!arg)
85
+ return;
86
+ if (typescript_1.default.isIdentifier(param.name)) {
87
+ paramMap.set(param.name.text, arg);
88
+ }
89
+ });
90
+ }
91
+ let handled = false;
92
+ if (fn) {
93
+ const idx = (0, functionAnalyzer_1.findTrackParamIndex)(fn);
94
+ if (idx !== null && node.arguments[idx]) {
95
+ const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
96
+ res?.values.forEach(v => {
97
+ if (isValidEvent(v))
98
+ events.add(v);
59
99
  });
100
+ handled = true;
101
+ }
102
+ }
103
+ // fallback
104
+ if (!handled) {
105
+ for (const arg of node.arguments) {
106
+ const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
107
+ res?.values.forEach(v => {
108
+ if (isValidEvent(v))
109
+ events.add(v);
110
+ });
111
+ }
112
+ }
113
+ // cross-file traversal
114
+ if (fn) {
115
+ const sf = fn.getSourceFile();
116
+ if (!visited.has(sf.fileName)) {
117
+ const res = scanSource(sf, checker, config, visited, depth + 1);
118
+ res.events.forEach(e => events.add(e));
60
119
  }
61
- if (fn && typescript_1.default.isFunctionLike(fn)) {
62
- const idx = (0, functionAnalyzer_1.findTrackParamIndex)(fn);
63
- if (idx !== null) {
64
- const realArg = node.arguments[idx];
65
- if (realArg) {
66
- const res = (0, resolver_1.resolveNodeValue)(realArg, checker, paramMap);
67
- res?.values.forEach(v => {
68
- if (v &&
69
- v.length > 1 &&
70
- v.length < 100 &&
71
- !v.includes(" ") &&
72
- /^[a-zA-Z0-9._:-]+$/.test(v)) {
73
- events.add(v);
74
- }
75
- });
76
- return;
77
- }
120
+ }
121
+ }
122
+ // JSX COMPONENTS
123
+ if (typescript_1.default.isJsxSelfClosingElement(node) ||
124
+ typescript_1.default.isJsxOpeningElement(node)) {
125
+ const tagNode = typescript_1.default.isPropertyAccessExpression(node.tagName)
126
+ ? node.tagName.name
127
+ : node.tagName;
128
+ let symbol = checker.getSymbolAtLocation(tagNode);
129
+ if (symbol) {
130
+ const resolved = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker);
131
+ if (resolved)
132
+ symbol = resolved;
133
+ }
134
+ if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
135
+ const aliased = checker.getAliasedSymbol(symbol);
136
+ if (aliased)
137
+ symbol = aliased;
138
+ }
139
+ if (!symbol)
140
+ return;
141
+ const tagName = typescript_1.default.isPropertyAccessExpression(node.tagName)
142
+ ? node.tagName.name.getText()
143
+ : node.tagName.getText();
144
+ const declarations = symbol.getDeclarations();
145
+ if (!declarations)
146
+ return;
147
+ for (const decl of declarations) {
148
+ let fn = null;
149
+ if (typescript_1.default.isFunctionDeclaration(decl)) {
150
+ fn = decl;
151
+ }
152
+ if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
153
+ const init = decl.initializer;
154
+ if (typescript_1.default.isArrowFunction(init) ||
155
+ typescript_1.default.isFunctionExpression(init)) {
156
+ fn = init;
78
157
  }
79
158
  }
80
- // fallback (heuristic)
81
- const target = pickBestArg(node.arguments, checker, paramMap);
82
- if (target) {
83
- const res = (0, resolver_1.resolveNodeValue)(target, checker, paramMap);
159
+ if (!fn)
160
+ continue;
161
+ const param = fn.parameters[0];
162
+ if (!param)
163
+ continue;
164
+ if (!typescript_1.default.isObjectBindingPattern(param.name))
165
+ continue;
166
+ for (const el of param.name.elements) {
167
+ const prop = el.name.getText();
168
+ const isEvent = isPropUsedInTracking(fn, prop) ||
169
+ config.wrappers.some(w => w.name === tagName && w.prop === prop);
170
+ if (!isEvent)
171
+ continue;
172
+ const attr = node.attributes.properties.find(p => {
173
+ if (!typescript_1.default.isJsxAttribute(p))
174
+ return false;
175
+ return getJsxAttrName(p.name) === prop;
176
+ });
177
+ if (!attr || !attr.initializer)
178
+ continue;
179
+ const expr = typescript_1.default.isJsxExpression(attr.initializer)
180
+ ? attr.initializer.expression
181
+ : attr.initializer;
182
+ if (!expr)
183
+ continue;
184
+ const res = (0, resolver_1.resolveNodeValue)(expr, checker);
84
185
  res?.values.forEach(v => {
85
- if (v &&
86
- v.length > 1 &&
87
- v.length < 100 &&
88
- !v.includes(" ") &&
89
- /^[a-zA-Z0-9._:-]+$/.test(v)) {
186
+ if (isValidEvent(v))
90
187
  events.add(v);
91
- }
92
188
  });
189
+ detectedComponentWrappers.set(tagName, prop);
93
190
  }
94
191
  }
95
192
  }
96
193
  typescript_1.default.forEachChild(node, visit);
97
194
  }
98
195
  visit(source);
99
- return events;
196
+ return {
197
+ events,
198
+ detectedFunctionWrappers,
199
+ detectedComponentWrappers,
200
+ };
100
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -28,7 +28,7 @@ export async function check({ fix = false }: { fix?: boolean }) {
28
28
 
29
29
  const { content, virtualFile } = processFile(file, raw);
30
30
 
31
- engine.scanFile(virtualFile, content);
31
+ engine.scanFile(virtualFile, content, config);
32
32
 
33
33
  } catch (err) {
34
34
  console.log(chalk.yellow(`Skipped: ${file}`));
@@ -23,13 +23,24 @@ export async function sync() {
23
23
 
24
24
  const engine = new EventraEngine(process.cwd());
25
25
 
26
+ const detectedFn = new Set<string>();
27
+ const detectedCmp = new Map<string, string>();
28
+
26
29
  for (const file of files) {
27
30
  try {
28
31
  const raw = await fs.readFile(file, "utf-8");
29
32
 
30
33
  const { content, virtualFile } = processFile(file, raw);
31
34
 
32
- engine.scanFile(virtualFile, content);
35
+ const res = engine.scanFile(virtualFile, content, config);
36
+
37
+ for (const w of res.detectedFunctionWrappers) {
38
+ detectedFn.add(w);
39
+ }
40
+
41
+ for (const [k, v] of res.detectedComponentWrappers) {
42
+ detectedCmp.set(k, v);
43
+ }
33
44
 
34
45
  } catch (err) {
35
46
  console.log(chalk.yellow(`Skipped: ${file}`));
@@ -40,10 +51,59 @@ export async function sync() {
40
51
  }
41
52
  }
42
53
 
54
+ // EVENTS
43
55
  const events = engine.getAllEvents().sort();
44
56
 
45
- config.events = events;
57
+ const mergedEvents = new Set(config.events ?? []);
58
+ events.forEach(e => mergedEvents.add(e));
59
+
60
+ config.events = [...mergedEvents].sort();
61
+
62
+ // FUNCTION WRAPPERS
63
+ config.functionWrappers = mergeUnique(
64
+ config.functionWrappers ?? [],
65
+ [...detectedFn].map(name => ({ name })),
66
+ w => w.name
67
+ );
68
+
69
+ // COMPONENT WRAPPERS
70
+ config.wrappers = mergeUnique(
71
+ config.wrappers ?? [],
72
+ [...detectedCmp.entries()].map(([name, prop]) => ({
73
+ name,
74
+ prop,
75
+ })),
76
+ w => `${w.name}:${w.prop}`
77
+ );
78
+
46
79
  await saveConfig(config);
47
80
 
48
81
  console.log(chalk.green(`Found ${events.length} events`));
82
+
83
+ if (detectedFn.size) {
84
+ console.log(chalk.blue("\nDetected function wrappers:"));
85
+ detectedFn.forEach(w => console.log(chalk.gray(`• ${w}`)));
86
+ }
87
+
88
+ if (detectedCmp.size) {
89
+ console.log(chalk.blue("\nDetected component wrappers:"));
90
+ detectedCmp.forEach((prop, name) =>
91
+ console.log(chalk.gray(`• ${name} (${prop})`))
92
+ );
93
+ }
94
+ }
95
+
96
+ // HELPERS
97
+ function mergeUnique<T>(
98
+ a: T[],
99
+ b: T[],
100
+ key: (v: T) => string
101
+ ): T[] {
102
+ const map = new Map<string, T>();
103
+
104
+ [...b, ...a].forEach(item => {
105
+ map.set(key(item), item);
106
+ });
107
+
108
+ return [...map.values()];
49
109
  }
@@ -25,7 +25,7 @@ export async function watch() {
25
25
 
26
26
  const { content, virtualFile } = processFile(file, raw);
27
27
 
28
- engine.scanFile(virtualFile, content);
28
+ engine.scanFile(virtualFile, content, config);
29
29
 
30
30
  } catch {
31
31
  console.log(chalk.gray(`skip: ${file}`));
@@ -56,7 +56,7 @@ export async function watch() {
56
56
 
57
57
  const { content, virtualFile } = processFile(file, raw);
58
58
 
59
- engine.updateFile(virtualFile, content);
59
+ engine.updateFile(virtualFile, content, config);
60
60
 
61
61
  } catch {
62
62
  console.log(chalk.gray(`skip: ${file}`));
@@ -1,25 +1,92 @@
1
1
  import ts from "typescript";
2
2
 
3
+ function isFunctionLike(
4
+ node: ts.Node
5
+ ): node is ts.FunctionLikeDeclaration {
6
+ return (
7
+ ts.isFunctionDeclaration(node) ||
8
+ ts.isMethodDeclaration(node) ||
9
+ ts.isConstructorDeclaration(node) ||
10
+ ts.isFunctionExpression(node) ||
11
+ ts.isArrowFunction(node)
12
+ );
13
+ }
14
+
15
+ function resolveFromSymbol(
16
+ symbol: ts.Symbol,
17
+ checker: ts.TypeChecker,
18
+ visited = new Set<ts.Symbol>()
19
+ ): ts.FunctionLikeDeclaration | null {
20
+ if (visited.has(symbol)) return null;
21
+ visited.add(symbol);
22
+
23
+ // alias unwrap
24
+ if (symbol.flags & ts.SymbolFlags.Alias) {
25
+ const aliased = checker.getAliasedSymbol(symbol);
26
+ return resolveFromSymbol(aliased, checker, visited);
27
+ }
28
+
29
+ const decls = symbol.getDeclarations() ?? [];
30
+
31
+ for (const decl of decls) {
32
+ if (isFunctionLike(decl)) {
33
+ return decl;
34
+ }
35
+
36
+ if (ts.isMethodSignature(decl)) {
37
+ const parent = decl.parent;
38
+
39
+ if (ts.isInterfaceDeclaration(parent) || ts.isTypeLiteralNode(parent)) {
40
+ const name = decl.name.getText();
41
+
42
+ const type = checker.getTypeAtLocation(parent);
43
+ const props = type.getProperties();
44
+
45
+ for (const prop of props) {
46
+ if (prop.name !== name) continue;
47
+
48
+ const resolved = resolveFromSymbol(prop, checker, visited);
49
+ if (resolved) return resolved;
50
+ }
51
+ }
52
+ }
53
+
54
+ // fallback
55
+ const s = checker.getSymbolAtLocation(decl);
56
+ if (s) {
57
+ const resolved = resolveFromSymbol(s, checker, visited);
58
+ if (resolved) return resolved;
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
64
+
3
65
  export function resolveFunctionFromCall(
4
66
  expr: ts.Expression,
5
67
  checker: ts.TypeChecker
6
68
  ): ts.FunctionLikeDeclaration | null {
7
-
8
69
  const type = checker.getTypeAtLocation(expr);
9
70
  const signatures = type.getCallSignatures();
10
71
 
11
72
  for (const sig of signatures) {
12
73
  const decl = sig.getDeclaration();
13
74
 
14
- if (
15
- decl &&
16
- (ts.isFunctionDeclaration(decl) ||
17
- ts.isMethodDeclaration(decl) ||
18
- ts.isFunctionExpression(decl) ||
19
- ts.isArrowFunction(decl))
20
- ) {
75
+ if (!decl) continue;
76
+
77
+ if (isFunctionLike(decl)) {
21
78
  return decl;
22
79
  }
80
+
81
+ // MethodSignature case
82
+ if (ts.isMethodSignature(decl)) {
83
+ const symbol = checker.getSymbolAtLocation(decl.name);
84
+
85
+ if (symbol) {
86
+ const resolved = resolveFromSymbol(symbol, checker);
87
+ if (resolved) return resolved;
88
+ }
89
+ }
23
90
  }
24
91
 
25
92
  return null;
@@ -1,13 +1,14 @@
1
1
  import { TSService } from "./languageService";
2
- import { scanSource } from "./scanner";
2
+ import { ScanResult, scanSource } from "./scanner";
3
3
  import { DependencyGraph } from "./graph";
4
4
  import { hash } from "./hash";
5
+ import { EventraConfig } from "../types";
5
6
 
6
7
  export class EventraEngine {
7
8
  private ts: TSService;
8
9
  private graph: DependencyGraph;
9
10
 
10
- private fileEvents = new Map<string, Set<string>>();
11
+ private fileResults = new Map<string, ScanResult>();
11
12
  private fileHash = new Map<string, string>();
12
13
 
13
14
  constructor(root: string) {
@@ -17,37 +18,46 @@ export class EventraEngine {
17
18
  this.graph = new DependencyGraph(baseUrl, paths);
18
19
  }
19
20
 
20
- scanFile(file: string, content: string) {
21
+ scanFile(file: string, content: string, config: EventraConfig): ScanResult {
21
22
  const h = hash(content);
22
23
 
23
- // cache
24
24
  if (this.fileHash.get(file) === h) {
25
- return this.fileEvents.get(file) ?? new Set();
25
+ return (
26
+ this.fileResults.get(file) ?? {
27
+ events: new Set(),
28
+ detectedFunctionWrappers: new Set(),
29
+ detectedComponentWrappers: new Map(),
30
+ }
31
+ );
26
32
  }
27
33
 
28
34
  this.fileHash.set(file, h);
29
-
30
35
  this.ts.updateFile(file, content);
31
36
 
32
37
  const source = this.ts.getSourceFile(file);
33
38
  const checker = this.ts.getChecker();
34
39
 
35
- if (!source || !checker) return new Set();
40
+ if (!source || !checker) {
41
+ return {
42
+ events: new Set(),
43
+ detectedFunctionWrappers: new Set(),
44
+ detectedComponentWrappers: new Map(),
45
+ };
46
+ }
36
47
 
37
- // обновляем graph
38
48
  this.graph.update(file, source);
39
49
 
40
- const events = scanSource(source, checker);
50
+ const res = scanSource(source, checker, config);
41
51
 
42
- this.fileEvents.set(file, events);
52
+ this.fileResults.set(file, res);
43
53
 
44
- return events;
54
+ return res;
45
55
  }
46
56
 
47
- updateFile(file: string, content: string) {
48
- const events = this.scanFile(file, content);
57
+ updateFile(file: string, content: string, config: EventraConfig) {
58
+ const res = this.scanFile(file, content, config);
49
59
 
50
- const dependents = this.graph.getDependents(file);
60
+ const dependents = this.graph.getAllDependentsDeep(file);
51
61
 
52
62
  for (const dep of dependents) {
53
63
  const source = this.ts.getSourceFile(dep);
@@ -55,26 +65,25 @@ export class EventraEngine {
55
65
 
56
66
  if (!source || !checker) continue;
57
67
 
58
- const ev = scanSource(source, checker);
59
-
60
- this.fileEvents.set(dep, ev);
68
+ const r = scanSource(source, checker, config);
69
+ this.fileResults.set(dep, r);
61
70
  }
62
71
 
63
- return events;
72
+ return res;
64
73
  }
65
74
 
66
75
  getAllEvents() {
67
76
  const all = new Set<string>();
68
77
 
69
- for (const set of this.fileEvents.values()) {
70
- set.forEach(e => all.add(e));
78
+ for (const r of this.fileResults.values()) {
79
+ r.events.forEach(e => all.add(e));
71
80
  }
72
81
 
73
82
  return [...all];
74
83
  }
75
84
 
76
85
  removeFile(file: string) {
77
- this.fileEvents.delete(file);
86
+ this.fileResults.delete(file);
78
87
  this.fileHash.delete(file);
79
88
  this.graph.remove(file);
80
89
  }
@@ -0,0 +1,39 @@
1
+ import ts from "typescript";
2
+
3
+ export function resolveExportedSymbol(
4
+ symbol: ts.Symbol,
5
+ checker: ts.TypeChecker,
6
+ visited = new Set<ts.Symbol>()
7
+ ): ts.Symbol | null {
8
+
9
+ if (visited.has(symbol)) return null;
10
+ visited.add(symbol);
11
+ if (symbol.flags & ts.SymbolFlags.Alias) {
12
+ const aliased = checker.getAliasedSymbol(symbol);
13
+ return resolveExportedSymbol(aliased, checker, visited);
14
+ }
15
+
16
+ if (symbol.getDeclarations()?.length) {
17
+ return symbol;
18
+ }
19
+
20
+ const declarations = symbol.getDeclarations() ?? [];
21
+
22
+ for (const decl of declarations) {
23
+ const source = decl.getSourceFile();
24
+ const moduleSymbol = checker.getSymbolAtLocation(source);
25
+
26
+ if (!moduleSymbol) continue;
27
+
28
+ const exports = checker.getExportsOfModule(moduleSymbol);
29
+
30
+ for (const exp of exports) {
31
+ const resolved = resolveExportedSymbol(exp, checker, visited);
32
+ if (resolved?.getDeclarations()?.length) {
33
+ return resolved;
34
+ }
35
+ }
36
+ }
37
+
38
+ return symbol;
39
+ }