@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.
@@ -5,7 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveNodeValue = resolveNodeValue;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
- function resolveNodeValue(node, checker) {
8
+ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
9
+ if (seen.has(node))
10
+ return null;
11
+ seen.add(node);
12
+ // param mapping
13
+ if (typescript_1.default.isIdentifier(node) && paramMap?.has(node.text)) {
14
+ return resolveNodeValue(paramMap.get(node.text), checker, paramMap);
15
+ }
9
16
  if (typescript_1.default.isStringLiteral(node)) {
10
17
  return { values: [node.text], dynamic: false };
11
18
  }
@@ -18,8 +25,8 @@ function resolveNodeValue(node, checker) {
18
25
  return { values: [result], dynamic: true };
19
26
  }
20
27
  if (typescript_1.default.isConditionalExpression(node)) {
21
- const a = resolveNodeValue(node.whenTrue, checker);
22
- const b = resolveNodeValue(node.whenFalse, checker);
28
+ const a = resolveNodeValue(node.whenTrue, checker, paramMap, seen);
29
+ const b = resolveNodeValue(node.whenFalse, checker, paramMap, seen);
23
30
  return {
24
31
  values: [...new Set([...(a?.values ?? []), ...(b?.values ?? [])])],
25
32
  dynamic: true,
@@ -34,7 +41,7 @@ function resolveNodeValue(node, checker) {
34
41
  }
35
42
  for (const decl of symbol.getDeclarations() ?? []) {
36
43
  if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
37
- return resolveNodeValue(decl.initializer, checker);
44
+ return resolveNodeValue(decl.initializer, checker, paramMap, seen);
38
45
  }
39
46
  if (typescript_1.default.isEnumMember(decl) && decl.initializer) {
40
47
  if (typescript_1.default.isStringLiteral(decl.initializer)) {
@@ -65,5 +72,31 @@ function resolveNodeValue(node, checker) {
65
72
  }
66
73
  }
67
74
  }
75
+ if (typescript_1.default.isObjectLiteralExpression(node)) {
76
+ const values = [];
77
+ for (const prop of node.properties) {
78
+ if (!typescript_1.default.isPropertyAssignment(prop))
79
+ continue;
80
+ const name = prop.name.getText();
81
+ if (name === "event") {
82
+ const res = resolveNodeValue(prop.initializer, checker, paramMap, seen);
83
+ if (res)
84
+ values.push(...res.values);
85
+ }
86
+ }
87
+ return { values, dynamic: true };
88
+ }
89
+ if (typescript_1.default.isBinaryExpression(node)) {
90
+ if (node.operatorToken.kind === typescript_1.default.SyntaxKind.PlusToken) {
91
+ const left = resolveNodeValue(node.left, checker, paramMap, seen);
92
+ const right = resolveNodeValue(node.right, checker, paramMap, seen);
93
+ if (left && right) {
94
+ return {
95
+ values: left.values.flatMap(l => right.values.map(r => l + r)),
96
+ dynamic: true,
97
+ };
98
+ }
99
+ }
100
+ }
68
101
  return null;
69
102
  }
@@ -7,7 +7,37 @@ 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
- function scanSource(source, checker, visited = new Set()) {
10
+ const functionAnalyzer_1 = require("./functionAnalyzer");
11
+ function getCallName(expr) {
12
+ if (typescript_1.default.isIdentifier(expr))
13
+ return expr.text;
14
+ if (typescript_1.default.isPropertyAccessExpression(expr)) {
15
+ return expr.name.text;
16
+ }
17
+ return "";
18
+ }
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 };
34
+ }
35
+ }
36
+ return best?.node;
37
+ }
38
+ function scanSource(source, checker, visited = new Set(), depth = 0) {
39
+ if (depth > 5)
40
+ return new Set();
11
41
  const events = new Set();
12
42
  if (visited.has(source.fileName))
13
43
  return events;
@@ -16,19 +46,47 @@ function scanSource(source, checker, visited = new Set()) {
16
46
  if (typescript_1.default.isCallExpression(node)) {
17
47
  const name = getCallName(node.expression);
18
48
  if (/(track|event|capture|send)/i.test(name)) {
19
- let target = node.arguments[0];
20
49
  const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
21
- // 🔥 cross-file
22
- if (fn) {
23
- const sf = fn.getSourceFile();
24
- if (!visited.has(sf.fileName)) {
25
- scanSource(sf, checker, visited).forEach(e => events.add(e));
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
+ }
59
+ });
60
+ }
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
+ }
26
78
  }
