@eventra_dev/eventra-cli 0.1.7 → 0.1.9

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,14 @@
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.isExternalFile = isExternalFile;
7
+ const path_1 = __importDefault(require("path"));
8
+ function isExternalFile(file) {
9
+ const normalized = path_1.default.resolve(file);
10
+ return (normalized.includes(`${path_1.default.sep}node_modules${path_1.default.sep}`) ||
11
+ normalized.includes(`${path_1.default.sep}dist${path_1.default.sep}`) ||
12
+ normalized.includes(`${path_1.default.sep}build${path_1.default.sep}`) ||
13
+ normalized.endsWith(".d.ts"));
14
+ }
@@ -12,20 +12,23 @@ function isFunctionLike(node) {
12
12
  typescript_1.default.isFunctionExpression(node) ||
13
13
  typescript_1.default.isArrowFunction(node));
14
14
  }
15
- function resolveFromSymbol(symbol, checker, visited = new Set()) {
15
+ function resolveFromSymbol(symbol, checker, visited = new Set(), depth = 0) {
16
16
  if (visited.has(symbol))
17
17
  return null;
18
+ if (depth > 5)
19
+ return null;
18
20
  visited.add(symbol);
19
- // alias unwrap
21
+ // unwrap alias
20
22
  if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
21
23
  const aliased = checker.getAliasedSymbol(symbol);
22
- return resolveFromSymbol(aliased, checker, visited);
24
+ return resolveFromSymbol(aliased, checker, visited, depth + 1);
23
25
  }
24
26
  const decls = symbol.getDeclarations() ?? [];
25
27
  for (const decl of decls) {
26
28
  if (isFunctionLike(decl)) {
27
29
  return decl;
28
30
  }
31
+ // MethodSignature
29
32
  if (typescript_1.default.isMethodSignature(decl)) {
30
33
  const parent = decl.parent;
31
34
  if (typescript_1.default.isInterfaceDeclaration(parent) || typescript_1.default.isTypeLiteralNode(parent)) {
@@ -35,16 +38,16 @@ function resolveFromSymbol(symbol, checker, visited = new Set()) {
35
38
  for (const prop of props) {
36
39
  if (prop.name !== name)
37
40
  continue;
38
- const resolved = resolveFromSymbol(prop, checker, visited);
41
+ const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
39
42
  if (resolved)
40
43
  return resolved;
41
44
  }
42
45
  }
43
46
  }
44
- // fallback
47
+ // SAFE fallback
45
48
  const s = checker.getSymbolAtLocation(decl);
46
- if (s) {
47
- const resolved = resolveFromSymbol(s, checker, visited);
49
+ if (s && s !== symbol) {
50
+ const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
48
51
  if (resolved)
49
52
  return resolved;
50
53
  }
@@ -61,7 +64,7 @@ function resolveFunctionFromCall(expr, checker) {
61
64
  if (isFunctionLike(decl)) {
62
65
  return decl;
63
66
  }
64
- // MethodSignature case
67
+ // interface method
65
68
  if (typescript_1.default.isMethodSignature(decl)) {
66
69
  const symbol = checker.getSymbolAtLocation(decl.name);
67
70
  if (symbol) {
@@ -5,6 +5,7 @@ const languageService_1 = require("./languageService");
5
5
  const scanner_1 = require("./scanner");
6
6
  const graph_1 = require("./graph");
7
7
  const hash_1 = require("./hash");
8
+ const boundary_1 = require("./boundary");
8
9
  class EventraEngine {
9
10
  ts;
10
11
  graph;
@@ -15,8 +16,10 @@ class EventraEngine {
15
16
  const { baseUrl, paths } = this.ts.getModuleResolutionConfig();
16
17
  this.graph = new graph_1.DependencyGraph(baseUrl, paths);
17
18
  }
19
+ // SCAN FILE
18
20
  scanFile(file, content, config) {
19
21
  const h = (0, hash_1.hash)(content);
22
+ // skip unchanged
20
23
  if (this.fileHash.get(file) === h) {
21
24
  return (this.fileResults.get(file) ?? {
22
25
  events: new Set(),
@@ -35,24 +38,39 @@ class EventraEngine {
35
38
  detectedComponentWrappers: new Map(),
36
39
  };
37
40
  }
41
+ // update dependency graph
38
42
  this.graph.update(file, source);
39
43
  const res = (0, scanner_1.scanSource)(source, checker, config);
40
44
  this.fileResults.set(file, res);
41
45
  return res;
42
46
  }
47
+ // UPDATE FILE (incremental)
43
48
  updateFile(file, content, config) {
44
49
  const res = this.scanFile(file, content, config);
45
50
  const dependents = this.graph.getAllDependentsDeep(file);
46
51
  for (const dep of dependents) {
52
+ // skip external files
53
+ if ((0, boundary_1.isExternalFile)(dep))
54
+ continue;
47
55
  const source = this.ts.getSourceFile(dep);
48
56
  const checker = this.ts.getChecker();
49
57
  if (!source || !checker)
50
58
  continue;
59
+ const depContent = this.ts.getFileContent(dep);
60
+ if (!depContent)
61
+ continue;
62
+ const h = (0, hash_1.hash)(depContent);
63
+ // skip unchanged dependents
64
+ if (this.fileHash.get(dep) === h)
65
+ continue;
66
+ this.fileHash.set(dep, h);
67
+ this.graph.update(dep, source);
51
68
  const r = (0, scanner_1.scanSource)(source, checker, config);
52
69
  this.fileResults.set(dep, r);
53
70
  }
54
71
  return res;
55
72
  }
73
+ // GET ALL EVENTS
56
74
  getAllEvents() {
57
75
  const all = new Set();
58
76
  for (const r of this.fileResults.values()) {
@@ -60,10 +78,36 @@ class EventraEngine {
60
78
  }
61
79
  return [...all];
62
80
  }
63
- removeFile(file) {
81
+ // REMOVE FILE
82
+ removeFile(file, config) {
83
+ this.ts.removeFile(file);
64
84
  this.fileResults.delete(file);
65
85
  this.fileHash.delete(file);
66
86
  this.graph.remove(file);
87
+ const dependents = this.graph.getAllDependentsDeep(file);
88
+ for (const dep of dependents) {
89
+ if ((0, boundary_1.isExternalFile)(dep))
90
+ continue;
91
+ const source = this.ts.getSourceFile(dep);
92
+ const checker = this.ts.getChecker();
93
+ if (!source || !checker)
94
+ continue;
95
+ const content = this.ts.getFileContent(dep);
96
+ if (!content)
97
+ continue;
98
+ const h = (0, hash_1.hash)(content);
99
+ // skip unchanged
100
+ if (this.fileHash.get(dep) === h)
101
+ continue;
102
+ this.fileHash.set(dep, h);
103
+ const r = (0, scanner_1.scanSource)(source, checker, config ?? {
104
+ events: [],
105
+ wrappers: [],
106
+ functionWrappers: [],
107
+ sync: { include: [], exclude: [] },
108
+ });
109
+ this.fileResults.set(dep, r);
110
+ }
67
111
  }
68
112
  }
69
113
  exports.EventraEngine = EventraEngine;
@@ -48,6 +48,9 @@ class TSService {
48
48
  },
49
49
  getScriptVersion: (file) => this.files.get(file)?.version.toString() ?? "0",
50
50
  getScriptSnapshot: (file) => {
51
+ if (!this.fileNames.has(file)) {
52
+ return undefined;
53
+ }
51
54
  const f = this.files.get(file);
52
55
  if (f) {
53
56
  return typescript_1.default.ScriptSnapshot.fromString(f.content);
@@ -83,6 +86,13 @@ class TSService {
83
86
  getSourceFile(file) {
84
87
  return this.getProgram()?.getSourceFile(file);
85
88
  }
89
+ getFileContent(file) {
90
+ return this.files.get(file)?.content;
91
+ }
92
+ removeFile(file) {
93
+ this.files.delete(file);
94
+ this.fileNames.delete(file);
95
+ }
86
96
  getModuleResolutionConfig() {
87
97
  return {
88
98
  baseUrl: this.baseUrl,
@@ -12,7 +12,7 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
12
12
  seen.add(node);
13
13
  // param mapping
14
14
  if (typescript_1.default.isIdentifier(node) && paramMap?.has(node.text)) {
15
- return resolveNodeValue(paramMap.get(node.text), checker, paramMap);
15
+ return resolveNodeValue(paramMap.get(node.text), checker, paramMap, seen);
16
16
  }
17
17
  if (typescript_1.default.isStringLiteral(node)) {
18
18
  return { values: [node.text], dynamic: false };
@@ -37,13 +37,16 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
37
37
  let symbol = checker.getSymbolAtLocation(node);
38
38
  if (!symbol)
39
39
  return null;
40
+ // variable inline resolve FIRST
40
41
  for (const decl of symbol.getDeclarations() ?? []) {
41
42
  if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
42
43
  return resolveNodeValue(decl.initializer, checker, paramMap, seen);
43
44
  }
44
45
  }
46
+ const original = symbol;
45
47
  symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
46
- if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
48
+ // avoid alias loop
49
+ if (symbol !== original && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
47
50
  symbol = checker.getAliasedSymbol(symbol);
48
51
  }
49
52
  for (const decl of symbol.getDeclarations() ?? []) {
@@ -9,6 +9,7 @@ const resolver_1 = require("./resolver");
9
9
  const callResolver_1 = require("./callResolver");
10
10
  const exportResolver_1 = require("./exportResolver");
11
11
  const functionAnalyzer_1 = require("./functionAnalyzer");
12
+ const boundary_1 = require("./boundary");
12
13
  const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
13
14
  function getCallName(expr) {
14
15
  if (typescript_1.default.isIdentifier(expr))
@@ -51,7 +52,7 @@ function isPropUsedInTracking(fn, prop) {
51
52
  visit(fn);
52
53
  return found;
53
54
  }
54
- function scanSource(source, checker, config, visited = new Set(), depth = 0) {
55
+ function scanSource(source, checker, config, visited = new Set(), depth = 0, inheritedParamMap) {
55
56
  const events = new Set();
56
57
  const detectedFunctionWrappers = new Set();
57
58
  const detectedComponentWrappers = new Map();
@@ -68,16 +69,16 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
68
69
  const isAutoWrapper = fn && (0, functionAnalyzer_1.findTrackParamIndex)(fn) !== null;
69
70
  const isTracking = isKnownWrapper ||
70
71
  TRACKING_NAMES.has(name) ||
71
- isAutoWrapper;
72
+ (isAutoWrapper &&
73
+ fn &&
74
+ !(0, boundary_1.isExternalFile)(fn.getSourceFile().fileName));
72
75
  if (!isTracking)
73
76
  return;
74
- // AUTO DETECT WRAPPER
75
- if (!isKnownWrapper &&
76
- fn &&
77
- !TRACKING_NAMES.has(name)) {
77
+ if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
78
78
  detectedFunctionWrappers.add(name);
79
79
  }
80
- const paramMap = new Map();
80
+ // paramMap
81
+ const paramMap = new Map(inheritedParamMap ?? []);
81
82
  if (fn && typescript_1.default.isFunctionLike(fn)) {
82
83
  fn.parameters.forEach((param, i) => {
83
84
  const arg = node.arguments[i];
@@ -110,11 +111,11 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
110
111
  });
111
112
  }
112
113
  }
113
- // cross-file traversal
114
+ // cross-file WITH paramMap
114
115
  if (fn) {
115
116
  const sf = fn.getSourceFile();
116
117
  if (!visited.has(sf.fileName)) {
117
- const res = scanSource(sf, checker, config, visited, depth + 1);
118
+ const res = scanSource(sf, checker, config, visited, depth + 1, paramMap);
118
119
  res.events.forEach(e => events.add(e));
119
120
  }
120
121
  }
@@ -127,28 +128,20 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
127
128
  : node.tagName;
128
129
  let symbol = checker.getSymbolAtLocation(tagNode);
129
130
  if (symbol) {
130
- const resolved = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker);
131
- if (resolved)
132
- symbol = resolved;
131
+ symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
133
132
  }
134
133
  if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
135
- const aliased = checker.getAliasedSymbol(symbol);
136
- if (aliased)
137
- symbol = aliased;
134
+ symbol = checker.getAliasedSymbol(symbol);
138
135
  }
139
136
  if (!symbol)
140
137
  return;
141
138
  const tagName = typescript_1.default.isPropertyAccessExpression(node.tagName)
142
139
  ? node.tagName.name.getText()
143
140
  : node.tagName.getText();
144
- const declarations = symbol.getDeclarations();
145
- if (!declarations)
146
- return;
147
- for (const decl of declarations) {
141
+ for (const decl of symbol.getDeclarations() ?? []) {
148
142
  let fn = null;
149
- if (typescript_1.default.isFunctionDeclaration(decl)) {
143
+ if (typescript_1.default.isFunctionDeclaration(decl))
150
144
  fn = decl;
151
- }
152
145
  if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
153
146
  const init = decl.initializer;
154
147
  if (typescript_1.default.isArrowFunction(init) ||
@@ -159,9 +152,7 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
159
152
  if (!fn)
160
153
  continue;
161
154
  const param = fn.parameters[0];
162
- if (!param)
163
- continue;
164
- if (!typescript_1.default.isObjectBindingPattern(param.name))
155
+ if (!param || !typescript_1.default.isObjectBindingPattern(param.name))
165
156
  continue;
166
157
  for (const el of param.name.elements) {
167
158
  const prop = el.name.getText();
@@ -181,11 +172,10 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
181
172
  : attr.initializer;
182
173
  if (!expr)
183
174
  continue;
184
- const res = (0, resolver_1.resolveNodeValue)(expr, checker);
185
- res?.values.forEach(v => {
186
- if (isValidEvent(v))
187
- events.add(v);
188
- });
175
+ const paramMap = new Map();
176
+ paramMap.set(prop, expr);
177
+ const res = scanSource(fn.getSourceFile(), checker, config, visited, depth + 1, paramMap);
178
+ res.events.forEach(e => events.add(e));
189
179
  detectedComponentWrappers.set(tagName, prop);
190
180
  }
191
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -0,0 +1,12 @@
1
+ import path from "path";
2
+
3
+ export function isExternalFile(file: string) {
4
+ const normalized = path.resolve(file);
5
+
6
+ return (
7
+ normalized.includes(`${path.sep}node_modules${path.sep}`) ||
8
+ normalized.includes(`${path.sep}dist${path.sep}`) ||
9
+ normalized.includes(`${path.sep}build${path.sep}`) ||
10
+ normalized.endsWith(".d.ts")
11
+ );
12
+ }
@@ -15,15 +15,18 @@ function isFunctionLike(
15
15
  function resolveFromSymbol(
16
16
  symbol: ts.Symbol,
17
17
  checker: ts.TypeChecker,
18
- visited = new Set<ts.Symbol>()
18
+ visited = new Set<ts.Symbol>(),
19
+ depth = 0
19
20
  ): ts.FunctionLikeDeclaration | null {
20
21
  if (visited.has(symbol)) return null;
22
+ if (depth > 5) return null;
23
+
21
24
  visited.add(symbol);
22
25
 
23
- // alias unwrap
26
+ // unwrap alias
24
27
  if (symbol.flags & ts.SymbolFlags.Alias) {
25
28
  const aliased = checker.getAliasedSymbol(symbol);
26
- return resolveFromSymbol(aliased, checker, visited);
29
+ return resolveFromSymbol(aliased, checker, visited, depth + 1);
27
30
  }
28
31
 
29
32
  const decls = symbol.getDeclarations() ?? [];
@@ -33,6 +36,7 @@ function resolveFromSymbol(
33
36
  return decl;
34
37
  }
35
38
 
39
+ // MethodSignature
36
40
  if (ts.isMethodSignature(decl)) {
37
41
  const parent = decl.parent;
38
42
 
@@ -45,16 +49,16 @@ function resolveFromSymbol(
45
49
  for (const prop of props) {
46
50
  if (prop.name !== name) continue;
47
51
 
48
- const resolved = resolveFromSymbol(prop, checker, visited);
52
+ const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
49
53
  if (resolved) return resolved;
50
54
  }
51
55
  }
52
56
  }
53
57
 
54
- // fallback
58
+ // SAFE fallback
55
59
  const s = checker.getSymbolAtLocation(decl);
56
- if (s) {
57
- const resolved = resolveFromSymbol(s, checker, visited);
60
+ if (s && s !== symbol) {
61
+ const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
58
62
  if (resolved) return resolved;
59
63
  }
60
64
  }
@@ -71,14 +75,13 @@ export function resolveFunctionFromCall(
71
75
 
72
76
  for (const sig of signatures) {
73
77
  const decl = sig.getDeclaration();
74
-
75
78
  if (!decl) continue;
76
79
 
77
80
  if (isFunctionLike(decl)) {
78
81
  return decl;
79
82
  }
80
83
 
81
- // MethodSignature case
84
+ // interface method
82
85
  if (ts.isMethodSignature(decl)) {
83
86
  const symbol = checker.getSymbolAtLocation(decl.name);
84
87
 
@@ -3,6 +3,7 @@ import { ScanResult, scanSource } from "./scanner";
3
3
  import { DependencyGraph } from "./graph";
4
4
  import { hash } from "./hash";
5
5
  import { EventraConfig } from "../types";
6
+ import { isExternalFile } from "./boundary";
6
7
 
7
8
  export class EventraEngine {
8
9
  private ts: TSService;
@@ -18,9 +19,15 @@ export class EventraEngine {
18
19
  this.graph = new DependencyGraph(baseUrl, paths);
19
20
  }
20
21
 
21
- scanFile(file: string, content: string, config: EventraConfig): ScanResult {
22
+ // SCAN FILE
23
+ scanFile(
24
+ file: string,
25
+ content: string,
26
+ config: EventraConfig
27
+ ): ScanResult {
22
28
  const h = hash(content);
23
29
 
30
+ // skip unchanged
24
31
  if (this.fileHash.get(file) === h) {
25
32
  return (
26
33
  this.fileResults.get(file) ?? {
@@ -45,6 +52,7 @@ export class EventraEngine {
45
52
  };
46
53
  }
47
54
 
55
+ // update dependency graph
48
56
  this.graph.update(file, source);
49
57
 
50
58
  const res = scanSource(source, checker, config);
@@ -54,17 +62,37 @@ export class EventraEngine {
54
62
  return res;
55
63
  }
56
64
 
57
- updateFile(file: string, content: string, config: EventraConfig) {
65
+ // UPDATE FILE (incremental)
66
+ updateFile(
67
+ file: string,
68
+ content: string,
69
+ config: EventraConfig
70
+ ) {
58
71
  const res = this.scanFile(file, content, config);
59
72
 
60
73
  const dependents = this.graph.getAllDependentsDeep(file);
61
74
 
62
75
  for (const dep of dependents) {
76
+ // skip external files
77
+ if (isExternalFile(dep)) continue;
78
+
63
79
  const source = this.ts.getSourceFile(dep);
64
80
  const checker = this.ts.getChecker();
65
81
 
66
82
  if (!source || !checker) continue;
67
83
 
84
+ const depContent = this.ts.getFileContent(dep);
85
+ if (!depContent) continue;
86
+
87
+ const h = hash(depContent);
88
+
89
+ // skip unchanged dependents
90
+ if (this.fileHash.get(dep) === h) continue;
91
+
92
+ this.fileHash.set(dep, h);
93
+
94
+ this.graph.update(dep, source);
95
+
68
96
  const r = scanSource(source, checker, config);
69
97
  this.fileResults.set(dep, r);
70
98
  }
@@ -72,6 +100,7 @@ export class EventraEngine {
72
100
  return res;
73
101
  }
74
102
 
103
+ // GET ALL EVENTS
75
104
  getAllEvents() {
76
105
  const all = new Set<string>();
77
106
 
@@ -82,9 +111,44 @@ export class EventraEngine {
82
111
  return [...all];
83
112
  }
84
113
 
85
- removeFile(file: string) {
114
+ // REMOVE FILE
115
+ removeFile(file: string, config?: EventraConfig) {
116
+ this.ts.removeFile(file);
86
117
  this.fileResults.delete(file);
87
118
  this.fileHash.delete(file);
88
119
  this.graph.remove(file);
120
+ const dependents = this.graph.getAllDependentsDeep(file);
121
+
122
+ for (const dep of dependents) {
123
+ if (isExternalFile(dep)) continue;
124
+
125
+ const source = this.ts.getSourceFile(dep);
126
+ const checker = this.ts.getChecker();
127
+
128
+ if (!source || !checker) continue;
129
+
130
+ const content = this.ts.getFileContent(dep);
131
+ if (!content) continue;
132
+
133
+ const h = hash(content);
134
+
135
+ // skip unchanged
136
+ if (this.fileHash.get(dep) === h) continue;
137
+
138
+ this.fileHash.set(dep, h);
139
+
140
+ const r = scanSource(
141
+ source,
142
+ checker,
143
+ config ?? {
144
+ events: [],
145
+ wrappers: [],
146
+ functionWrappers: [],
147
+ sync: { include: [], exclude: [] },
148
+ }
149
+ );
150
+
151
+ this.fileResults.set(dep, r);
152
+ }
89
153
  }
90
154
  }
@@ -62,6 +62,10 @@ export class TSService {
62
62
  this.files.get(file)?.version.toString() ?? "0",
63
63
 
64
64
  getScriptSnapshot: (file) => {
65
+ if (!this.fileNames.has(file)) {
66
+ return undefined;
67
+ }
68
+
65
69
  const f = this.files.get(file);
66
70
 
67
71
  if (f) {
@@ -113,6 +117,15 @@ export class TSService {
113
117
  return this.getProgram()?.getSourceFile(file);
114
118
  }
115
119
 
120
+ getFileContent(file: string) {
121
+ return this.files.get(file)?.content;
122
+ }
123
+
124
+ removeFile(file: string) {
125
+ this.files.delete(file);
126
+ this.fileNames.delete(file);
127
+ }
128
+
116
129
  getModuleResolutionConfig() {
117
130
  return {
118
131
  baseUrl: this.baseUrl,
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import {resolveExportedSymbol} from "./exportResolver";
2
+ import { resolveExportedSymbol } from "./exportResolver";
3
3
 
4
4
  export type ResolveResult = {
5
5
  values: string[];
@@ -21,7 +21,8 @@ export function resolveNodeValue(
21
21
  return resolveNodeValue(
22
22
  paramMap.get(node.text)!,
23
23
  checker,
24
- paramMap
24
+ paramMap,
25
+ seen
25
26
  );
26
27
  }
27
28
 
@@ -53,6 +54,8 @@ export function resolveNodeValue(
53
54
  if (ts.isIdentifier(node)) {
54
55
  let symbol = checker.getSymbolAtLocation(node);
55
56
  if (!symbol) return null;
57
+
58
+ // variable inline resolve FIRST
56
59
  for (const decl of symbol.getDeclarations() ?? []) {
57
60
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
58
61
  return resolveNodeValue(
@@ -64,15 +67,23 @@ export function resolveNodeValue(
64
67
  }
65
68
  }
66
69
 
70
+ const original = symbol;
71
+
67
72
  symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
68
73
 
69
- if (symbol.flags & ts.SymbolFlags.Alias) {
74
+ // avoid alias loop
75
+ if (symbol !== original && (symbol.flags & ts.SymbolFlags.Alias)) {
70
76
  symbol = checker.getAliasedSymbol(symbol);
71
77
  }
72
78
 
73
79
  for (const decl of symbol.getDeclarations() ?? []) {
74
80
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
75
- return resolveNodeValue(decl.initializer, checker, paramMap, seen);
81
+ return resolveNodeValue(
82
+ decl.initializer,
83
+ checker,
84
+ paramMap,
85
+ seen
86
+ );
76
87
  }
77
88
 
78
89
  if (ts.isEnumMember(decl) && decl.initializer) {
@@ -4,6 +4,7 @@ import { resolveFunctionFromCall } from "./callResolver";
4
4
  import { resolveExportedSymbol } from "./exportResolver";
5
5
  import { findTrackParamIndex } from "./functionAnalyzer";
6
6
  import { EventraConfig } from "../types";
7
+ import { isExternalFile } from "./boundary";
7
8
 
8
9
  const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
9
10
 
@@ -70,7 +71,8 @@ export function scanSource(
70
71
  checker: ts.TypeChecker,
71
72
  config: EventraConfig,
72
73
  visited = new Set<string>(),
73
- depth = 0
74
+ depth = 0,
75
+ inheritedParamMap?: Map<string, ts.Expression>
74
76
  ): ScanResult {
75
77
  const events = new Set<string>();
76
78
  const detectedFunctionWrappers = new Set<string>();
@@ -99,20 +101,18 @@ export function scanSource(
99
101
  const isTracking =
100
102
  isKnownWrapper ||
101
103
  TRACKING_NAMES.has(name) ||
102
- isAutoWrapper;
104
+ (isAutoWrapper &&
105
+ fn &&
106
+ !isExternalFile(fn.getSourceFile().fileName));
103
107
 
104
108
  if (!isTracking) return;
105
109
 
106
- // AUTO DETECT WRAPPER
107
- if (
108
- !isKnownWrapper &&
109
- fn &&
110
- !TRACKING_NAMES.has(name)
111
- ) {
110
+ if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
112
111
  detectedFunctionWrappers.add(name);
113
112
  }
114
113
 
115
- const paramMap = new Map<string, ts.Expression>();
114
+ // paramMap
115
+ const paramMap = new Map(inheritedParamMap ?? []);
116
116
 
117
117
  if (fn && ts.isFunctionLike(fn)) {
118
118
  fn.parameters.forEach((param, i) => {
@@ -156,7 +156,7 @@ export function scanSource(
156
156
  }
157
157
  }
158
158
 
159
- // cross-file traversal
159
+ // cross-file WITH paramMap
160
160
  if (fn) {
161
161
  const sf = fn.getSourceFile();
162
162
 
@@ -166,7 +166,8 @@ export function scanSource(
166
166
  checker,
167
167
  config,
168
168
  visited,
169
- depth + 1
169
+ depth + 1,
170
+ paramMap
170
171
  );
171
172
 
172
173
  res.events.forEach(e => events.add(e));
@@ -187,13 +188,11 @@ export function scanSource(
187
188
  let symbol = checker.getSymbolAtLocation(tagNode);
188
189
 
189
190
  if (symbol) {
190
- const resolved = resolveExportedSymbol(symbol, checker);
191
- if (resolved) symbol = resolved;
191
+ symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
192
192
  }
193
193
 
194
194
  if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
195
- const aliased = checker.getAliasedSymbol(symbol);
196
- if (aliased) symbol = aliased;
195
+ symbol = checker.getAliasedSymbol(symbol);
197
196
  }
198
197
 
199
198
  if (!symbol) return;
@@ -203,19 +202,13 @@ export function scanSource(
203
202
  ? node.tagName.name.getText()
204
203
  : node.tagName.getText();
205
204
 
206
- const declarations = symbol.getDeclarations();
207
- if (!declarations) return;
208
-
209
- for (const decl of declarations) {
205
+ for (const decl of symbol.getDeclarations() ?? []) {
210
206
  let fn: ts.FunctionLikeDeclaration | null = null;
211
207
 
212
- if (ts.isFunctionDeclaration(decl)) {
213
- fn = decl;
214
- }
208
+ if (ts.isFunctionDeclaration(decl)) fn = decl;
215
209
 
216
210
  if (ts.isVariableDeclaration(decl) && decl.initializer) {
217
211
  const init = decl.initializer;
218
-
219
212
  if (
220
213
  ts.isArrowFunction(init) ||
221
214
  ts.isFunctionExpression(init)
@@ -227,9 +220,7 @@ export function scanSource(
227
220
  if (!fn) continue;
228
221
 
229
222
  const param = fn.parameters[0];
230
- if (!param) continue;
231
-
232
- if (!ts.isObjectBindingPattern(param.name)) continue;
223
+ if (!param || !ts.isObjectBindingPattern(param.name)) continue;
233
224
 
234
225
  for (const el of param.name.elements) {
235
226
  const prop = el.name.getText();
@@ -255,11 +246,19 @@ export function scanSource(
255
246
 
256
247
  if (!expr) continue;
257
248
 
258
- const res = resolveNodeValue(expr, checker);
249
+ const paramMap = new Map<string, ts.Expression>();
250
+ paramMap.set(prop, expr);
259
251
 
260
- res?.values.forEach(v => {
261
- if (isValidEvent(v)) events.add(v);
262
- });
252
+ const res = scanSource(
253
+ fn.getSourceFile(),
254
+ checker,
255
+ config,
256
+ visited,
257
+ depth + 1,
258
+ paramMap
259
+ );
260
+
261
+ res.events.forEach(e => events.add(e));
263
262
 
264
263
  detectedComponentWrappers.set(tagName, prop);
265
264
  }