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