27
79
  }
80
+ // fallback (heuristic)
81
+ const target = pickBestArg(node.arguments, checker, paramMap);
28
82
  if (target) {
29
- const res = (0, resolver_1.resolveNodeValue)(target, checker);
83
+ const res = (0, resolver_1.resolveNodeValue)(target, checker, paramMap);
30
84
  res?.values.forEach(v => {
31
- if (v && v.length > 1 && !v.includes(" ")) {
85
+ if (v &&
86
+ v.length > 1 &&
87
+ v.length < 100 &&
88
+ !v.includes(" ") &&
89
+ /^[a-zA-Z0-9._:-]+$/.test(v)) {
32
90
  events.add(v);
33
91
  }
34
92
  });
@@ -40,11 +98,3 @@ function scanSource(source, checker, visited = new Set()) {
40
98
  visit(source);
41
99
  return events;
42
100
  }
43
- function getCallName(expr) {
44
- if (typescript_1.default.isIdentifier(expr))
45
- return expr.text;
46
- if (typescript_1.default.isPropertyAccessExpression(expr)) {
47
- return expr.name.text;
48
- }
49
- return "";
50
- }
@@ -36,8 +36,13 @@ async function loadConfig() {
36
36
  if (!(await fs_extra_1.default.pathExists(configPath))) {
37
37
  return null;
38
38
  }
39
- const config = await fs_extra_1.default.readJSON(configPath);
40
- return normalizeConfig(config);
39
+ try {
40
+ const config = await fs_extra_1.default.readJSON(configPath);
41
+ return normalizeConfig(config);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
41
46
  }
42
47
  async function saveConfig(config) {
43
48
  const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
@@ -2,5 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseAstro = parseAstro;
4
4
  function parseAstro(content) {
5
- return content;
5
+ const frontmatter = content.match(/---([\s\S]*?)---/);
6
+ const scripts = [
7
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
8
+ ];
9
+ return [
10
+ frontmatter?.[1] ?? "",
11
+ ...scripts.map(s => s[1])
12
+ ].join("\n");
6
13
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectParser = detectParser;
4
4
  function detectParser(file) {
5
+ file = file.toLowerCase();
5
6
  if (file.endsWith(".vue"))
6
7
  return "vue";
7
8
  if (file.endsWith(".svelte"))
@@ -2,5 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseSvelte = parseSvelte;
4
4
  function parseSvelte(content) {
5
- return content;
5
+ const scriptMatches = [
6
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
7
+ ];
8
+ return scriptMatches.map(m => m[1]).join("\n");
6
9
  }
@@ -2,15 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseVue = parseVue;
4
4
  function parseVue(content) {
5
- const scriptMatch = content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/);
5
+ const scriptMatches = [
6
+ ...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)
7
+ ];
8
+ const script = scriptMatches.map(m => m[1]).join("\n");
9
+ // template
6
10
  const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
7
- const script = scriptMatch?.[1] ?? "";
8
11
  let template = templateMatch?.[1] ?? "";
9
12
  template = template.replace(/<!--[\s\S]*?-->/g, "");
13
+ // Vue → JSX
10
14
  template = template
15
+ .replace(/\bclass=/g, "className=")
16
+ .replace(/\bfor=/g, "htmlFor=")
17
+ .replace(/@(\w+)=/g, (_, e) => {
18
+ const name = e.charAt(0).toUpperCase() + e.slice(1);
19
+ return `on${name}=`;
20
+ })
11
21
  .replace(/v-if="[^"]+"/g, "")
12
- .replace(/v-for="[^"]+"/g, "")
13
- .replace(/@click=/g, "onClick=");
22
+ .replace(/v-for="[^"]+"/g, "");
14
23
  const jsx = `
15
24
  const __VUE_JSX__ = (
16
25
  <>
@@ -13,7 +13,9 @@ function processFile(file, content) {
13
13
  content = (0, svelte_1.parseSvelte)(content);
14
14
  if (parser === "astro")
15
15
  content = (0, astro_1.parseAstro)(content);
16
- const virtualFile = file.endsWith(".vue")
16
+ const virtualFile = file.endsWith(".vue") ||
17
+ file.endsWith(".svelte") ||
18
+ file.endsWith(".astro")
17
19
  ? file + ".tsx"
18
20
  : file;
19
21
  return { content, virtualFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -30,8 +30,12 @@ export async function check({ fix = false }: { fix?: boolean }) {
30
30
 
31
31
  engine.scanFile(virtualFile, content);
32
32
 
33
- } catch {
34
- console.log(chalk.gray(`Skipped: ${file}`));
33
+ } catch (err) {
34
+ console.log(chalk.yellow(`Skipped: ${file}`));
35
+
36
+ if (err instanceof Error) {
37
+ console.log(chalk.gray(err.message));
38
+ }
35
39
  }
36
40
  }
37
41
 
@@ -39,14 +43,17 @@ export async function check({ fix = false }: { fix?: boolean }) {
39
43
  const known = new Set(config.events ?? []);
40
44
 
41
45
  const newEvents = [...found].filter(e => !known.has(e));
46
+ const removed = [...known].filter(e => !found.has(e));
42
47
 
43
48
  if (fix) {
49
+ const merged = new Set(config.events ?? []);
50
+
44
51
  newEvents.forEach(e => {
45
- config.events.push(e);
52
+ merged.add(e);
46
53
  console.log(chalk.green(`+ ${e}`));
47
54
  });
48
55
 
49
- config.events = [...new Set(config.events)].sort();
56
+ config.events = [...merged].sort();
50
57
 
51
58
  await saveConfig(config);
52
59
 
@@ -54,9 +61,16 @@ export async function check({ fix = false }: { fix?: boolean }) {
54
61
  return;
55
62
  }
56
63
 
57
- if (newEvents.length) {
58
- console.log(chalk.red("\nNew events:"));
59
- newEvents.forEach(e => console.log(chalk.red(`+ ${e}`)));
64
+ if (newEvents.length || removed.length) {
65
+ if (newEvents.length) {
66
+ console.log(chalk.red("\nNew events:"));
67
+ newEvents.forEach(e => console.log(chalk.red(`+ ${e}`)));
68
+ }
69
+
70
+ if (removed.length) {
71
+ console.log(chalk.yellow("\nRemoved events:"));
72
+ removed.forEach(e => console.log(chalk.yellow(`- ${e}`)));
73
+ }
60
74
 
61
75
  process.exit(1);
62
76
  }
@@ -81,6 +81,9 @@ export async function send() {
81
81
  )
82
82
  );
83
83
 
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), 10000);
86
+
84
87
  try {
85
88
  const res = await fetch(
86
89
  endpoint,
@@ -99,8 +102,9 @@ export async function send() {
99
102
  version: CLI_VERSION,
100
103
  runtime: "node"
101
104
  }
102
- })
103
- }
105
+ }),
106
+ signal: controller.signal
107
+ },
104
108
  );
105
109
 
106
110
  if (!res.ok) {
@@ -203,5 +207,7 @@ export async function send() {
203
207
  )
204
208
  );
205
209
  }
210
+ } finally {
211
+ clearTimeout(timeout);
206
212
  }
207
213
  }
@@ -31,9 +31,12 @@ export async function sync() {
31
31
 
32
32
  engine.scanFile(virtualFile, content);
33
33
 
34
- } catch (e) {
35
- console.log(chalk.red(`ERROR: ${file}`));
36
- console.error(e);
34
+ } catch (err) {
35
+ console.log(chalk.yellow(`Skipped: ${file}`));
36
+
37
+ if (err instanceof Error) {
38
+ console.log(chalk.gray(err.message));
39
+ }
37
40
  }
38
41
  }
39
42
 
@@ -5,31 +5,8 @@ import fg from "fast-glob";
5
5
 
6
6
  import { loadConfig, saveConfig } from "../utils/config";
7
7
  import { EventraEngine } from "../engine/engine";
8
+ import { processFile } from "../utils/processFile";
8
9
 
9
- import { detectParser } from "../utils/parsers/router";
10
- import { parseVue } from "../utils/parsers/vue";
11
- import { parseSvelte } from "../utils/parsers/svelte";
12
- import { parseAstro } from "../utils/parsers/astro";
13
-
14
- // PREPROCESS (CONTENT)
15
- function preprocess(file: string, content: string) {
16
- const parser = detectParser(file);
17
-
18
- if (parser === "vue") return parseVue(content);
19
- if (parser === "svelte") return parseSvelte(content);
20
- if (parser === "astro") return parseAstro(content);
21
-
22
- return content;
23
- }
24
-
25
- // VIRTUAL FILE (PATH)
26
- function toVirtualFile(file: string) {
27
- return file.endsWith(".vue")
28
- ? file + ".tsx"
29
- : file;
30
- }
31
-
32
- // WATCH
33
10
  export async function watch() {
34
11
  const config = await loadConfig();
35
12
  if (!config) return;
@@ -40,20 +17,13 @@ export async function watch() {
40
17
  ignore: config.sync.exclude,
41
18
  });
42
19
 
43
- if (!files.length) {
44
- console.log(chalk.yellow("No files found"));
45
- return;
46
- }
47
-
48
20
  const engine = new EventraEngine(process.cwd());
49
21
 
50
- // INITIAL SCAN
51
22
  for (const file of files) {
52
23
  try {
53
24
  const raw = await fs.readFile(file, "utf-8");
54
25
 
55
- const content = preprocess(file, raw);
56
- const virtualFile = toVirtualFile(file);
26
+ const { content, virtualFile } = processFile(file, raw);
57
27
 
58
28
  engine.scanFile(virtualFile, content);
59
29
 
@@ -69,9 +39,9 @@ export async function watch() {
69
39
  chalk.gray(`Initial: ${config.events.length} events\n`)
70
40
  );
71
41
 
72
- // STATE
73
42
  let locked = false;
74
43
  let queued = new Set<string>();
44
+ let timer: NodeJS.Timeout | null = null;
75
45
 
76
46
  const run = async () => {
77
47
  if (locked) return;
@@ -84,8 +54,7 @@ export async function watch() {
84
54
  try {
85
55
  const raw = await fs.readFile(file, "utf-8");
86
56
 
87
- const content = preprocess(file, raw);
88
- const virtualFile = toVirtualFile(file);
57
+ const { content, virtualFile } = processFile(file, raw);
89
58
 
90
59
  engine.updateFile(virtualFile, content);
91
60
 
@@ -116,48 +85,33 @@ export async function watch() {
116
85
 
117
86
  const schedule = (file: string) => {
118
87
  queued.add(file);
119
- setTimeout(run, 50);
88
+
89
+ if (timer) clearTimeout(timer);
90
+
91
+ timer = setTimeout(run, 100);
120
92
  };
121
93
 
122
- // WATCHER
123
94
  const watcher = chokidar.watch(config.sync.include, {
124
95
  ignored: config.sync.exclude,
125
96
  ignoreInitial: true,
126
- awaitWriteFinish: {
127
- stabilityThreshold: 50,
128
- pollInterval: 10,
129
- },
130
97
  });
131
98
 
132
99
  watcher
133
- .on("add", (file) => {
134
- console.log(chalk.gray(`add: ${file}`));
135
- schedule(file);
136
- })
137
- .on("change", (file) => {
138
- console.log(chalk.gray(`change: ${file}`));
139
- schedule(file);
140
- })
100
+ .on("add", schedule)
101
+ .on("change", schedule)
141
102
  .on("unlink", async (file) => {
142
103
  console.log(chalk.gray(`remove: ${file}`));
143
104
 
144
- const virtualFile = toVirtualFile(file);
105
+ const { virtualFile } = processFile(file, "");
145
106
 
146
- engine.removeFile?.(virtualFile);
107
+ engine.removeFile(virtualFile);
147
108
 
148
109
  const next = engine.getAllEvents();
149
110
 
150
- const removed = config.events.filter(e => !next.includes(e));
151
-
152
- if (removed.length) {
153
- removed.forEach(e => console.log(chalk.red(`- ${e}`)));
154
-
155
- config.events = next.sort();
156
- await saveConfig(config);
157
- }
111
+ config.events = next.sort();
112
+ await saveConfig(config);
158
113
  });
159
114
 
160
- // EXIT
161
115
  process.on("SIGINT", async () => {
162
116
  console.log(chalk.yellow("\nStopping watcher..."));
163
117
  await watcher.close();
@@ -5,38 +5,21 @@ export function resolveFunctionFromCall(
5
5
  checker: ts.TypeChecker
6
6
  ): ts.FunctionLikeDeclaration | null {
7
7
 
8
- let symbol = checker.getSymbolAtLocation(expr);
8
+ const type = checker.getTypeAtLocation(expr);
9
+ const signatures = type.getCallSignatures();
9
10
 
10
- if (!symbol && ts.isPropertyAccessExpression(expr)) {
11
- symbol = checker.getSymbolAtLocation(expr.name);
12
- }
13
-
14
- if (!symbol) return null;
15
-
16
- if (symbol.flags & ts.SymbolFlags.Alias) {
17
- symbol = checker.getAliasedSymbol(symbol);
18
- }
11
+ for (const sig of signatures) {
12
+ const decl = sig.getDeclaration();
19
13
 
20
- for (const decl of symbol.getDeclarations() ?? []) {
21
14
  if (
22
- ts.isFunctionDeclaration(decl) ||
23
- ts.isMethodDeclaration(decl) ||
24
- ts.isFunctionExpression(decl) ||
25
- ts.isArrowFunction(decl)
15
+ decl &&
16
+ (ts.isFunctionDeclaration(decl) ||
17
+ ts.isMethodDeclaration(decl) ||
18
+ ts.isFunctionExpression(decl) ||
19
+ ts.isArrowFunction(decl))
26
20
  ) {
27
21
  return decl;
28
22
  }
29
-
30
- if (ts.isVariableDeclaration(decl) && decl.initializer) {
31
- const init = decl.initializer;
32
-
33
- if (
34
- ts.isArrowFunction(init) ||
35
- ts.isFunctionExpression(init)
36
- ) {
37
- return init;
38
- }
39
- }
40
23
  }
41
24
 
42
25
  return null;
@@ -22,7 +22,6 @@ export class EventraEngine {
22
22
  }
23
23
 
24
24
  this.fileHash.set(file, h);
25
-
26
25
  this.ts.updateFile(file, content);
27
26
 
28
27
  const source = this.ts.getSourceFile(file);
@@ -31,7 +30,6 @@ export class EventraEngine {
31
30
  if (!source || !checker) return new Set();
32
31
 
33
32
  this.graph.update(file, source);
34
-
35
33
  const events = scanSource(source, checker);
36
34
 
37
35
  this.fileEvents.set(file, events);
@@ -40,7 +38,20 @@ export class EventraEngine {
40
38
  }
41
39
 
42
40
  updateFile(file: string, content: string) {
43
- return this.scanFile(file, content);
41
+ const events = this.scanFile(file, content);
42
+ const dependents = this.graph.getDependents(file);
43
+
44
+ for (const dep of dependents) {
45
+ const source = this.ts.getSourceFile(dep);
46
+ const checker = this.ts.getChecker();
47
+
48
+ if (!source || !checker) continue;
49
+
50
+ const ev = scanSource(source, checker);
51
+ this.fileEvents.set(dep, ev);
52
+ }
53
+
54
+ return events;
44
55
  }
45
56
 
46
57
  getAllEvents() {
@@ -56,5 +67,6 @@ export class EventraEngine {
56
67
  removeFile(file: string) {
57
68
  this.fileEvents.delete(file);
58
69
  this.fileHash.delete(file);
70
+ this.graph.remove(file);
59
71
  }
60
72
  }
@@ -0,0 +1,51 @@
1
+ import ts from "typescript";
2
+
3
+ export function findTrackParamIndex(
4
+ fn: ts.FunctionLikeDeclaration
5
+ ): number | null {
6
+ const paramIndex = new Map<string, number>();
7
+
8
+ fn.parameters.forEach((p, i) => {
9
+ if (ts.isIdentifier(p.name)) {
10
+ paramIndex.set(p.name.text, i);
11
+ }
12
+ });
13
+
14
+ let result: number | null = null;
15
+
16
+ function visit(node: ts.Node) {
17
+ if (ts.isCallExpression(node)) {
18
+ const name = getCallName(node.expression);
19
+
20
+ if (/(track|event|capture|send)/i.test(name)) {
21
+ for (const arg of node.arguments) {
22
+ if (ts.isIdentifier(arg)) {
23
+ const idx = paramIndex.get(arg.text);
24
+ if (idx !== undefined) {
25
+ result = idx;
26
+ return;
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ ts.forEachChild(node, visit);
34
+ }
35
+
36
+ if (fn.body) {
37
+ visit(fn.body);
38
+ }
39
+
40
+ return result;
41
+ }
42
+
43
+ function getCallName(expr: ts.Expression): string {
44
+ if (ts.isIdentifier(expr)) return expr.text;
45
+
46
+ if (ts.isPropertyAccessExpression(expr)) {
47
+ return expr.name.text;
48
+ }
49
+
50
+ return "";
51
+ }