@eventra_dev/eventra-cli 0.1.27 → 0.1.29

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.
@@ -18,23 +18,6 @@ function getCallName(expr) {
18
18
  return expr.name.text;
19
19
  return "";
20
20
  }
21
- function getJsxAttrName(name) {
22
- if (typescript_1.default.isIdentifier(name))
23
- return name.text;
24
- if (typescript_1.default.isJsxNamespacedName(name))
25
- return name.name.text;
26
- return null;
27
- }
28
- // unwrap
29
- function unwrap(expr) {
30
- while (typescript_1.default.isAsExpression(expr) ||
31
- typescript_1.default.isParenthesizedExpression(expr) ||
32
- typescript_1.default.isTypeAssertionExpression(expr) ||
33
- typescript_1.default.isNonNullExpression(expr)) {
34
- expr = expr.expression;
35
- }
36
- return expr;
37
- }
38
21
  function isValidEvent(v) {
39
22
  return (v &&
40
23
  v.length > 1 &&
@@ -42,63 +25,60 @@ function isValidEvent(v) {
42
25
  !v.includes(" ") &&
43
26
  /^[a-zA-Z0-9._:-]+$/.test(v));
44
27
  }
45
- function isPropUsedInTracking(fn, checker, prop) {
46
- let found = false;
47
- function visit(node) {
48
- if (typescript_1.default.isCallExpression(node)) {
49
- if (!(0, trackingDetector_1.isTrackingCall)(node, checker)) {
50
- typescript_1.default.forEachChild(node, visit);
51
- return;
52
- }
53
- for (const arg of node.arguments) {
54
- const a = unwrap(arg);
55
- if ((typescript_1.default.isIdentifier(a) && a.text === prop) ||
56
- (typescript_1.default.isPropertyAccessExpression(a) &&
57
- a.name.text === prop)) {
58
- found = true;
59
- }
60
- }
61
- }
62
- typescript_1.default.forEachChild(node, visit);
28
+ function resolveFunctionDeep(expr, checker) {
29
+ let fn = (0, callResolver_1.resolveFunctionFromCall)(expr, checker);
30
+ if (fn)
31
+ return fn;
32
+ let symbol = checker.getSymbolAtLocation(expr);
33
+ if (!symbol)
34
+ return null;
35
+ if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
36
+ symbol = checker.getAliasedSymbol(symbol);
63
37
  }
64
- if (fn.body) {
65
- visit(fn.body);
38
+ const resolvedSymbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
39
+ for (const decl of resolvedSymbol.getDeclarations() ?? []) {
40
+ if (typescript_1.default.isFunctionDeclaration(decl)) {
41
+ return decl;
42
+ }
43
+ if (typescript_1.default.isVariableDeclaration(decl) &&
44
+ decl.initializer &&
45
+ (typescript_1.default.isArrowFunction(decl.initializer) ||
46
+ typescript_1.default.isFunctionExpression(decl.initializer))) {
47
+ return decl.initializer;
48
+ }
66
49
  }
67
- return found;
50
+ return null;
68
51
  }
69
52
  function scanSource(source, checker, config, visited = new Set(), depth = 0, inheritedParamMap) {
70
53
  const events = new Set();
71
54
  const detectedFunctionWrappers = new Set();
72
55
  const detectedComponentWrappers = new Map();
73
- if (visited.has(source.fileName) ||
74
- depth > 10 ||
75
- visited.size > 200) {
56
+ if (visited.size > 200 || visited.has(source.fileName)) {
76
57
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
77
58
  }
78
59
  visited.add(source.fileName);
79
60
  function visit(node) {
80
61
  // CALL EXPRESSIONS
81
62
  if (typescript_1.default.isCallExpression(node)) {
82
- const expr = unwrap(node.expression);
83
- const name = getCallName(expr);
63
+ const name = getCallName(node.expression);
84
64
  const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
85
65
  const isTrackingByType = (0, trackingDetector_1.isTrackingCall)(node, checker);
86
- let resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(expr, checker);
66
+ const resolvedFn = resolveFunctionDeep(node.expression, checker);
87
67
  const isAutoWrapper = resolvedFn && (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn) !== null;
88
68
  let isTracking = isKnownWrapper ||
89
69
  isTrackingByType ||
90
70
  name.startsWith("$") ||
91
71
  (isAutoWrapper &&
92
72
  resolvedFn &&
73
+ resolvedFn.getSourceFile &&
93
74
  !(0, boundary_1.isExternalFile)(resolvedFn.getSourceFile().fileName));
94
- // fallback
95
75
  if (!isTracking) {
96
- if (typescript_1.default.isPropertyAccessExpression(expr) &&
97
- expr.name.text === "track") {
76
+ if (typescript_1.default.isPropertyAccessExpression(node.expression) &&
77
+ node.expression.name.text === "track") {
98
78
  isTracking = true;
99
79
  }
100
- else if (typescript_1.default.isIdentifier(expr) &&
101
- expr.text === "track") {
80
+ else if (typescript_1.default.isIdentifier(node.expression) &&
81
+ node.expression.text === "track") {
102
82
  isTracking = true;
103
83
  }
104
84
  }
@@ -106,13 +86,14 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
106
86
  typescript_1.default.forEachChild(node, visit);
107
87
  return;
108
88
  }
109
- // detect wrapper
89
+ // DETECT WRAPPER
110
90
  if (!isKnownWrapper &&
111
91
  isAutoWrapper &&
112
92
  !isTrackingByType &&
113
93
  !name.startsWith("track")) {
114
94
  detectedFunctionWrappers.add(name);
115
95
  }
96
+ // PARAM MAP
116
97
  const paramMap = new Map(inheritedParamMap ?? []);
117
98
  if (resolvedFn && typescript_1.default.isFunctionLike(resolvedFn)) {
118
99
  resolvedFn.parameters.forEach((param, i) => {
@@ -124,100 +105,26 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
124
105
  }
125
106
  });
126
107
  }
127
- let handled = false;
128
- // MAIN RESOLVE
108
+ // STRICT EVENT RESOLVE
129
109
  if (resolvedFn) {
130
110
  const idx = (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn);
131
111
  if (idx !== null && node.arguments[idx]) {
132
- const res = (0, resolver_1.resolveNodeValue)(unwrap(node.arguments[idx]), checker, paramMap);
133
- res.values.forEach(v => {
134
- if (isValidEvent(v))
135
- events.add(v);
136
- });
137
- handled = true;
138
- }
139
- }
140
- // FALLBACK
141
- if (!handled) {
142
- for (const arg of node.arguments) {
143
- const res = (0, resolver_1.resolveNodeValue)(unwrap(arg), checker, paramMap);
112
+ const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
144
113
  res.values.forEach(v => {
145
114
  if (isValidEvent(v))
146
115
  events.add(v);
147
116
  });
148
117
  }
149
118
  }
150
- // CROSS FILE
119
+ // CROSS-FILE
151
120
  if (resolvedFn) {
152
- const sf = resolvedFn.getSourceFile();
153
- if (!visited.has(sf.fileName)) {
121
+ const sf = resolvedFn.getSourceFile?.();
122
+ if (sf && !visited.has(sf.fileName)) {
154
123
  const res = scanSource(sf, checker, config, visited, depth + 1, paramMap);
155
124
  res.events.forEach(e => events.add(e));
156
125
  }
157
126
  }
158
127
  }
159
- // JSX COMPONENTS
160
- if (typescript_1.default.isJsxSelfClosingElement(node) ||
161
- typescript_1.default.isJsxOpeningElement(node)) {
162
- const tagNode = typescript_1.default.isPropertyAccessExpression(node.tagName)
163
- ? node.tagName.name
164
- : node.tagName;
165
- let symbol = checker.getSymbolAtLocation(tagNode);
166
- if (symbol) {
167
- symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
168
- }
169
- if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
170
- symbol = checker.getAliasedSymbol(symbol);
171
- }
172
- if (!symbol) {
173
- typescript_1.default.forEachChild(node, visit);
174
- return;
175
- }
176
- const tagName = typescript_1.default.isPropertyAccessExpression(node.tagName)
177
- ? node.tagName.name.getText()
178
- : node.tagName.getText();
179
- for (const decl of symbol.getDeclarations() ?? []) {
180
- let fn = null;
181
- if (typescript_1.default.isFunctionDeclaration(decl))
182
- fn = decl;
183
- if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
184
- const init = decl.initializer;
185
- if (typescript_1.default.isArrowFunction(init) ||
186
- typescript_1.default.isFunctionExpression(init)) {
187
- fn = init;
188
- }
189
- }
190
- if (!fn)
191
- continue;
192
- const param = fn.parameters[0];
193
- if (!param || !typescript_1.default.isObjectBindingPattern(param.name))
194
- continue;
195
- for (const el of param.name.elements) {
196
- const prop = el.name.getText();
197
- const isEvent = isPropUsedInTracking(fn, checker, prop) ||
198
- config.wrappers.some(w => w.name === tagName && w.prop === prop);
199
- if (!isEvent)
200
- continue;
201
- const attr = node.attributes.properties.find(p => {
202
- if (!typescript_1.default.isJsxAttribute(p))
203
- return false;
204
- return getJsxAttrName(p.name) === prop;
205
- });
206
- if (!attr || !attr.initializer)
207
- continue;
208
- const expr = typescript_1.default.isJsxExpression(attr.initializer)
209
- ? attr.initializer.expression
210
- : attr.initializer;
211
- if (!expr)
212
- continue;
213
- const paramMap = new Map();
214
- paramMap.set(prop, expr);
215
- const res = scanSource(fn.getSourceFile(), checker, config, visited, depth + 1, paramMap);
216
- res.events.forEach(e => events.add(e));
217
- detectedComponentWrappers.set(tagName, prop);
218
- }
219
- }
220
- }
221
128
  typescript_1.default.forEachChild(node, visit);
222
129
  }
223
130
  visit(source);
@@ -5,18 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.parseUniversal = parseUniversal;
7
7
  const path_1 = __importDefault(require("path"));
8
- /**
9
- * Strip comments (HTML + JS)
10
- */
8
+ // Strip comments (HTML + JS)
11
9
  function stripComments(input) {
12
10
  return input
13
11
  .replace(/<!--[\s\S]*?-->/g, "")
14
12
  .replace(/\/\*[\s\S]*?\*\//g, "")
15
13
  .replace(/\/\/.*$/gm, "");
16
14
  }
17
- /**
18
- * Extract JS expressions
19
- */
15
+ // Extract JS expressions inside {}
20
16
  function extractJS(input) {
21
17
  const result = [];
22
18
  let i = 0;
@@ -63,34 +59,38 @@ function extractJS(input) {
63
59
  }
64
60
  return result;
65
61
  }
66
- /**
67
- * Loose JS extractor
68
- */
69
- function extractLooseJS(input) {
62
+ // Extract inline HTML handlers
63
+ function extractInlineHandlers(input) {
70
64
  const result = [];
71
- const lines = input.split("\n");
72
- for (const line of lines) {
73
- const l = line.trim();
74
- if (!l)
75
- continue;
76
- if (l.startsWith("<"))
77
- continue;
78
- if (/[a-zA-Z0-9_$]\s*\(/.test(l)) {
79
- result.push(l);
65
+ const matches = input.matchAll(/\bon\w+\s*=\s*["']([^"']+)["']/g);
66
+ for (const m of matches) {
67
+ const code = m[1];
68
+ if (/[a-zA-Z0-9_$]\s*\(/.test(code)) {
69
+ result.push(code);
80
70
  }
81
71
  }
82
72
  return result;
83
73
  }
84
- /**
85
- * UNIVERSAL PARSER
86
- * Support:
87
- * - React / JSX
88
- * - Vue
89
- * - Svelte
90
- * - Astro
91
- * - HTML + JS
92
- * - SSR
93
- */
74
+ // Extract dynamic imports
75
+ function extractDynamicImports(input, file) {
76
+ const deps = [];
77
+ const calls = [];
78
+ const matches = input.matchAll(/import\(\s*["'](.+?)["']\s*\)/g);
79
+ for (const m of matches) {
80
+ const mod = m[1];
81
+ const resolved = mod.startsWith("/")
82
+ ? path_1.default.resolve(process.cwd(), "." + mod)
83
+ : path_1.default.resolve(path_1.default.dirname(file), mod);
84
+ deps.push(resolved);
85
+ calls.push(`import("${mod}")`);
86
+ }
87
+ return { deps, calls };
88
+ }
89
+ // Filter only probable tracking-related code
90
+ function isProbablyTracking(code) {
91
+ return /track|event|analytics/i.test(code);
92
+ }
93
+ // UNIVERSAL PARSER
94
94
  function parseUniversal(content, file) {
95
95
  const parts = [];
96
96
  const deps = [];
@@ -113,7 +113,6 @@ function parseUniversal(content, file) {
113
113
  const scripts = clean.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
114
114
  for (const m of scripts) {
115
115
  const attrs = m[1];
116
- // skip JSON
117
116
  if (/type=["']application\/json/.test(attrs))
118
117
  continue;
119
118
  if (m[2].trim()) {
@@ -122,14 +121,19 @@ function parseUniversal(content, file) {
122
121
  }
123
122
  // REMOVE SCRIPTS → TEMPLATE PART
124
123
  const noScripts = clean.replace(/<script[\s\S]*?<\/script>/g, "");
125
- // LOOSE JS
126
- parts.push(...extractLooseJS(noScripts));
124
+ // INLINE HANDLERS
125
+ parts.push(...extractInlineHandlers(noScripts).filter(isProbablyTracking));
126
+ // DYNAMIC IMPORTS
127
+ const dyn = extractDynamicImports(clean, file);
128
+ deps.push(...dyn.deps);
129
+ parts.push(...dyn.calls.filter(isProbablyTracking));
127
130
  // EXPRESSIONS
128
131
  const extracted = extractJS(clean);
129
132
  for (const e of extracted) {
130
- parts.push(e + ";");
133
+ if (isProbablyTracking(e)) {
134
+ parts.push(e + ";");
135
+ }
131
136
  }
132
- // RESULT
133
137
  return {
134
138
  code: [...new Set(parts)].join("\n"),
135
139
  deps: [...new Set(deps.filter(Boolean))],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -13,25 +13,6 @@ function getCallName(expr: ts.Expression): string {
13
13
  return "";
14
14
  }
15
15
 
16
- function getJsxAttrName(name: ts.JsxAttributeName): string | null {
17
- if (ts.isIdentifier(name)) return name.text;
18
- if (ts.isJsxNamespacedName(name)) return name.name.text;
19
- return null;
20
- }
21
-
22
- // unwrap
23
- function unwrap(expr: ts.Expression): ts.Expression {
24
- while (
25
- ts.isAsExpression(expr) ||
26
- ts.isParenthesizedExpression(expr) ||
27
- ts.isTypeAssertionExpression(expr) ||
28
- ts.isNonNullExpression(expr)
29
- ) {
30
- expr = expr.expression;
31
- }
32
- return expr;
33
- }
34
-
35
16
  function isValidEvent(v: string) {
36
17
  return (
37
18
  v &&
@@ -42,41 +23,40 @@ function isValidEvent(v: string) {
42
23
  );
43
24
  }
44
25
 
45
- function isPropUsedInTracking(
46
- fn: ts.FunctionLikeDeclaration,
47
- checker: ts.TypeChecker,
48
- prop: string
49
- ) {
50
- let found = false;
26
+ function resolveFunctionDeep(
27
+ expr: ts.Expression,
28
+ checker: ts.TypeChecker
29
+ ): ts.FunctionLikeDeclaration | null {
30
+ let fn = resolveFunctionFromCall(expr, checker);
31
+ if (fn) return fn;
51
32
 
52
- function visit(node: ts.Node) {
53
- if (ts.isCallExpression(node)) {
54
- if (!isTrackingCall(node, checker)) {
55
- ts.forEachChild(node, visit);
56
- return;
57
- }
58
-
59
- for (const arg of node.arguments) {
60
- const a = unwrap(arg as ts.Expression);
33
+ let symbol = checker.getSymbolAtLocation(expr);
61
34
 
62
- if (
63
- (ts.isIdentifier(a) && a.text === prop) ||
64
- (ts.isPropertyAccessExpression(a) &&
65
- a.name.text === prop)
66
- ) {
67
- found = true;
68
- }
69
- }
70
- }
35
+ if (!symbol) return null;
71
36
 
72
- ts.forEachChild(node, visit);
37
+ if (symbol.flags & ts.SymbolFlags.Alias) {
38
+ symbol = checker.getAliasedSymbol(symbol);
73
39
  }
74
40
 
75
- if (fn.body) {
76
- visit(fn.body);
41
+ const resolvedSymbol =
42
+ resolveExportedSymbol(symbol, checker) ?? symbol;
43
+
44
+ for (const decl of resolvedSymbol.getDeclarations() ?? []) {
45
+ if (ts.isFunctionDeclaration(decl)) {
46
+ return decl;
47
+ }
48
+
49
+ if (
50
+ ts.isVariableDeclaration(decl) &&
51
+ decl.initializer &&
52
+ (ts.isArrowFunction(decl.initializer) ||
53
+ ts.isFunctionExpression(decl.initializer))
54
+ ) {
55
+ return decl.initializer;
56
+ }
77
57
  }
78
58
 
79
- return found;
59
+ return null;
80
60
  }
81
61
 
82
62
  export type ScanResult = {
@@ -97,11 +77,7 @@ export function scanSource(
97
77
  const detectedFunctionWrappers = new Set<string>();
98
78
  const detectedComponentWrappers = new Map<string, string>();
99
79
 
100
- if (
101
- visited.has(source.fileName) ||
102
- depth > 10 ||
103
- visited.size > 200
104
- ) {
80
+ if (visited.size > 200 || visited.has(source.fileName)) {
105
81
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
106
82
  }
107
83
 
@@ -110,8 +86,7 @@ export function scanSource(
110
86
  function visit(node: ts.Node) {
111
87
  // CALL EXPRESSIONS
112
88
  if (ts.isCallExpression(node)) {
113
- const expr = unwrap(node.expression);
114
- const name = getCallName(expr);
89
+ const name = getCallName(node.expression);
115
90
 
116
91
  const isKnownWrapper = config.functionWrappers.some(
117
92
  w => w.name === name
@@ -119,7 +94,10 @@ export function scanSource(
119
94
 
120
95
  const isTrackingByType = isTrackingCall(node, checker);
121
96
 
122
- let resolvedFn = resolveFunctionFromCall(expr, checker);
97
+ const resolvedFn = resolveFunctionDeep(
98
+ node.expression,
99
+ checker
100
+ );
123
101
 
124
102
  const isAutoWrapper =
125
103
  resolvedFn && findTrackParamIndex(resolvedFn) !== null;
@@ -130,18 +108,18 @@ export function scanSource(
130
108
  name.startsWith("$") ||
131
109
  (isAutoWrapper &&
132
110
  resolvedFn &&
111
+ resolvedFn.getSourceFile &&
133
112
  !isExternalFile(resolvedFn.getSourceFile().fileName));
134
113
 
135
- // fallback
136
114
  if (!isTracking) {
137
115
  if (
138
- ts.isPropertyAccessExpression(expr) &&
139
- expr.name.text === "track"
116
+ ts.isPropertyAccessExpression(node.expression) &&
117
+ node.expression.name.text === "track"
140
118
  ) {
141
119
  isTracking = true;
142
120
  } else if (
143
- ts.isIdentifier(expr) &&
144
- expr.text === "track"
121
+ ts.isIdentifier(node.expression) &&
122
+ node.expression.text === "track"
145
123
  ) {
146
124
  isTracking = true;
147
125
  }
@@ -152,7 +130,7 @@ export function scanSource(
152
130
  return;
153
131
  }
154
132
 
155
- // detect wrapper
133
+ // DETECT WRAPPER
156
134
  if (
157
135
  !isKnownWrapper &&
158
136
  isAutoWrapper &&
@@ -162,6 +140,7 @@ export function scanSource(
162
140
  detectedFunctionWrappers.add(name);
163
141
  }
164
142
 
143
+ // PARAM MAP
165
144
  const paramMap = new Map(inheritedParamMap ?? []);
166
145
 
167
146
  if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
@@ -175,15 +154,13 @@ export function scanSource(
175
154
  });
176
155
  }
177
156
 
178
- let handled = false;
179
-
180
- // MAIN RESOLVE
157
+ // STRICT EVENT RESOLVE
181
158
  if (resolvedFn) {
182
159
  const idx = findTrackParamIndex(resolvedFn);
183
160
 
184
161
  if (idx !== null && node.arguments[idx]) {
185
162
  const res = resolveNodeValue(
186
- unwrap(node.arguments[idx]),
163
+ node.arguments[idx],
187
164
  checker,
188
165
  paramMap
189
166
  );
@@ -191,31 +168,14 @@ export function scanSource(
191
168
  res.values.forEach(v => {
192
169
  if (isValidEvent(v)) events.add(v);
193
170
  });
194
-
195
- handled = true;
196
171
  }
197
172
  }
198
173
 
199
- // FALLBACK
200
- if (!handled) {
201
- for (const arg of node.arguments) {
202
- const res = resolveNodeValue(
203
- unwrap(arg),
204
- checker,
205
- paramMap
206
- );
207
-
208
- res.values.forEach(v => {
209
- if (isValidEvent(v)) events.add(v);
210
- });
211
- }
212
- }
213
-
214
- // CROSS FILE
174
+ // CROSS-FILE
215
175
  if (resolvedFn) {
216
- const sf = resolvedFn.getSourceFile();
176
+ const sf = resolvedFn.getSourceFile?.();
217
177
 
218
- if (!visited.has(sf.fileName)) {
178
+ if (sf && !visited.has(sf.fileName)) {
219
179
  const res = scanSource(
220
180
  sf,
221
181
  checker,
@@ -230,100 +190,6 @@ export function scanSource(
230
190
  }
231
191
  }
232
192
 
233
- // JSX COMPONENTS
234
- if (
235
- ts.isJsxSelfClosingElement(node) ||
236
- ts.isJsxOpeningElement(node)
237
- ) {
238
- const tagNode =
239
- ts.isPropertyAccessExpression(node.tagName)
240
- ? node.tagName.name
241
- : node.tagName;
242
-
243
- let symbol = checker.getSymbolAtLocation(tagNode);
244
-
245
- if (symbol) {
246
- symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
247
- }
248
-
249
- if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
250
- symbol = checker.getAliasedSymbol(symbol);
251
- }
252
-
253
- if (!symbol) {
254
- ts.forEachChild(node, visit);
255
- return;
256
- }
257
-
258
- const tagName =
259
- ts.isPropertyAccessExpression(node.tagName)
260
- ? node.tagName.name.getText()
261
- : node.tagName.getText();
262
-
263
- for (const decl of symbol.getDeclarations() ?? []) {
264
- let fn: ts.FunctionLikeDeclaration | null = null;
265
-
266
- if (ts.isFunctionDeclaration(decl)) fn = decl;
267
-
268
- if (ts.isVariableDeclaration(decl) && decl.initializer) {
269
- const init = decl.initializer;
270
-
271
- if (
272
- ts.isArrowFunction(init) ||
273
- ts.isFunctionExpression(init)
274
- ) {
275
- fn = init;
276
- }
277
- }
278
-
279
- if (!fn) continue;
280
-
281
- const param = fn.parameters[0];
282
- if (!param || !ts.isObjectBindingPattern(param.name)) continue;
283
-
284
- for (const el of param.name.elements) {
285
- const prop = el.name.getText();
286
-
287
- const isEvent =
288
- isPropUsedInTracking(fn, checker, prop) ||
289
- config.wrappers.some(
290
- w => w.name === tagName && w.prop === prop
291
- );
292
-
293
- if (!isEvent) continue;
294
-
295
- const attr = node.attributes.properties.find(p => {
296
- if (!ts.isJsxAttribute(p)) return false;
297
- return getJsxAttrName(p.name) === prop;
298
- }) as ts.JsxAttribute | undefined;
299
-
300
- if (!attr || !attr.initializer) continue;
301
-
302
- const expr = ts.isJsxExpression(attr.initializer)
303
- ? attr.initializer.expression
304
- : attr.initializer;
305
-
306
- if (!expr) continue;
307
-
308
- const paramMap = new Map<string, ts.Expression>();
309
- paramMap.set(prop, expr);
310
-
311
- const res = scanSource(
312
- fn.getSourceFile(),
313
- checker,
314
- config,
315
- visited,
316
- depth + 1,
317
- paramMap
318
- );
319
-
320
- res.events.forEach(e => events.add(e));
321
-
322
- detectedComponentWrappers.set(tagName, prop);
323
- }
324
- }
325
- }
326
-
327
193
  ts.forEachChild(node, visit);
328
194
  }
329
195
 
@@ -1,9 +1,7 @@
1
1
  import path from "path";
2
2
  import { ParseResult } from "../types";
3
3
 
4
- /**
5
- * Strip comments (HTML + JS)
6
- */
4
+ // Strip comments (HTML + JS)
7
5
  function stripComments(input: string): string {
8
6
  return input
9
7
  .replace(/<!--[\s\S]*?-->/g, "")
@@ -11,9 +9,7 @@ function stripComments(input: string): string {
11
9
  .replace(/\/\/.*$/gm, "");
12
10
  }
13
11
 
14
- /**
15
- * Extract JS expressions
16
- */
12
+ // Extract JS expressions inside {}
17
13
  function extractJS(input: string): string[] {
18
14
  const result: string[] = [];
19
15
 
@@ -68,38 +64,56 @@ function extractJS(input: string): string[] {
68
64
  return result;
69
65
  }
70
66
 
71
- /**
72
- * Loose JS extractor
73
- */
74
- function extractLooseJS(input: string): string[] {
67
+ // Extract inline HTML handlers
68
+ function extractInlineHandlers(input: string): string[] {
75
69
  const result: string[] = [];
76
70
 
77
- const lines = input.split("\n");
78
-
79
- for (const line of lines) {
80
- const l = line.trim();
81
-
82
- if (!l) continue;
83
- if (l.startsWith("<")) continue;
71
+ const matches = input.matchAll(
72
+ /\bon\w+\s*=\s*["']([^"']+)["']/g
73
+ );
84
74
 
85
- if (/[a-zA-Z0-9_$]\s*\(/.test(l)) {
86
- result.push(l);
75
+ for (const m of matches) {
76
+ const code = m[1];
77
+ if (/[a-zA-Z0-9_$]\s*\(/.test(code)) {
78
+ result.push(code);
87
79
  }
88
80
  }
89
81
 
90
82
  return result;
91
83
  }
92
84
 
93
- /**
94
- * UNIVERSAL PARSER
95
- * Support:
96
- * - React / JSX
97
- * - Vue
98
- * - Svelte
99
- * - Astro
100
- * - HTML + JS
101
- * - SSR
102
- */
85
+ // Extract dynamic imports
86
+ function extractDynamicImports(
87
+ input: string,
88
+ file: string
89
+ ): { deps: string[]; calls: string[] } {
90
+ const deps: string[] = [];
91
+ const calls: string[] = [];
92
+
93
+ const matches = input.matchAll(
94
+ /import\(\s*["'](.+?)["']\s*\)/g
95
+ );
96
+
97
+ for (const m of matches) {
98
+ const mod = m[1];
99
+
100
+ const resolved = mod.startsWith("/")
101
+ ? path.resolve(process.cwd(), "." + mod)
102
+ : path.resolve(path.dirname(file), mod);
103
+
104
+ deps.push(resolved);
105
+ calls.push(`import("${mod}")`);
106
+ }
107
+
108
+ return { deps, calls };
109
+ }
110
+
111
+ // Filter only probable tracking-related code
112
+ function isProbablyTracking(code: string): boolean {
113
+ return /track|event|analytics/i.test(code);
114
+ }
115
+
116
+ // UNIVERSAL PARSER
103
117
  export function parseUniversal(
104
118
  content: string,
105
119
  file: string
@@ -138,7 +152,6 @@ export function parseUniversal(
138
152
  for (const m of scripts) {
139
153
  const attrs = m[1];
140
154
 
141
- // skip JSON
142
155
  if (/type=["']application\/json/.test(attrs)) continue;
143
156
 
144
157
  if (m[2].trim()) {
@@ -152,16 +165,25 @@ export function parseUniversal(
152
165
  ""
153
166
  );
154
167
 
155
- // LOOSE JS
156
- parts.push(...extractLooseJS(noScripts));
168
+ // INLINE HANDLERS
169
+ parts.push(
170
+ ...extractInlineHandlers(noScripts).filter(isProbablyTracking)
171
+ );
172
+
173
+ // DYNAMIC IMPORTS
174
+ const dyn = extractDynamicImports(clean, file);
175
+ deps.push(...dyn.deps);
176
+ parts.push(...dyn.calls.filter(isProbablyTracking));
157
177
 
158
178
  // EXPRESSIONS
159
179
  const extracted = extractJS(clean);
180
+
160
181
  for (const e of extracted) {
161
- parts.push(e + ";");
182
+ if (isProbablyTracking(e)) {
183
+ parts.push(e + ";");
184
+ }
162
185
  }
163
186
 
164
- // RESULT
165
187
  return {
166
188
  code: [...new Set(parts)].join("\n"),
167
189
  deps: [...new Set(deps.filter(Boolean))],