@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.
- package/dist/engine/scanner.js +36 -129
- package/dist/utils/universal.js +38 -34
- package/package.json +1 -1
- package/src/engine/scanner.ts +45 -179
- package/src/utils/universal.ts +56 -34
package/dist/engine/scanner.js
CHANGED
|
@@ -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
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
97
|
-
|
|
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(
|
|
101
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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)(
|
|
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
|
|
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);
|
package/dist/utils/universal.js
CHANGED
|
@@ -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
|
-
|
|
68
|
-
*/
|
|
69
|
-
function extractLooseJS(input) {
|
|
62
|
+
// Extract inline HTML handlers
|
|
63
|
+
function extractInlineHandlers(input) {
|
|
70
64
|
const result = [];
|
|
71
|
-
const
|
|
72
|
-
for (const
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
126
|
-
parts.push(...
|
|
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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
139
|
-
|
|
116
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
117
|
+
node.expression.name.text === "track"
|
|
140
118
|
) {
|
|
141
119
|
isTracking = true;
|
|
142
120
|
} else if (
|
|
143
|
-
ts.isIdentifier(
|
|
144
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
package/src/utils/universal.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
//
|
|
156
|
-
parts.push(
|
|
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
|
-
|
|
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))],
|