@eventra_dev/eventra-cli 0.1.3 → 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.
@@ -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
+ }
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  class DependencyGraph {
11
11
  reverse = new Map();
12
+ exports = new Map();
12
13
  normalize(file) {
13
14
  return path_1.default.resolve(file);
14
15
  }
@@ -35,26 +36,55 @@ class DependencyGraph {
35
36
  }
36
37
  update(file, source) {
37
38
  const normalizedFile = this.normalize(file);
39
+ this.exports.set(normalizedFile, new Set());
38
40
  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());
41
+ // IMPORT
42
+ if (typescript_1.default.isImportDeclaration(stmt)) {
43
+ const module = stmt.moduleSpecifier.getText().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());
49
+ }
50
+ this.reverse.get(resolved).add(normalizedFile);
49
51
  }
50
- this.reverse.get(resolved).add(normalizedFile);
52
+ // EXPORT
53
+ if (typescript_1.default.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
54
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
55
+ const resolved = this.resolveImport(normalizedFile, module);
56
+ if (!resolved)
57
+ continue;
58
+ if (!this.exports.has(normalizedFile)) {
59
+ this.exports.set(normalizedFile, new Set());
60
+ }
61
+ this.exports.get(normalizedFile).add(resolved);
62
+ if (!this.reverse.has(resolved)) {
63
+ this.reverse.set(resolved, new Set());
64
+ }
65
+ this.reverse.get(resolved).add(normalizedFile);
66
+ }
67
+ }
68
+ }
69
+ getAllRelated(file, visited = new Set()) {
70
+ const normalized = this.normalize(file);
71
+ if (visited.has(normalized))
72
+ return visited;
73
+ visited.add(normalized);
74
+ const deps = this.exports.get(normalized);
75
+ if (!deps)
76
+ return visited;
77
+ for (const dep of deps) {
78
+ this.getAllRelated(dep, visited);
51
79
  }
80
+ return visited;
52
81
  }
53
82
  getDependents(file) {
54
83
  return [...(this.reverse.get(this.normalize(file)) ?? [])];
55
84
  }
56
85
  remove(file) {
57
86
  const normalized = this.normalize(file);
87
+ this.exports.delete(normalized);
58
88
  for (const set of this.reverse.values()) {
59
89
  set.delete(normalized);
60
90
  }
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
9
9
  class TSService {
10
10
  root;
11
11
  files = new Map();
12
+ fileNames = new Set();
12
13
  service;
13
14
  constructor(root) {
14
15
  this.root = root;
@@ -30,10 +31,7 @@ class TSService {
30
31
  };
31
32
  }
32
33
  const host = {
33
- getScriptFileNames: () => [
34
- ...this.files.keys(),
35
- ...typescript_1.default.sys.readDirectory(this.root, [".ts", ".tsx"], ["node_modules", "dist", ".git"])
36
- ],
34
+ getScriptFileNames: () => [...this.fileNames],
37
35
  getScriptVersion: (file) => this.files.get(file)?.version.toString() ?? "0",
38
36
  getScriptSnapshot: (file) => {
39
37
  const f = this.files.get(file);
@@ -60,6 +58,7 @@ class TSService {
60
58
  content,
61
59
  version: prev ? prev.version + 1 : 1,
62
60
  });
61
+ this.fileNames.add(file);
63
62
  }
64
63
  getProgram() {
65
64
  return this.service.getProgram();
@@ -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.4",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -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,6 +4,7 @@ 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>>();
7
8
 
8
9
  private normalize(file: string) {
9
10
  return path.resolve(file);
@@ -37,23 +38,60 @@ export class DependencyGraph {
37
38
  update(file: string, source: ts.SourceFile) {
38
39
  const normalizedFile = this.normalize(file);
39
40
 
41
+ this.exports.set(normalizedFile, new Set());
42
+
40
43
  for (const stmt of source.statements) {
41
- if (!ts.isImportDeclaration(stmt)) continue;
44
+ // IMPORT
45
+ if (ts.isImportDeclaration(stmt)) {
46
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
47
+ const resolved = this.resolveImport(normalizedFile, module);
48
+
49
+ if (!resolved) continue;
50
+
51
+ if (!this.reverse.has(resolved)) {
52
+ this.reverse.set(resolved, new Set());
53
+ }
54
+
55
+ this.reverse.get(resolved)!.add(normalizedFile);
56
+ }
57
+
58
+ // EXPORT
59
+ if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
60
+ const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
61
+ const resolved = this.resolveImport(normalizedFile, module);
42
62
 
43
- const module = stmt.moduleSpecifier
44
- .getText()
45
- .replace(/['"]/g, "");
63
+ if (!resolved) continue;
46
64
 
47
- const resolved = this.resolveImport(normalizedFile, module);
65
+ if (!this.exports.has(normalizedFile)) {
66
+ this.exports.set(normalizedFile, new Set());
67
+ }
48
68
 
49
- if (!resolved) continue;
69
+ this.exports.get(normalizedFile)!.add(resolved);
50
70
 
51
- if (!this.reverse.has(resolved)) {
52
- this.reverse.set(resolved, new Set());
71
+ if (!this.reverse.has(resolved)) {
72
+ this.reverse.set(resolved, new Set());
73
+ }
74
+
75
+ this.reverse.get(resolved)!.add(normalizedFile);
53
76
  }
77
+ }
78
+ }
79
+
80
+ getAllRelated(file: string, visited = new Set<string>()): Set<string> {
81
+ const normalized = this.normalize(file);
82
+
83
+ if (visited.has(normalized)) return visited;
84
+ visited.add(normalized);
54
85
 
55
- this.reverse.get(resolved)!.add(normalizedFile);
86
+ const deps = this.exports.get(normalized);
87
+
88
+ if (!deps) return visited;
89
+
90
+ for (const dep of deps) {
91
+ this.getAllRelated(dep, visited);
56
92
  }
93
+
94
+ return visited;
57
95
  }
58
96
 
59
97
  getDependents(file: string) {
@@ -63,6 +101,8 @@ export class DependencyGraph {
63
101
  remove(file: string) {
64
102
  const normalized = this.normalize(file);
65
103
 
104
+ this.exports.delete(normalized);
105
+
66
106
  for (const set of this.reverse.values()) {
67
107
  set.delete(normalized);
68
108
  }
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
 
4
4
  export class TSService {
5
5
  private files = new Map<string, { version: number; content: string }>();
6
+ private fileNames = new Set<string>();
6
7
  private service: ts.LanguageService;
7
8
 
8
9
  constructor(private root: string) {
@@ -40,14 +41,8 @@ export class TSService {
40
41
  }
41
42
 
42
43
  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
- ],
44
+ getScriptFileNames: () => [...this.fileNames],
45
+
51
46
  getScriptVersion: (file) =>
52
47
  this.files.get(file)?.version.toString() ?? "0",
53
48
 
@@ -87,6 +82,8 @@ export class TSService {
87
82
  content,
88
83
  version: prev ? prev.version + 1 : 1,
89
84
  });
85
+
86
+ this.fileNames.add(file);
90
87
  }
91
88
 
92
89
  getProgram() {
@@ -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
- }