@eventra_dev/eventra-cli 0.1.29 → 0.1.30
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/functionAnalyzer.js +15 -50
- package/dist/engine/resolver.js +57 -138
- package/dist/engine/scanner.js +16 -59
- package/dist/engine/trackingDetector.js +3 -54
- package/dist/utils/universal.js +49 -68
- package/package.json +1 -1
- package/src/engine/functionAnalyzer.ts +14 -54
- package/src/engine/resolver.ts +83 -184
- package/src/engine/scanner.ts +15 -81
- package/src/engine/trackingDetector.ts +3 -80
- package/src/utils/universal.ts +60 -74
package/src/engine/scanner.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { resolveNodeValue } from "./resolver";
|
|
3
3
|
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
|
-
import { resolveExportedSymbol } from "./exportResolver";
|
|
5
4
|
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
6
5
|
import { EventraConfig } from "../types";
|
|
7
6
|
import { isExternalFile } from "./boundary";
|
|
8
|
-
import { isTrackingCall } from "./trackingDetector";
|
|
9
7
|
|
|
10
8
|
function getCallName(expr: ts.Expression): string {
|
|
11
9
|
if (ts.isIdentifier(expr)) return expr.text;
|
|
@@ -23,42 +21,6 @@ function isValidEvent(v: string) {
|
|
|
23
21
|
);
|
|
24
22
|
}
|
|
25
23
|
|
|
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;
|
|
32
|
-
|
|
33
|
-
let symbol = checker.getSymbolAtLocation(expr);
|
|
34
|
-
|
|
35
|
-
if (!symbol) return null;
|
|
36
|
-
|
|
37
|
-
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
38
|
-
symbol = checker.getAliasedSymbol(symbol);
|
|
39
|
-
}
|
|
40
|
-
|
|
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
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
24
|
export type ScanResult = {
|
|
63
25
|
events: Set<string>;
|
|
64
26
|
detectedFunctionWrappers: Set<string>;
|
|
@@ -70,7 +32,6 @@ export function scanSource(
|
|
|
70
32
|
checker: ts.TypeChecker,
|
|
71
33
|
config: EventraConfig,
|
|
72
34
|
visited = new Set<string>(),
|
|
73
|
-
depth = 0,
|
|
74
35
|
inheritedParamMap?: Map<string, ts.Expression>
|
|
75
36
|
): ScanResult {
|
|
76
37
|
const events = new Set<string>();
|
|
@@ -84,7 +45,6 @@ export function scanSource(
|
|
|
84
45
|
visited.add(source.fileName);
|
|
85
46
|
|
|
86
47
|
function visit(node: ts.Node) {
|
|
87
|
-
// CALL EXPRESSIONS
|
|
88
48
|
if (ts.isCallExpression(node)) {
|
|
89
49
|
const name = getCallName(node.expression);
|
|
90
50
|
|
|
@@ -92,55 +52,28 @@ export function scanSource(
|
|
|
92
52
|
w => w.name === name
|
|
93
53
|
);
|
|
94
54
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const resolvedFn = resolveFunctionDeep(
|
|
98
|
-
node.expression,
|
|
99
|
-
checker
|
|
100
|
-
);
|
|
55
|
+
const resolvedFn = resolveFunctionFromCall(node.expression, checker);
|
|
101
56
|
|
|
102
57
|
const isAutoWrapper =
|
|
103
|
-
resolvedFn && findTrackParamIndex(resolvedFn) !== null;
|
|
58
|
+
!!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
|
|
104
59
|
|
|
105
|
-
|
|
60
|
+
const isTracking =
|
|
61
|
+
name === "track" ||
|
|
106
62
|
isKnownWrapper ||
|
|
107
|
-
isTrackingByType ||
|
|
108
|
-
name.startsWith("$") ||
|
|
109
63
|
(isAutoWrapper &&
|
|
110
64
|
resolvedFn &&
|
|
111
|
-
resolvedFn.getSourceFile &&
|
|
112
65
|
!isExternalFile(resolvedFn.getSourceFile().fileName));
|
|
113
66
|
|
|
114
|
-
if (!isTracking) {
|
|
115
|
-
if (
|
|
116
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
117
|
-
node.expression.name.text === "track"
|
|
118
|
-
) {
|
|
119
|
-
isTracking = true;
|
|
120
|
-
} else if (
|
|
121
|
-
ts.isIdentifier(node.expression) &&
|
|
122
|
-
node.expression.text === "track"
|
|
123
|
-
) {
|
|
124
|
-
isTracking = true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
67
|
if (!isTracking) {
|
|
129
68
|
ts.forEachChild(node, visit);
|
|
130
69
|
return;
|
|
131
70
|
}
|
|
132
71
|
|
|
133
|
-
//
|
|
134
|
-
if (
|
|
135
|
-
!isKnownWrapper &&
|
|
136
|
-
isAutoWrapper &&
|
|
137
|
-
!isTrackingByType &&
|
|
138
|
-
!name.startsWith("track")
|
|
139
|
-
) {
|
|
72
|
+
// авто-детект wrapper
|
|
73
|
+
if (!isKnownWrapper && isAutoWrapper && name !== "track") {
|
|
140
74
|
detectedFunctionWrappers.add(name);
|
|
141
75
|
}
|
|
142
76
|
|
|
143
|
-
// PARAM MAP
|
|
144
77
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
145
78
|
|
|
146
79
|
if (resolvedFn && ts.isFunctionLike(resolvedFn)) {
|
|
@@ -154,7 +87,7 @@ export function scanSource(
|
|
|
154
87
|
});
|
|
155
88
|
}
|
|
156
89
|
|
|
157
|
-
//
|
|
90
|
+
// ✅ ТОЛЬКО правильный аргумент
|
|
158
91
|
if (resolvedFn) {
|
|
159
92
|
const idx = findTrackParamIndex(resolvedFn);
|
|
160
93
|
|
|
@@ -165,23 +98,24 @@ export function scanSource(
|
|
|
165
98
|
paramMap
|
|
166
99
|
);
|
|
167
100
|
|
|
168
|
-
res.values
|
|
169
|
-
if (isValidEvent(v))
|
|
170
|
-
|
|
101
|
+
for (const v of res.values) {
|
|
102
|
+
if (isValidEvent(v)) {
|
|
103
|
+
events.add(v);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
171
106
|
}
|
|
172
107
|
}
|
|
173
108
|
|
|
174
|
-
// CROSS
|
|
109
|
+
// CROSS FILE
|
|
175
110
|
if (resolvedFn) {
|
|
176
|
-
const sf = resolvedFn.getSourceFile
|
|
111
|
+
const sf = resolvedFn.getSourceFile();
|
|
177
112
|
|
|
178
|
-
if (
|
|
113
|
+
if (!visited.has(sf.fileName)) {
|
|
179
114
|
const res = scanSource(
|
|
180
115
|
sf,
|
|
181
116
|
checker,
|
|
182
117
|
config,
|
|
183
118
|
visited,
|
|
184
|
-
depth + 1,
|
|
185
119
|
paramMap
|
|
186
120
|
);
|
|
187
121
|
|
|
@@ -1,93 +1,16 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
|
|
3
|
-
function isStringLike(type: ts.Type) {
|
|
4
|
-
return (
|
|
5
|
-
(type.flags & ts.TypeFlags.StringLike) !== 0 ||
|
|
6
|
-
(type.flags & ts.TypeFlags.StringLiteral) !== 0
|
|
7
|
-
);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function hasTrackLikeSignature(
|
|
11
|
-
type: ts.Type,
|
|
12
|
-
checker: ts.TypeChecker
|
|
13
|
-
): boolean {
|
|
14
|
-
const sigs = type.getCallSignatures();
|
|
15
|
-
|
|
16
|
-
for (const sig of sigs) {
|
|
17
|
-
const params = sig.getParameters();
|
|
18
|
-
if (!params.length) continue;
|
|
19
|
-
|
|
20
|
-
const t = checker.getTypeOfSymbolAtLocation(
|
|
21
|
-
params[0],
|
|
22
|
-
params[0].valueDeclaration!
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
if (isStringLike(t)) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isEventraLikeType(
|
|
34
|
-
type: ts.Type,
|
|
35
|
-
checker: ts.TypeChecker
|
|
36
|
-
): boolean {
|
|
37
|
-
const props = type.getProperties();
|
|
38
|
-
|
|
39
|
-
for (const prop of props) {
|
|
40
|
-
const decl = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
41
|
-
if (!decl) continue;
|
|
42
|
-
|
|
43
|
-
const t = checker.getTypeOfSymbolAtLocation(prop, decl);
|
|
44
|
-
|
|
45
|
-
if (hasTrackLikeSignature(t, checker)) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
3
|
export function isTrackingCall(
|
|
54
|
-
node: ts.CallExpression
|
|
55
|
-
checker: ts.TypeChecker
|
|
4
|
+
node: ts.CallExpression
|
|
56
5
|
): boolean {
|
|
57
6
|
const expr = node.expression;
|
|
58
7
|
|
|
59
|
-
// fn(...)
|
|
60
8
|
if (ts.isIdentifier(expr)) {
|
|
61
|
-
|
|
62
|
-
if (hasTrackLikeSignature(type, checker)) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
9
|
+
return expr.text === "track";
|
|
65
10
|
}
|
|
66
11
|
|
|
67
|
-
// obj.method(...)
|
|
68
12
|
if (ts.isPropertyAccessExpression(expr)) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// объект похож на tracker
|
|
72
|
-
if (isEventraLikeType(objType, checker)) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// fallback
|
|
77
|
-
const methodType = checker.getTypeAtLocation(expr.name);
|
|
78
|
-
|
|
79
|
-
if (hasTrackLikeSignature(methodType, checker)) {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// obj["track"](...)
|
|
85
|
-
if (ts.isElementAccessExpression(expr)) {
|
|
86
|
-
const objType = checker.getTypeAtLocation(expr.expression);
|
|
87
|
-
|
|
88
|
-
if (isEventraLikeType(objType, checker)) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
13
|
+
return expr.name.text === "track";
|
|
91
14
|
}
|
|
92
15
|
|
|
93
16
|
return false;
|
package/src/utils/universal.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { ParseResult } from "../types";
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Remove comments
|
|
5
5
|
function stripComments(input: string): string {
|
|
6
6
|
return input
|
|
7
7
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
@@ -9,80 +9,78 @@ function stripComments(input: string): string {
|
|
|
9
9
|
.replace(/\/\/.*$/gm, "");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
function
|
|
12
|
+
// Basic filter
|
|
13
|
+
function looksLikeTracking(code: string) {
|
|
14
|
+
return /track/i.test(code);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// HTML inline handlers (onclick="")
|
|
18
|
+
function extractHTMLHandlers(input: string): string[] {
|
|
14
19
|
const result: string[] = [];
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const matches = input.matchAll(
|
|
22
|
+
/\bon\w+\s*=\s*["']([^"']+)["']/g
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
for (const m of matches) {
|
|
26
|
+
if (looksLikeTracking(m[1])) {
|
|
27
|
+
result.push(m[1]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
// Vue handlers (@click / v-on)
|
|
35
|
+
function extractVueHandlers(input: string): string[] {
|
|
36
|
+
const result: string[] = [];
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
else if (ch === inString) inString = null;
|
|
29
|
-
i++;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
38
|
+
const matches = input.matchAll(
|
|
39
|
+
/(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g
|
|
40
|
+
);
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
continue;
|
|
42
|
+
for (const m of matches) {
|
|
43
|
+
if (looksLikeTracking(m[1])) {
|
|
44
|
+
result.push(m[1]);
|
|
37
45
|
}
|
|
46
|
+
}
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let j = i + 1;
|
|
42
|
-
|
|
43
|
-
while (j < len && depth > 0) {
|
|
44
|
-
if (input[j] === "{") depth++;
|
|
45
|
-
else if (input[j] === "}") depth--;
|
|
46
|
-
j++;
|
|
47
|
-
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
// Svelte handlers (on:click)
|
|
52
|
+
function extractSvelteHandlers(input: string): string[] {
|
|
53
|
+
const result: string[] = [];
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
const matches = input.matchAll(
|
|
56
|
+
/\bon:[\w]+\s*=\s*{([^}]+)}/g
|
|
57
|
+
);
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
for (const m of matches) {
|
|
60
|
+
if (looksLikeTracking(m[1])) {
|
|
61
|
+
result.push(m[1]);
|
|
59
62
|
}
|
|
60
|
-
|
|
61
|
-
i++;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
return result;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
//
|
|
68
|
-
function
|
|
68
|
+
// JSX / template { ... }
|
|
69
|
+
function extractJSExpressions(input: string): string[] {
|
|
69
70
|
const result: string[] = [];
|
|
70
71
|
|
|
71
|
-
const matches = input.matchAll(
|
|
72
|
-
/\bon\w+\s*=\s*["']([^"']+)["']/g
|
|
73
|
-
);
|
|
72
|
+
const matches = input.matchAll(/\{([^}]+)\}/g);
|
|
74
73
|
|
|
75
74
|
for (const m of matches) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
result.push(code);
|
|
75
|
+
if (looksLikeTracking(m[1])) {
|
|
76
|
+
result.push(m[1]);
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
return result;
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
//
|
|
83
|
+
// dynamic import()
|
|
86
84
|
function extractDynamicImports(
|
|
87
85
|
input: string,
|
|
88
86
|
file: string
|
|
@@ -102,17 +100,15 @@ function extractDynamicImports(
|
|
|
102
100
|
: path.resolve(path.dirname(file), mod);
|
|
103
101
|
|
|
104
102
|
deps.push(resolved);
|
|
105
|
-
|
|
103
|
+
|
|
104
|
+
if (looksLikeTracking(mod)) {
|
|
105
|
+
calls.push(`import("${mod}")`);
|
|
106
|
+
}
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
return { deps, calls };
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
// Filter only probable tracking-related code
|
|
112
|
-
function isProbablyTracking(code: string): boolean {
|
|
113
|
-
return /track|event|analytics/i.test(code);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
112
|
// UNIVERSAL PARSER
|
|
117
113
|
export function parseUniversal(
|
|
118
114
|
content: string,
|
|
@@ -150,39 +146,29 @@ export function parseUniversal(
|
|
|
150
146
|
);
|
|
151
147
|
|
|
152
148
|
for (const m of scripts) {
|
|
153
|
-
const attrs = m[1];
|
|
154
|
-
|
|
155
|
-
if (/type=["']application\/json/.test(attrs)) continue;
|
|
156
|
-
|
|
157
149
|
if (m[2].trim()) {
|
|
158
150
|
parts.push(m[2]);
|
|
159
151
|
}
|
|
160
152
|
}
|
|
161
153
|
|
|
162
|
-
//
|
|
154
|
+
// TEMPLATE PART
|
|
163
155
|
const noScripts = clean.replace(
|
|
164
156
|
/<script[\s\S]*?<\/script>/g,
|
|
165
157
|
""
|
|
166
158
|
);
|
|
167
159
|
|
|
168
|
-
//
|
|
169
|
-
parts.push(
|
|
170
|
-
|
|
171
|
-
);
|
|
160
|
+
// handlers
|
|
161
|
+
parts.push(...extractHTMLHandlers(noScripts));
|
|
162
|
+
parts.push(...extractVueHandlers(noScripts));
|
|
163
|
+
parts.push(...extractSvelteHandlers(noScripts));
|
|
172
164
|
|
|
173
|
-
//
|
|
165
|
+
// JSX / Astro / Vue expressions
|
|
166
|
+
parts.push(...extractJSExpressions(clean));
|
|
167
|
+
|
|
168
|
+
// dynamic imports
|
|
174
169
|
const dyn = extractDynamicImports(clean, file);
|
|
175
170
|
deps.push(...dyn.deps);
|
|
176
|
-
parts.push(...dyn.calls
|
|
177
|
-
|
|
178
|
-
// EXPRESSIONS
|
|
179
|
-
const extracted = extractJS(clean);
|
|
180
|
-
|
|
181
|
-
for (const e of extracted) {
|
|
182
|
-
if (isProbablyTracking(e)) {
|
|
183
|
-
parts.push(e + ";");
|
|
184
|
-
}
|
|
185
|
-
}
|
|
171
|
+
parts.push(...dyn.calls);
|
|
186
172
|
|
|
187
173
|
return {
|
|
188
174
|
code: [...new Set(parts)].join("\n"),
|