@eventra_dev/eventra-cli 0.1.3 → 0.1.5

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,14 +7,17 @@ const graph_1 = require("./graph");
7
7
  const hash_1 = require("./hash");
8
8
  class EventraEngine {
9
9
  ts;
10
- graph = new graph_1.DependencyGraph();
10
+ graph;
11
11
  fileEvents = new Map();
12
12
  fileHash = new Map();
13
13
  constructor(root) {
14
14
  this.ts = new languageService_1.TSService(root);
15
+ const { baseUrl, paths } = this.ts.getModuleResolutionConfig();
16
+ this.graph = new graph_1.DependencyGraph(baseUrl, paths);
15
17
  }
16
18
  scanFile(file, content) {
17
19
  const h = (0, hash_1.hash)(content);
20
+ // cache
18
21
  if (this.fileHash.get(file) === h) {
19
22
  return this.fileEvents.get(file) ?? new Set();
20
23
  }
@@ -24,6 +27,7 @@ class EventraEngine {
24
27
  const checker = this.ts.getChecker();
25
28
  if (!source || !checker)
26
29
  return new Set();
30
+ // обновляем graph
27
31
  this.graph.update(file, source);
28
32
  const events = (0, scanner_1.scanSource)(source, checker);
29
33
  this.fileEvents.set(file, events);
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findTrackParamIndex = findTrackParamIndex;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ function findTrackParamIndex(fn) {
9
+ const paramIndex = new Map();
10
+ fn.parameters.forEach((p, i) => {
11
+ if (typescript_1.default.isIdentifier(p.name)) {
12
+ paramIndex.set(p.name.text, i);
13
+ }
14
+ });
15
+ let result = null;
16
+ function visit(node) {
17
+ if (typescript_1.default.isCallExpression(node)) {
18
+ const name = getCallName(node.expression);
19
+ if (/(track|event|capture|send)/i.test(name)) {
20
+ for (const arg of node.arguments) {
21
+ if (typescript_1.default.isIdentifier(arg)) {
22
+ const idx = paramIndex.get(arg.text);
23
+ if (idx !== undefined) {
24
+ result = idx;
25
+ return;
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ typescript_1.default.forEachChild(node, visit);
32
+ }
33
+ if (fn.body) {
34
+ visit(fn.body);
35
+ }
36
+ return result;
37
+ }
38
+ function getCallName(expr) {
39
+ if (typescript_1.default.isIdentifier(expr))
40
+ return expr.text;
41
+ if (typescript_1.default.isPropertyAccessExpression(expr)) {
42
+ return expr.name.text;
43
+ }
44
+ return "";
45
+ }
@@ -8,14 +8,48 @@ const typescript_1 = __importDefault(require("typescript"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  class DependencyGraph {
11
+ baseUrl;
12
+ paths;
11
13
  reverse = new Map();
14
+ exports = new Map();
15
+ constructor(baseUrl, paths) {
16
+ this.baseUrl = baseUrl;
17
+ this.paths = paths;
18
+ }
12
19
  normalize(file) {
13
20
  return path_1.default.resolve(file);
14
21
  }
15
- resolveImport(from, module) {
16
- if (!module.startsWith("."))
17
- return null;
18
- const base = path_1.default.resolve(path_1.default.dirname(from), module);
22
+ // TS-like resolver
23
+ resolveModule(module, from) {
24
+ // relative
25
+ if (module.startsWith(".")) {
26
+ const abs = path_1.default.resolve(path_1.default.dirname(from), module);
27
+ return this.resolveFile(abs);
28
+ }
29
+ // paths mapping
30
+ for (const key in this.paths) {
31
+ const pattern = key.replace("*", "(.*)");
32
+ const regex = new RegExp("^" + pattern + "$");
33
+ const match = module.match(regex);
34
+ if (!match)
35
+ continue;
36
+ const wildcard = match[1] || "";
37
+ for (const target of this.paths[key]) {
38
+ const replaced = target.replace("*", wildcard);
39
+ const abs = path_1.default.resolve(this.baseUrl, replaced);
40
+ const resolved = this.resolveFile(abs);
41
+ if (resolved)
42
+ return resolved;
43
+ }
44
+ }
45
+ // baseUrl fallback
46
+ const abs = path_1.default.resolve(this.baseUrl, module);
47
+ const resolved = this.resolveFile(abs);
48
+ if (resolved)
49
+ return resolved;
50
+ return null;
51
+ }
52
+ resolveFile(base) {
19
53
  const tryFiles = [
20
54
  base,
21
55
  base + ".ts",
@@ -31,23 +65,38 @@ class DependencyGraph {
31
65
  return this.normalize(f);
32
66
  }
33
67
  }
34
- return this.normalize(base);
68
+ return null;
35
69
  }
36
70
  update(file, source) {
37
71
  const normalizedFile = this.normalize(file);
72
+ this.exports.set(normalizedFile, new Set());
38
73
  for (const stmt of source.statements) {
39
- if (!typescript_1.default.isImportDeclaration(stmt))
40
- continue;
41
- const module = stmt.moduleSpecifier
42
- .getText()
43
- .replace(/['"]/g, "");
44
- const resolved = this.resolveImport(normalizedFile, module);
45
- if (!resolved)
46
- continue;
47
- if (!this.reverse.has(resolved)) {
48
- this.reverse.set(resolved, new Set());
74
+ // IMPORT
75
+ if (typescript_1.default.isImportDeclaration(stmt)) {
76
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
77
+ const resolved = this.resolveModule(module, normalizedFile);
78
+ if (!resolved)
79
+ continue;
80
+ if (!this.reverse.has(resolved)) {
81
+ this.reverse.set(resolved, new Set());
82
+ }
83
+ this.reverse.get(resolved).add(normalizedFile);
84
+ }
85
+ // EXPORT *
86
+ if (typescript_1.default.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
87
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
88
+ const resolved = this.resolveModule(module, normalizedFile);
89
+ if (!resolved)
90
+ continue;
91
+ if (!this.exports.has(normalizedFile)) {
92
+ this.exports.set(normalizedFile, new Set());
93
+ }
94
+ this.exports.get(normalizedFile).add(resolved);
95
+ if (!this.reverse.has(resolved)) {
96
+ this.reverse.set(resolved, new Set());
97
+ }
98
+ this.reverse.get(resolved).add(normalizedFile);
49
99
  }
50
- this.reverse.get(resolved).add(normalizedFile);
51
100
  }
52
101
  }
53
102
  getDependents(file) {
@@ -55,6 +104,7 @@ class DependencyGraph {
55
104
  }
56
105
  remove(file) {
57
106
  const normalized = this.normalize(file);
107
+ this.exports.delete(normalized);
58
108
  for (const set of this.reverse.values()) {
59
109
  set.delete(normalized);
60
110
  }
@@ -6,10 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TSService = void 0;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
9
10
  class TSService {
10
11
  root;
11
12
  files = new Map();
13
+ fileNames = new Set();
12
14
  service;
15
+ compilerOptions = {};
16
+ baseUrl = "";
17
+ paths = {};
13
18
  constructor(root) {
14
19
  this.root = root;
15
20
  const configPath = typescript_1.default.findConfigFile(root, typescript_1.default.sys.fileExists, "tsconfig.json");
@@ -28,12 +33,17 @@ class TSService {
28
33
  ...compilerOptions,
29
34
  ...parsed.options,
30
35
  };
36
+ this.paths = parsed.options.paths || {};
37
+ this.baseUrl = parsed.options.baseUrl
38
+ ? path_1.default.resolve(root, parsed.options.baseUrl)
39
+ : root;
31
40
  }
41
+ else {
42
+ this.baseUrl = root;
43
+ }
44
+ this.compilerOptions = compilerOptions;
32
45
  const host = {
33
- getScriptFileNames: () => [
34
- ...this.files.keys(),
35
- ...typescript_1.default.sys.readDirectory(this.root, [".ts", ".tsx"], ["node_modules", "dist", ".git"])
36
- ],
46
+ getScriptFileNames: () => [...this.fileNames],
37
47
  getScriptVersion: (file) => this.files.get(file)?.version.toString() ?? "0",
38
48
  getScriptSnapshot: (file) => {
39
49
  const f = this.files.get(file);
@@ -46,7 +56,7 @@ class TSService {
46
56
  return undefined;
47
57
  },
48
58
  getCurrentDirectory: () => this.root,
49
- getCompilationSettings: () => compilerOptions,
59
+ getCompilationSettings: () => this.compilerOptions,
50
60
  getDefaultLibFileName: (opts) => typescript_1.default.getDefaultLibFilePath(opts),
51
61
  readFile: typescript_1.default.sys.readFile,
52
62
  fileExists: typescript_1.default.sys.fileExists,
@@ -60,6 +70,7 @@ class TSService {
60
70
  content,
61
71
  version: prev ? prev.version + 1 : 1,
62
72
  });
73
+ this.fileNames.add(file);
63
74
  }
64
75
  getProgram() {
65
76
  return this.service.getProgram();
@@ -70,5 +81,11 @@ class TSService {
70
81
  getSourceFile(file) {
71
82
  return this.getProgram()?.getSourceFile(file);
72
83
  }
84
+ getModuleResolutionConfig() {
85
+ return {
86
+ baseUrl: this.baseUrl,
87
+ paths: this.paths,
88
+ };
89
+ }
73
90
  }
74
91
  exports.TSService = TSService;
@@ -7,6 +7,34 @@ 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 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
+ }
10
38
  function scanSource(source, checker, visited = new Set(), depth = 0) {
11
39
  if (depth > 5)
12
40
  return new Set();
@@ -19,8 +47,7 @@ function scanSource(source, checker, visited = new Set(), depth = 0) {
19
47
  const name = getCallName(node.expression);
20
48
  if (/(track|event|capture|send)/i.test(name)) {
21
49
  const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
22
- // param mapping
23
- let paramMap = new Map();
50
+ const paramMap = new Map();
24
51
  if (fn && typescript_1.default.isFunctionLike(fn)) {
25
52
  fn.parameters.forEach((param, index) => {
26
53
  const arg = node.arguments[index];
@@ -31,16 +58,29 @@ function scanSource(source, checker, visited = new Set(), depth = 0) {
31
58
  }
32
59
  });
33
60
  }
34
- // cross-file traversal
35
- if (fn) {
36
- const sf = fn.getSourceFile();
37
- if (!visited.has(sf.fileName)) {
38
- scanSource(sf, checker, visited, depth + 1)
39
- .forEach(e => events.add(e));
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
+ }
40
78
  }
41
79
  }
42
- for (const arg of node.arguments) {
43
- const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
80
+ // fallback (heuristic)
81
+ const target = pickBestArg(node.arguments, checker, paramMap);
82
+ if (target) {
83
+ const res = (0, resolver_1.resolveNodeValue)(target, checker, paramMap);
44
84
  res?.values.forEach(v => {
45
85
  if (v &&
46
86
  v.length > 1 &&
@@ -58,11 +98,3 @@ function scanSource(source, checker, visited = new Set(), depth = 0) {
58
98
  visit(source);
59
99
  return events;
60
100
  }
61
- function getCallName(expr) {
62
- if (typescript_1.default.isIdentifier(expr))
63
- return expr.text;
64
- if (typescript_1.default.isPropertyAccessExpression(expr)) {
65
- return expr.name.text;
66
- }
67
- return "";
68
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -5,23 +5,28 @@ import { hash } from "./hash";
5
5
 
6
6
  export class EventraEngine {
7
7
  private ts: TSService;
8
- private graph = new DependencyGraph();
8
+ private graph: DependencyGraph;
9
9
 
10
10
  private fileEvents = new Map<string, Set<string>>();
11
11
  private fileHash = new Map<string, string>();
12
12
 
13
13
  constructor(root: string) {
14
14
  this.ts = new TSService(root);
15
+
16
+ const { baseUrl, paths } = this.ts.getModuleResolutionConfig();
17
+ this.graph = new DependencyGraph(baseUrl, paths);
15
18
  }
16
19
 
17
20
  scanFile(file: string, content: string) {
18
21
  const h = hash(content);
19
22
 
23
+ // cache
20
24
  if (this.fileHash.get(file) === h) {
21
25
  return this.fileEvents.get(file) ?? new Set();
22
26
  }
23
27
 
24
28
  this.fileHash.set(file, h);
29
+
25
30
  this.ts.updateFile(file, content);
26
31
 
27
32
  const source = this.ts.getSourceFile(file);
@@ -29,7 +34,9 @@ export class EventraEngine {
29
34
 
30
35
  if (!source || !checker) return new Set();
31
36
 
37
+ // обновляем graph
32
38
  this.graph.update(file, source);
39
+
33
40
  const events = scanSource(source, checker);
34
41
 
35
42
  this.fileEvents.set(file, events);
@@ -39,6 +46,7 @@ export class EventraEngine {
39
46
 
40
47
  updateFile(file: string, content: string) {
41
48
  const events = this.scanFile(file, content);
49
+
42
50
  const dependents = this.graph.getDependents(file);
43
51
 
44
52
  for (const dep of dependents) {
@@ -48,6 +56,7 @@ export class EventraEngine {
48
56
  if (!source || !checker) continue;
49
57
 
50
58
  const ev = scanSource(source, checker);
59
+
51
60
  this.fileEvents.set(dep, ev);
52
61
  }
53
62
 
@@ -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
+ }
@@ -4,16 +4,53 @@ import fs from "fs";
4
4
 
5
5
  export class DependencyGraph {
6
6
  private reverse = new Map<string, Set<string>>();
7
+ private exports = new Map<string, Set<string>>();
8
+
9
+ constructor(
10
+ private baseUrl: string,
11
+ private paths: Record<string, string[]>
12
+ ) {}
7
13
 
8
14
  private normalize(file: string) {
9
15
  return path.resolve(file);
10
16
  }
11
17
 
12
- private resolveImport(from: string, module: string): string | null {
13
- if (!module.startsWith(".")) return null;
18
+ // TS-like resolver
19
+ private resolveModule(module: string, from: string): string | null {
20
+ // relative
21
+ if (module.startsWith(".")) {
22
+ const abs = path.resolve(path.dirname(from), module);
23
+ return this.resolveFile(abs);
24
+ }
25
+
26
+ // paths mapping
27
+ for (const key in this.paths) {
28
+ const pattern = key.replace("*", "(.*)");
29
+ const regex = new RegExp("^" + pattern + "$");
30
+ const match = module.match(regex);
31
+
32
+ if (!match) continue;
33
+
34
+ const wildcard = match[1] || "";
35
+
36
+ for (const target of this.paths[key]) {
37
+ const replaced = target.replace("*", wildcard);
38
+ const abs = path.resolve(this.baseUrl, replaced);
39
+
40
+ const resolved = this.resolveFile(abs);
41
+ if (resolved) return resolved;
42
+ }
43
+ }
44
+
45
+ // baseUrl fallback
46
+ const abs = path.resolve(this.baseUrl, module);
47
+ const resolved = this.resolveFile(abs);
48
+ if (resolved) return resolved;
14
49
 
15
- const base = path.resolve(path.dirname(from), module);
50
+ return null;
51
+ }
16
52
 
53
+ private resolveFile(base: string): string | null {
17
54
  const tryFiles = [
18
55
  base,
19
56
  base + ".ts",
@@ -31,28 +68,48 @@ export class DependencyGraph {
31
68
  }
32
69
  }
33
70
 
34
- return this.normalize(base);
71
+ return null;
35
72
  }
36
73
 
37
74
  update(file: string, source: ts.SourceFile) {
38
75
  const normalizedFile = this.normalize(file);
39
76
 
40
- for (const stmt of source.statements) {
41
- if (!ts.isImportDeclaration(stmt)) continue;
77
+ this.exports.set(normalizedFile, new Set());
42
78
 
43
- const module = stmt.moduleSpecifier
44
- .getText()
45
- .replace(/['"]/g, "");
79
+ for (const stmt of source.statements) {
80
+ // IMPORT
81
+ if (ts.isImportDeclaration(stmt)) {
82
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
46
83
 
47
- const resolved = this.resolveImport(normalizedFile, module);
84
+ const resolved = this.resolveModule(module, normalizedFile);
85
+ if (!resolved) continue;
48
86
 
49
- if (!resolved) continue;
87
+ if (!this.reverse.has(resolved)) {
88
+ this.reverse.set(resolved, new Set());
89
+ }
50
90
 
51
- if (!this.reverse.has(resolved)) {
52
- this.reverse.set(resolved, new Set());
91
+ this.reverse.get(resolved)!.add(normalizedFile);
53
92
  }
54
93
 
55
- this.reverse.get(resolved)!.add(normalizedFile);
94
+ // EXPORT *
95
+ if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
96
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
97
+
98
+ const resolved = this.resolveModule(module, normalizedFile);
99
+ if (!resolved) continue;
100
+
101
+ if (!this.exports.has(normalizedFile)) {
102
+ this.exports.set(normalizedFile, new Set());
103
+ }
104
+
105
+ this.exports.get(normalizedFile)!.add(resolved);
106
+
107
+ if (!this.reverse.has(resolved)) {
108
+ this.reverse.set(resolved, new Set());
109
+ }
110
+
111
+ this.reverse.get(resolved)!.add(normalizedFile);
112
+ }
56
113
  }
57
114
  }
58
115
 
@@ -63,6 +120,8 @@ export class DependencyGraph {
63
120
  remove(file: string) {
64
121
  const normalized = this.normalize(file);
65
122
 
123
+ this.exports.delete(normalized);
124
+
66
125
  for (const set of this.reverse.values()) {
67
126
  set.delete(normalized);
68
127
  }
@@ -1,10 +1,16 @@
1
1
  import ts from "typescript";
2
2
  import fs from "fs";
3
+ import path from "path";
3
4
 
4
5
  export class TSService {
5
6
  private files = new Map<string, { version: number; content: string }>();
7
+ private fileNames = new Set<string>();
6
8
  private service: ts.LanguageService;
7
9
 
10
+ private readonly compilerOptions: ts.CompilerOptions = {};
11
+ private readonly baseUrl: string = "";
12
+ private readonly paths: Record<string, string[]> = {};
13
+
8
14
  constructor(private root: string) {
9
15
  const configPath = ts.findConfigFile(
10
16
  root,
@@ -22,10 +28,7 @@ export class TSService {
22
28
  };
23
29
 
24
30
  if (configPath) {
25
- const configFile = ts.readConfigFile(
26
- configPath,
27
- ts.sys.readFile
28
- );
31
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
29
32
 
30
33
  const parsed = ts.parseJsonConfigFileContent(
31
34
  configFile.config,
@@ -37,17 +40,20 @@ export class TSService {
37
40
  ...compilerOptions,
38
41
  ...parsed.options,
39
42
  };
43
+
44
+ this.paths = parsed.options.paths || {};
45
+ this.baseUrl = parsed.options.baseUrl
46
+ ? path.resolve(root, parsed.options.baseUrl)
47
+ : root;
48
+ } else {
49
+ this.baseUrl = root;
40
50
  }
41
51
 
52
+ this.compilerOptions = compilerOptions;
53
+
42
54
  const host: ts.LanguageServiceHost = {
43
- getScriptFileNames: () => [
44
- ...this.files.keys(),
45
- ...ts.sys.readDirectory(
46
- this.root,
47
- [".ts", ".tsx"],
48
- ["node_modules", "dist", ".git"]
49
- )
50
- ],
55
+ getScriptFileNames: () => [...this.fileNames],
56
+
51
57
  getScriptVersion: (file) =>
52
58
  this.files.get(file)?.version.toString() ?? "0",
53
59
 
@@ -68,7 +74,7 @@ export class TSService {
68
74
  },
69
75
 
70
76
  getCurrentDirectory: () => this.root,
71
- getCompilationSettings: () => compilerOptions,
77
+ getCompilationSettings: () => this.compilerOptions,
72
78
  getDefaultLibFileName: (opts) =>
73
79
  ts.getDefaultLibFilePath(opts),
74
80
 
@@ -87,6 +93,8 @@ export class TSService {
87
93
  content,
88
94
  version: prev ? prev.version + 1 : 1,
89
95
  });
96
+
97
+ this.fileNames.add(file);
90
98
  }
91
99
 
92
100
  getProgram() {
@@ -100,4 +108,11 @@ export class TSService {
100
108
  getSourceFile(file: string) {
101
109
  return this.getProgram()?.getSourceFile(file);
102
110
  }
111
+
112
+ getModuleResolutionConfig() {
113
+ return {
114
+ baseUrl: this.baseUrl,
115
+ paths: this.paths,
116
+ };
117
+ }
103
118
  }
@@ -1,6 +1,43 @@
1
1
  import ts from "typescript";
2
2
  import { resolveNodeValue } from "./resolver";
3
3
  import { resolveFunctionFromCall } from "./callResolver";
4
+ import { findTrackParamIndex } from "./functionAnalyzer";
5
+
6
+ function getCallName(expr: ts.Expression): string {
7
+ if (ts.isIdentifier(expr)) return expr.text;
8
+
9
+ if (ts.isPropertyAccessExpression(expr)) {
10
+ return expr.name.text;
11
+ }
12
+
13
+ return "";
14
+ }
15
+
16
+ function pickBestArg(
17
+ args: readonly ts.Expression[],
18
+ checker: ts.TypeChecker,
19
+ paramMap: Map<string, ts.Expression>
20
+ ) {
21
+ let best: { score: number; node: ts.Expression } | null = null;
22
+
23
+ for (const arg of args) {
24
+ const res = resolveNodeValue(arg, checker, paramMap);
25
+
26
+ if (!res?.values?.length) continue;
27
+
28
+ let score = 0;
29
+
30
+ if (!res.dynamic) score += 3;
31
+ if (res.values.every(v => v.length < 50)) score += 2;
32
+ if (res.values.some(v => v.includes("*"))) score -= 2;
33
+
34
+ if (!best || score > best.score) {
35
+ best = { score, node: arg };
36
+ }
37
+ }
38
+
39
+ return best?.node;
40
+ }
4
41
 
5
42
  export function scanSource(
6
43
  source: ts.SourceFile,
@@ -9,6 +46,7 @@ export function scanSource(
9
46
  depth = 0
10
47
  ): Set<string> {
11
48
  if (depth > 5) return new Set();
49
+
12
50
  const events = new Set<string>();
13
51
 
14
52
  if (visited.has(source.fileName)) return events;
@@ -19,11 +57,9 @@ export function scanSource(
19
57
  const name = getCallName(node.expression);
20
58
 
21
59
  if (/(track|event|capture|send)/i.test(name)) {
22
-
23
60
  const fn = resolveFunctionFromCall(node.expression, checker);
24
61
 
25
- // param mapping
26
- let paramMap = new Map<string, ts.Expression>();
62
+ const paramMap = new Map<string, ts.Expression>();
27
63
 
28
64
  if (fn && ts.isFunctionLike(fn)) {
29
65
  fn.parameters.forEach((param, index) => {
@@ -36,18 +72,37 @@ export function scanSource(
36
72
  });
37
73
  }
38
74
 
39
- // cross-file traversal
40
- if (fn) {
41
- const sf = fn.getSourceFile();
42
-
43
- if (!visited.has(sf.fileName)) {
44
- scanSource(sf, checker, visited, depth + 1)
45
- .forEach(e => events.add(e));
75
+ if (fn && ts.isFunctionLike(fn)) {
76
+ const idx = findTrackParamIndex(fn);
77
+
78
+ if (idx !== null) {
79
+ const realArg = node.arguments[idx];
80
+
81
+ if (realArg) {
82
+ const res = resolveNodeValue(realArg, checker, paramMap);
83
+
84
+ res?.values.forEach(v => {
85
+ if (
86
+ v &&
87
+ v.length > 1 &&
88
+ v.length < 100 &&
89
+ !v.includes(" ") &&
90
+ /^[a-zA-Z0-9._:-]+$/.test(v)
91
+ ) {
92
+ events.add(v);
93
+ }
94
+ });
95
+
96
+ return;
97
+ }
46
98
  }
47
99
  }
48
100
 
49
- for (const arg of node.arguments) {
50
- const res = resolveNodeValue(arg, checker, paramMap);
101
+ // fallback (heuristic)
102
+ const target = pickBestArg(node.arguments, checker, paramMap);
103
+
104
+ if (target) {
105
+ const res = resolveNodeValue(target, checker, paramMap);
51
106
 
52
107
  res?.values.forEach(v => {
53
108
  if (
@@ -71,13 +126,3 @@ export function scanSource(
71
126
 
72
127
  return events;
73
128
  }
74
-
75
- function getCallName(expr: ts.Expression): string {
76
- if (ts.isIdentifier(expr)) return expr.text;
77
-
78
- if (ts.isPropertyAccessExpression(expr)) {
79
- return expr.name.text;
80
- }
81
-
82
- return "";
83
- }