@eventra_dev/eventra-cli 0.1.30 → 0.1.31
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/README.md +0 -1
- package/dist/engine/engine.js +7 -4
- package/dist/engine/scanner.js +205 -11
- package/dist/utils/config.js +0 -1
- package/dist/utils/processFile.js +3 -3
- package/dist/utils/universal.js +73 -57
- package/package.json +1 -1
- package/src/engine/engine.ts +26 -7
- package/src/engine/scanner.ts +274 -20
- package/src/types.ts +6 -1
- package/src/utils/config.ts +0 -1
- package/src/utils/processFile.ts +3 -3
- package/src/utils/universal.ts +85 -82
- package/tests/run.ts +0 -1
- package/dist/engine/trackingDetector.js +0 -17
- package/src/engine/trackingDetector.ts +0 -17
package/README.md
CHANGED
package/dist/engine/engine.js
CHANGED
|
@@ -14,6 +14,7 @@ const boundary_1 = require("./boundary");
|
|
|
14
14
|
class EventraEngine {
|
|
15
15
|
ts;
|
|
16
16
|
graph;
|
|
17
|
+
wrapperCache = new Map();
|
|
17
18
|
fileResults = new Map();
|
|
18
19
|
fileHash = new Map();
|
|
19
20
|
constructor(root) {
|
|
@@ -54,14 +55,15 @@ class EventraEngine {
|
|
|
54
55
|
if (!source || !checker) {
|
|
55
56
|
return this.empty();
|
|
56
57
|
}
|
|
57
|
-
//
|
|
58
|
+
// update graph
|
|
58
59
|
this.graph.update(file, source);
|
|
59
|
-
const res = (0, scanner_1.scanSource)(source, checker, config);
|
|
60
|
+
const res = (0, scanner_1.scanSource)(source, checker, config, new Set(), undefined, this.wrapperCache);
|
|
60
61
|
this.fileResults.set(file, res);
|
|
61
62
|
return res;
|
|
62
63
|
}
|
|
63
64
|
// UPDATE FILE (incremental)
|
|
64
65
|
updateFile(file, content, config) {
|
|
66
|
+
this.wrapperCache.clear();
|
|
65
67
|
file = this.normalize(file);
|
|
66
68
|
const res = this.scanFile(file, content, config);
|
|
67
69
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
@@ -89,7 +91,7 @@ class EventraEngine {
|
|
|
89
91
|
if (!source || !checker)
|
|
90
92
|
continue;
|
|
91
93
|
this.graph.update(abs, source);
|
|
92
|
-
const r = (0, scanner_1.scanSource)(source, checker, config);
|
|
94
|
+
const r = (0, scanner_1.scanSource)(source, checker, config, new Set(), undefined, this.wrapperCache);
|
|
93
95
|
this.fileResults.set(abs, r);
|
|
94
96
|
}
|
|
95
97
|
return res;
|
|
@@ -104,6 +106,7 @@ class EventraEngine {
|
|
|
104
106
|
}
|
|
105
107
|
// REMOVE FILE
|
|
106
108
|
removeFile(file, config) {
|
|
109
|
+
this.wrapperCache.clear();
|
|
107
110
|
file = this.normalize(file);
|
|
108
111
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
109
112
|
this.ts.removeFile(file);
|
|
@@ -138,7 +141,7 @@ class EventraEngine {
|
|
|
138
141
|
wrappers: [],
|
|
139
142
|
functionWrappers: [],
|
|
140
143
|
sync: { include: [], exclude: [] },
|
|
141
|
-
});
|
|
144
|
+
}, new Set(), undefined, this.wrapperCache);
|
|
142
145
|
this.fileResults.set(abs, r);
|
|
143
146
|
}
|
|
144
147
|
}
|
package/dist/engine/scanner.js
CHANGED
|
@@ -8,7 +8,24 @@ const typescript_1 = __importDefault(require("typescript"));
|
|
|
8
8
|
const resolver_1 = require("./resolver");
|
|
9
9
|
const callResolver_1 = require("./callResolver");
|
|
10
10
|
const functionAnalyzer_1 = require("./functionAnalyzer");
|
|
11
|
+
const exportResolver_1 = require("./exportResolver");
|
|
11
12
|
const boundary_1 = require("./boundary");
|
|
13
|
+
// UTILS
|
|
14
|
+
function getJsxAttrName(name) {
|
|
15
|
+
if (typescript_1.default.isIdentifier(name))
|
|
16
|
+
return name.text;
|
|
17
|
+
if (typescript_1.default.isJsxNamespacedName(name))
|
|
18
|
+
return name.name.text;
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
function getJsxTagName(tag) {
|
|
22
|
+
if (typescript_1.default.isIdentifier(tag))
|
|
23
|
+
return tag.text;
|
|
24
|
+
if (typescript_1.default.isPropertyAccessExpression(tag)) {
|
|
25
|
+
return tag.name.text;
|
|
26
|
+
}
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
12
29
|
function getCallName(expr) {
|
|
13
30
|
if (typescript_1.default.isIdentifier(expr))
|
|
14
31
|
return expr.text;
|
|
@@ -23,7 +40,104 @@ function isValidEvent(v) {
|
|
|
23
40
|
!v.includes(" ") &&
|
|
24
41
|
/^[a-zA-Z0-9._:-]+$/.test(v));
|
|
25
42
|
}
|
|
26
|
-
|
|
43
|
+
// COMPONENT ANALYSIS
|
|
44
|
+
function getComponentPropsMap(fn) {
|
|
45
|
+
const map = new Map();
|
|
46
|
+
const firstParam = fn.parameters[0];
|
|
47
|
+
if (!firstParam)
|
|
48
|
+
return map;
|
|
49
|
+
// ({ event }) => ...
|
|
50
|
+
if (typescript_1.default.isObjectBindingPattern(firstParam.name)) {
|
|
51
|
+
for (const el of firstParam.name.elements) {
|
|
52
|
+
if (!typescript_1.default.isBindingElement(el))
|
|
53
|
+
continue;
|
|
54
|
+
const propName = el.propertyName?.getText() || el.name.getText();
|
|
55
|
+
map.set(el.name.getText(), propName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// (props) => props.event
|
|
59
|
+
if (typescript_1.default.isIdentifier(firstParam.name)) {
|
|
60
|
+
map.set(firstParam.name.text, "*");
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
function findTrackedPropName(fn, checker) {
|
|
65
|
+
const propMap = getComponentPropsMap(fn);
|
|
66
|
+
let result = null;
|
|
67
|
+
function resolveIdentifierToProp(node) {
|
|
68
|
+
const mapped = propMap.get(node.text);
|
|
69
|
+
if (mapped && mapped !== "*")
|
|
70
|
+
return mapped;
|
|
71
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
72
|
+
if (!symbol)
|
|
73
|
+
return null;
|
|
74
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
75
|
+
// const e = props.event
|
|
76
|
+
if (typescript_1.default.isVariableDeclaration(decl) &&
|
|
77
|
+
decl.initializer &&
|
|
78
|
+
typescript_1.default.isPropertyAccessExpression(decl.initializer)) {
|
|
79
|
+
return decl.initializer.name.text;
|
|
80
|
+
}
|
|
81
|
+
// const e = event
|
|
82
|
+
if (typescript_1.default.isVariableDeclaration(decl) &&
|
|
83
|
+
decl.initializer &&
|
|
84
|
+
typescript_1.default.isIdentifier(decl.initializer)) {
|
|
85
|
+
const inner = propMap.get(decl.initializer.text);
|
|
86
|
+
if (inner)
|
|
87
|
+
return inner;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function visit(node) {
|
|
93
|
+
if (result)
|
|
94
|
+
return;
|
|
95
|
+
if (typescript_1.default.isCallExpression(node)) {
|
|
96
|
+
const expr = node.expression;
|
|
97
|
+
const isTrack = (typescript_1.default.isIdentifier(expr) && expr.text === "track") ||
|
|
98
|
+
(typescript_1.default.isPropertyAccessExpression(expr) &&
|
|
99
|
+
expr.name.text === "track");
|
|
100
|
+
if (!isTrack) {
|
|
101
|
+
typescript_1.default.forEachChild(node, visit);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const arg of node.arguments) {
|
|
105
|
+
// track(event)
|
|
106
|
+
if (typescript_1.default.isIdentifier(arg)) {
|
|
107
|
+
const prop = resolveIdentifierToProp(arg);
|
|
108
|
+
if (prop) {
|
|
109
|
+
result = prop;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// track(props.a.b.event)
|
|
114
|
+
if (typescript_1.default.isPropertyAccessExpression(arg)) {
|
|
115
|
+
let current = arg;
|
|
116
|
+
while (typescript_1.default.isPropertyAccessExpression(current)) {
|
|
117
|
+
result = current.name.text;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// track(fn())
|
|
122
|
+
if (typescript_1.default.isCallExpression(arg)) {
|
|
123
|
+
const inner = (0, callResolver_1.resolveFunctionFromCall)(arg.expression, checker);
|
|
124
|
+
if (inner?.body) {
|
|
125
|
+
visit(inner.body);
|
|
126
|
+
if (result)
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
typescript_1.default.forEachChild(node, visit);
|
|
133
|
+
}
|
|
134
|
+
if (fn.body)
|
|
135
|
+
visit(fn.body);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
// MAIN SCANNER
|
|
139
|
+
function scanSource(source, checker, config, visited = new Set(), inheritedParamMap, wrapperCache) {
|
|
140
|
+
wrapperCache = wrapperCache ?? new Map();
|
|
27
141
|
const events = new Set();
|
|
28
142
|
const detectedFunctionWrappers = new Set();
|
|
29
143
|
const detectedComponentWrappers = new Map();
|
|
@@ -32,9 +146,93 @@ function scanSource(source, checker, config, visited = new Set(), inheritedParam
|
|
|
32
146
|
}
|
|
33
147
|
visited.add(source.fileName);
|
|
34
148
|
function visit(node) {
|
|
149
|
+
// JSX WRAPPERS
|
|
150
|
+
if (typescript_1.default.isJsxSelfClosingElement(node) ||
|
|
151
|
+
typescript_1.default.isJsxOpeningElement(node)) {
|
|
152
|
+
const tagName = getJsxTagName(node.tagName);
|
|
153
|
+
// skip html
|
|
154
|
+
if (!tagName || tagName[0] === tagName[0].toLowerCase()) {
|
|
155
|
+
typescript_1.default.forEachChild(node, visit);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let wrapper = config.wrappers.find((w) => w.name === tagName);
|
|
159
|
+
if (!wrapper) {
|
|
160
|
+
let symbol = checker.getSymbolAtLocation(node.tagName);
|
|
161
|
+
let cacheKey = tagName;
|
|
162
|
+
if (symbol) {
|
|
163
|
+
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
164
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
165
|
+
}
|
|
166
|
+
symbol =
|
|
167
|
+
(0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
168
|
+
cacheKey = checker.getFullyQualifiedName(symbol);
|
|
169
|
+
}
|
|
170
|
+
if (wrapperCache?.has(cacheKey)) {
|
|
171
|
+
const cached = wrapperCache.get(cacheKey);
|
|
172
|
+
if (cached) {
|
|
173
|
+
wrapper = { name: tagName, prop: cached };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (symbol) {
|
|
177
|
+
let detected = null;
|
|
178
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
179
|
+
let fn = null;
|
|
180
|
+
if (typescript_1.default.isFunctionDeclaration(decl))
|
|
181
|
+
fn = decl;
|
|
182
|
+
if (typescript_1.default.isVariableDeclaration(decl) &&
|
|
183
|
+
decl.initializer &&
|
|
184
|
+
(typescript_1.default.isArrowFunction(decl.initializer) ||
|
|
185
|
+
typescript_1.default.isFunctionExpression(decl.initializer))) {
|
|
186
|
+
fn = decl.initializer;
|
|
187
|
+
}
|
|
188
|
+
if (!fn)
|
|
189
|
+
continue;
|
|
190
|
+
const prop = findTrackedPropName(fn, checker);
|
|
191
|
+
if (prop) {
|
|
192
|
+
detected = prop;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
wrapperCache?.set(cacheKey, detected);
|
|
197
|
+
if (detected) {
|
|
198
|
+
wrapper = { name: tagName, prop: detected };
|
|
199
|
+
detectedComponentWrappers.set(tagName, detected);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (wrapper) {
|
|
204
|
+
for (const attr of node.attributes.properties) {
|
|
205
|
+
if (!typescript_1.default.isJsxAttribute(attr))
|
|
206
|
+
continue;
|
|
207
|
+
const name = getJsxAttrName(attr.name);
|
|
208
|
+
if (name !== wrapper.prop)
|
|
209
|
+
continue;
|
|
210
|
+
if (!attr.initializer)
|
|
211
|
+
continue;
|
|
212
|
+
if (typescript_1.default.isStringLiteral(attr.initializer)) {
|
|
213
|
+
if (isValidEvent(attr.initializer.text)) {
|
|
214
|
+
events.add(attr.initializer.text);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (typescript_1.default.isJsxExpression(attr.initializer)) {
|
|
218
|
+
const expr = attr.initializer.expression;
|
|
219
|
+
if (!expr)
|
|
220
|
+
continue;
|
|
221
|
+
const res = (0, resolver_1.resolveNodeValue)(expr, checker, inheritedParamMap);
|
|
222
|
+
res.values.forEach((v) => {
|
|
223
|
+
if (isValidEvent(v))
|
|
224
|
+
events.add(v);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
typescript_1.default.forEachChild(node, visit);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// FUNCTION TRACKING
|
|
35
233
|
if (typescript_1.default.isCallExpression(node)) {
|
|
36
234
|
const name = getCallName(node.expression);
|
|
37
|
-
const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
|
|
235
|
+
const isKnownWrapper = config.functionWrappers.some((w) => w.name === name);
|
|
38
236
|
const resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
|
|
39
237
|
const isAutoWrapper = !!resolvedFn && (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn) !== null;
|
|
40
238
|
const isTracking = name === "track" ||
|
|
@@ -46,7 +244,6 @@ function scanSource(source, checker, config, visited = new Set(), inheritedParam
|
|
|
46
244
|
typescript_1.default.forEachChild(node, visit);
|
|
47
245
|
return;
|
|
48
246
|
}
|
|
49
|
-
// авто-детект wrapper
|
|
50
247
|
if (!isKnownWrapper && isAutoWrapper && name !== "track") {
|
|
51
248
|
detectedFunctionWrappers.add(name);
|
|
52
249
|
}
|
|
@@ -61,24 +258,21 @@ function scanSource(source, checker, config, visited = new Set(), inheritedParam
|
|
|
61
258
|
}
|
|
62
259
|
});
|
|
63
260
|
}
|
|
64
|
-
// ✅ ТОЛЬКО правильный аргумент
|
|
65
261
|
if (resolvedFn) {
|
|
66
262
|
const idx = (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn);
|
|
67
263
|
if (idx !== null && node.arguments[idx]) {
|
|
68
264
|
const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
|
|
69
|
-
|
|
70
|
-
if (isValidEvent(v))
|
|
265
|
+
res.values.forEach((v) => {
|
|
266
|
+
if (isValidEvent(v))
|
|
71
267
|
events.add(v);
|
|
72
|
-
|
|
73
|
-
}
|
|
268
|
+
});
|
|
74
269
|
}
|
|
75
270
|
}
|
|
76
|
-
// CROSS FILE
|
|
77
271
|
if (resolvedFn) {
|
|
78
272
|
const sf = resolvedFn.getSourceFile();
|
|
79
273
|
if (!visited.has(sf.fileName)) {
|
|
80
|
-
const res = scanSource(sf, checker, config, visited, paramMap);
|
|
81
|
-
res.events.forEach(e => events.add(e));
|
|
274
|
+
const res = scanSource(sf, checker, config, visited, paramMap, wrapperCache);
|
|
275
|
+
res.events.forEach((e) => events.add(e));
|
|
82
276
|
}
|
|
83
277
|
}
|
|
84
278
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -17,7 +17,6 @@ function normalizeConfig(config) {
|
|
|
17
17
|
endpoint: config.endpoint ?? "",
|
|
18
18
|
wrappers: config.wrappers ?? [],
|
|
19
19
|
functionWrappers: config.functionWrappers ?? [],
|
|
20
|
-
aliases: config.aliases ?? {},
|
|
21
20
|
sync: config.sync ?? {
|
|
22
21
|
include: [
|
|
23
22
|
"**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
|
|
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.processFile = processFile;
|
|
4
4
|
const universal_1 = require("./universal");
|
|
5
5
|
function processFile(file, content) {
|
|
6
|
-
const
|
|
6
|
+
const parsed = (0, universal_1.parseUniversal)(content, file);
|
|
7
7
|
return {
|
|
8
|
-
content: code || "",
|
|
9
|
-
deps: [...new Set(deps.filter(Boolean))],
|
|
8
|
+
content: parsed.code || "",
|
|
9
|
+
deps: [...new Set(parsed.deps.filter(Boolean))],
|
|
10
10
|
};
|
|
11
11
|
}
|
package/dist/utils/universal.js
CHANGED
|
@@ -5,116 +5,132 @@ 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
|
-
//
|
|
8
|
+
// SAFE COMMENT STRIP
|
|
9
9
|
function stripComments(input) {
|
|
10
10
|
return input
|
|
11
11
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
12
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
13
|
-
.replace(/\/\/.*$/gm, "");
|
|
12
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
14
13
|
}
|
|
15
|
-
//
|
|
14
|
+
// TRACK FILTER
|
|
16
15
|
function looksLikeTracking(code) {
|
|
17
|
-
return
|
|
16
|
+
return /\btrack\w*\s*\(/.test(code);
|
|
18
17
|
}
|
|
19
|
-
//
|
|
18
|
+
// SAFE {} PARSER
|
|
19
|
+
function extractBracedExpressions(input) {
|
|
20
|
+
const result = [];
|
|
21
|
+
let depth = 0;
|
|
22
|
+
let start = -1;
|
|
23
|
+
for (let i = 0; i < input.length; i++) {
|
|
24
|
+
const char = input[i];
|
|
25
|
+
if (char === "{") {
|
|
26
|
+
if (depth === 0)
|
|
27
|
+
start = i + 1;
|
|
28
|
+
depth++;
|
|
29
|
+
}
|
|
30
|
+
else if (char === "}") {
|
|
31
|
+
depth--;
|
|
32
|
+
if (depth === 0 && start !== -1) {
|
|
33
|
+
const expr = input.slice(start, i);
|
|
34
|
+
if (looksLikeTracking(expr)) {
|
|
35
|
+
result.push(expr);
|
|
36
|
+
}
|
|
37
|
+
start = -1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
// HTML handlers
|
|
20
44
|
function extractHTMLHandlers(input) {
|
|
21
45
|
const result = [];
|
|
22
|
-
const
|
|
23
|
-
for (const m of
|
|
46
|
+
const regex = /\bon\w+\s*=\s*["']([^"']+)["']/g;
|
|
47
|
+
for (const m of input.matchAll(regex)) {
|
|
24
48
|
if (looksLikeTracking(m[1])) {
|
|
25
49
|
result.push(m[1]);
|
|
26
50
|
}
|
|
27
51
|
}
|
|
28
52
|
return result;
|
|
29
53
|
}
|
|
30
|
-
//
|
|
54
|
+
// VUE handlers
|
|
31
55
|
function extractVueHandlers(input) {
|
|
32
56
|
const result = [];
|
|
33
|
-
const
|
|
34
|
-
for (const m of
|
|
57
|
+
const regex = /(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g;
|
|
58
|
+
for (const m of input.matchAll(regex)) {
|
|
35
59
|
if (looksLikeTracking(m[1])) {
|
|
36
60
|
result.push(m[1]);
|
|
37
61
|
}
|
|
38
62
|
}
|
|
39
63
|
return result;
|
|
40
64
|
}
|
|
41
|
-
//
|
|
65
|
+
// SVELTE handlers
|
|
42
66
|
function extractSvelteHandlers(input) {
|
|
43
67
|
const result = [];
|
|
44
|
-
const
|
|
45
|
-
for (const m of
|
|
68
|
+
const regex = /\bon:[\w]+\s*=\s*{([^}]+)}/g;
|
|
69
|
+
for (const m of input.matchAll(regex)) {
|
|
46
70
|
if (looksLikeTracking(m[1])) {
|
|
47
71
|
result.push(m[1]);
|
|
48
72
|
}
|
|
49
73
|
}
|
|
50
74
|
return result;
|
|
51
75
|
}
|
|
52
|
-
//
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
// SCRIPT EXTRACTION
|
|
77
|
+
function extractScripts(input, file) {
|
|
78
|
+
const scripts = [];
|
|
79
|
+
const deps = [];
|
|
80
|
+
// src scripts
|
|
81
|
+
const srcRegex = /<script\b[^>]*?\bsrc=["'](.+?)["'][^>]*>/g;
|
|
82
|
+
for (const m of input.matchAll(srcRegex)) {
|
|
83
|
+
const src = m[1];
|
|
84
|
+
const resolved = src.startsWith("/")
|
|
85
|
+
? path_1.default.resolve(process.cwd(), "." + src)
|
|
86
|
+
: path_1.default.resolve(path_1.default.dirname(file), src);
|
|
87
|
+
deps.push(resolved);
|
|
88
|
+
}
|
|
89
|
+
// inline scripts
|
|
90
|
+
const scriptRegex = /<script\b[^>]*>([\s\S]*?)<\/script>/g;
|
|
91
|
+
for (const m of input.matchAll(scriptRegex)) {
|
|
92
|
+
if (m[1]?.trim()) {
|
|
93
|
+
scripts.push(m[1]);
|
|
59
94
|
}
|
|
60
95
|
}
|
|
61
|
-
|
|
96
|
+
const noScripts = input.replace(scriptRegex, "");
|
|
97
|
+
return { scripts, deps, noScripts };
|
|
62
98
|
}
|
|
63
|
-
//
|
|
99
|
+
// DYNAMIC IMPORTS
|
|
64
100
|
function extractDynamicImports(input, file) {
|
|
65
101
|
const deps = [];
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
for (const m of matches) {
|
|
102
|
+
const regex = /import\(\s*["'](.+?)["']\s*\)/g;
|
|
103
|
+
for (const m of input.matchAll(regex)) {
|
|
69
104
|
const mod = m[1];
|
|
70
105
|
const resolved = mod.startsWith("/")
|
|
71
106
|
? path_1.default.resolve(process.cwd(), "." + mod)
|
|
72
107
|
: path_1.default.resolve(path_1.default.dirname(file), mod);
|
|
73
108
|
deps.push(resolved);
|
|
74
|
-
if (looksLikeTracking(mod)) {
|
|
75
|
-
calls.push(`import("${mod}")`);
|
|
76
|
-
}
|
|
77
109
|
}
|
|
78
|
-
return
|
|
110
|
+
return deps;
|
|
79
111
|
}
|
|
80
|
-
//
|
|
112
|
+
// MAIN
|
|
81
113
|
function parseUniversal(content, file) {
|
|
82
114
|
const parts = [];
|
|
83
115
|
const deps = [];
|
|
84
116
|
const clean = stripComments(content);
|
|
85
|
-
|
|
117
|
+
/* ---------- ASTRO FRONTMATTER ---------- */
|
|
86
118
|
const fm = clean.match(/^---([\s\S]*?)---/);
|
|
87
119
|
if (fm?.[1]) {
|
|
88
120
|
parts.push(fm[1]);
|
|
89
121
|
}
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
? path_1.default.resolve(process.cwd(), "." + src)
|
|
96
|
-
: path_1.default.resolve(path_1.default.dirname(file), src);
|
|
97
|
-
deps.push(resolved);
|
|
98
|
-
}
|
|
99
|
-
// INLINE <script>
|
|
100
|
-
const scripts = clean.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
|
|
101
|
-
for (const m of scripts) {
|
|
102
|
-
if (m[2].trim()) {
|
|
103
|
-
parts.push(m[2]);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
// TEMPLATE PART
|
|
107
|
-
const noScripts = clean.replace(/<script[\s\S]*?<\/script>/g, "");
|
|
108
|
-
// handlers
|
|
122
|
+
/* ---------- SCRIPTS ---------- */
|
|
123
|
+
const { scripts, deps: scriptDeps, noScripts } = extractScripts(clean, file);
|
|
124
|
+
parts.push(...scripts);
|
|
125
|
+
deps.push(...scriptDeps);
|
|
126
|
+
/* ---------- HANDLERS ---------- */
|
|
109
127
|
parts.push(...extractHTMLHandlers(noScripts));
|
|
110
128
|
parts.push(...extractVueHandlers(noScripts));
|
|
111
129
|
parts.push(...extractSvelteHandlers(noScripts));
|
|
112
|
-
|
|
113
|
-
parts.push(...
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
deps.push(...dyn.deps);
|
|
117
|
-
parts.push(...dyn.calls);
|
|
130
|
+
/* ---------- JSX / TEMPLATE ---------- */
|
|
131
|
+
parts.push(...extractBracedExpressions(clean));
|
|
132
|
+
/* ---------- IMPORTS ---------- */
|
|
133
|
+
deps.push(...extractDynamicImports(clean, file));
|
|
118
134
|
return {
|
|
119
135
|
code: [...new Set(parts)].join("\n"),
|
|
120
136
|
deps: [...new Set(deps.filter(Boolean))],
|
package/package.json
CHANGED
package/src/engine/engine.ts
CHANGED
|
@@ -2,16 +2,16 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
|
|
4
4
|
import { TSService } from "./languageService";
|
|
5
|
-
import {
|
|
5
|
+
import { scanSource } from "./scanner";
|
|
6
6
|
import { DependencyGraph } from "./graph";
|
|
7
7
|
import { hash } from "./hash";
|
|
8
|
-
import {
|
|
8
|
+
import {EventraConfig, ScanResult} from "../types";
|
|
9
9
|
import { isExternalFile } from "./boundary";
|
|
10
10
|
|
|
11
11
|
export class EventraEngine {
|
|
12
12
|
private ts: TSService;
|
|
13
13
|
private graph: DependencyGraph;
|
|
14
|
-
|
|
14
|
+
private wrapperCache = new Map<string, string | null>();
|
|
15
15
|
private fileResults = new Map<string, ScanResult>();
|
|
16
16
|
private fileHash = new Map<string, string>();
|
|
17
17
|
|
|
@@ -71,10 +71,17 @@ export class EventraEngine {
|
|
|
71
71
|
return this.empty();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// update graph
|
|
75
75
|
this.graph.update(file, source);
|
|
76
76
|
|
|
77
|
-
const res = scanSource(
|
|
77
|
+
const res = scanSource(
|
|
78
|
+
source,
|
|
79
|
+
checker,
|
|
80
|
+
config,
|
|
81
|
+
new Set(),
|
|
82
|
+
undefined,
|
|
83
|
+
this.wrapperCache
|
|
84
|
+
);
|
|
78
85
|
this.fileResults.set(file, res);
|
|
79
86
|
|
|
80
87
|
return res;
|
|
@@ -86,6 +93,7 @@ export class EventraEngine {
|
|
|
86
93
|
content: string,
|
|
87
94
|
config: EventraConfig
|
|
88
95
|
) {
|
|
96
|
+
this.wrapperCache.clear();
|
|
89
97
|
file = this.normalize(file);
|
|
90
98
|
|
|
91
99
|
const res = this.scanFile(file, content, config);
|
|
@@ -124,7 +132,14 @@ export class EventraEngine {
|
|
|
124
132
|
|
|
125
133
|
this.graph.update(abs, source);
|
|
126
134
|
|
|
127
|
-
const r = scanSource(
|
|
135
|
+
const r = scanSource(
|
|
136
|
+
source,
|
|
137
|
+
checker,
|
|
138
|
+
config,
|
|
139
|
+
new Set(),
|
|
140
|
+
undefined,
|
|
141
|
+
this.wrapperCache
|
|
142
|
+
);
|
|
128
143
|
this.fileResults.set(abs, r);
|
|
129
144
|
}
|
|
130
145
|
|
|
@@ -144,6 +159,7 @@ export class EventraEngine {
|
|
|
144
159
|
|
|
145
160
|
// REMOVE FILE
|
|
146
161
|
removeFile(file: string, config?: EventraConfig) {
|
|
162
|
+
this.wrapperCache.clear();
|
|
147
163
|
file = this.normalize(file);
|
|
148
164
|
|
|
149
165
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
@@ -191,7 +207,10 @@ export class EventraEngine {
|
|
|
191
207
|
wrappers: [],
|
|
192
208
|
functionWrappers: [],
|
|
193
209
|
sync: { include: [], exclude: [] },
|
|
194
|
-
}
|
|
210
|
+
},
|
|
211
|
+
new Set(),
|
|
212
|
+
undefined,
|
|
213
|
+
this.wrapperCache
|
|
195
214
|
);
|
|
196
215
|
|
|
197
216
|
this.fileResults.set(abs, r);
|
package/src/engine/scanner.ts
CHANGED
|
@@ -2,9 +2,27 @@ import ts from "typescript";
|
|
|
2
2
|
import { resolveNodeValue } from "./resolver";
|
|
3
3
|
import { resolveFunctionFromCall } from "./callResolver";
|
|
4
4
|
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveExportedSymbol } from "./exportResolver";
|
|
6
|
+
import { EventraConfig, ScanResult } from "../types";
|
|
6
7
|
import { isExternalFile } from "./boundary";
|
|
7
8
|
|
|
9
|
+
// UTILS
|
|
10
|
+
function getJsxAttrName(name: ts.JsxAttributeName): string {
|
|
11
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
12
|
+
if (ts.isJsxNamespacedName(name)) return name.name.text;
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getJsxTagName(tag: ts.JsxTagNameExpression): string {
|
|
17
|
+
if (ts.isIdentifier(tag)) return tag.text;
|
|
18
|
+
|
|
19
|
+
if (ts.isPropertyAccessExpression(tag)) {
|
|
20
|
+
return tag.name.text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
function getCallName(expr: ts.Expression): string {
|
|
9
27
|
if (ts.isIdentifier(expr)) return expr.text;
|
|
10
28
|
if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
|
|
@@ -21,19 +39,144 @@ function isValidEvent(v: string) {
|
|
|
21
39
|
);
|
|
22
40
|
}
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
42
|
+
// COMPONENT ANALYSIS
|
|
43
|
+
function getComponentPropsMap(
|
|
44
|
+
fn: ts.FunctionLikeDeclaration
|
|
45
|
+
): Map<string, string> {
|
|
46
|
+
const map = new Map<string, string>();
|
|
47
|
+
const firstParam = fn.parameters[0];
|
|
48
|
+
if (!firstParam) return map;
|
|
49
|
+
|
|
50
|
+
// ({ event }) => ...
|
|
51
|
+
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
52
|
+
for (const el of firstParam.name.elements) {
|
|
53
|
+
if (!ts.isBindingElement(el)) continue;
|
|
54
|
+
|
|
55
|
+
const propName =
|
|
56
|
+
el.propertyName?.getText() || el.name.getText();
|
|
57
|
+
|
|
58
|
+
map.set(el.name.getText(), propName);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
29
61
|
|
|
62
|
+
// (props) => props.event
|
|
63
|
+
if (ts.isIdentifier(firstParam.name)) {
|
|
64
|
+
map.set(firstParam.name.text, "*");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findTrackedPropName(
|
|
71
|
+
fn: ts.FunctionLikeDeclaration,
|
|
72
|
+
checker: ts.TypeChecker
|
|
73
|
+
): string | null {
|
|
74
|
+
const propMap = getComponentPropsMap(fn);
|
|
75
|
+
let result: string | null = null;
|
|
76
|
+
|
|
77
|
+
function resolveIdentifierToProp(
|
|
78
|
+
node: ts.Identifier
|
|
79
|
+
): string | null {
|
|
80
|
+
const mapped = propMap.get(node.text);
|
|
81
|
+
if (mapped && mapped !== "*") return mapped;
|
|
82
|
+
|
|
83
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
84
|
+
if (!symbol) return null;
|
|
85
|
+
|
|
86
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
87
|
+
// const e = props.event
|
|
88
|
+
if (
|
|
89
|
+
ts.isVariableDeclaration(decl) &&
|
|
90
|
+
decl.initializer &&
|
|
91
|
+
ts.isPropertyAccessExpression(decl.initializer)
|
|
92
|
+
) {
|
|
93
|
+
return decl.initializer.name.text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// const e = event
|
|
97
|
+
if (
|
|
98
|
+
ts.isVariableDeclaration(decl) &&
|
|
99
|
+
decl.initializer &&
|
|
100
|
+
ts.isIdentifier(decl.initializer)
|
|
101
|
+
) {
|
|
102
|
+
const inner = propMap.get(decl.initializer.text);
|
|
103
|
+
if (inner) return inner;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function visit(node: ts.Node) {
|
|
111
|
+
if (result) return;
|
|
112
|
+
|
|
113
|
+
if (ts.isCallExpression(node)) {
|
|
114
|
+
const expr = node.expression;
|
|
115
|
+
|
|
116
|
+
const isTrack =
|
|
117
|
+
(ts.isIdentifier(expr) && expr.text === "track") ||
|
|
118
|
+
(ts.isPropertyAccessExpression(expr) &&
|
|
119
|
+
expr.name.text === "track");
|
|
120
|
+
|
|
121
|
+
if (!isTrack) {
|
|
122
|
+
ts.forEachChild(node, visit);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const arg of node.arguments) {
|
|
127
|
+
// track(event)
|
|
128
|
+
if (ts.isIdentifier(arg)) {
|
|
129
|
+
const prop = resolveIdentifierToProp(arg);
|
|
130
|
+
if (prop) {
|
|
131
|
+
result = prop;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// track(props.a.b.event)
|
|
137
|
+
if (ts.isPropertyAccessExpression(arg)) {
|
|
138
|
+
let current = arg;
|
|
139
|
+
|
|
140
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
141
|
+
result = current.name.text;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// track(fn())
|
|
147
|
+
if (ts.isCallExpression(arg)) {
|
|
148
|
+
const inner = resolveFunctionFromCall(
|
|
149
|
+
arg.expression,
|
|
150
|
+
checker
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (inner?.body) {
|
|
154
|
+
visit(inner.body);
|
|
155
|
+
if (result) return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ts.forEachChild(node, visit);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (fn.body) visit(fn.body);
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// MAIN SCANNER
|
|
30
170
|
export function scanSource(
|
|
31
171
|
source: ts.SourceFile,
|
|
32
172
|
checker: ts.TypeChecker,
|
|
33
173
|
config: EventraConfig,
|
|
34
174
|
visited = new Set<string>(),
|
|
35
|
-
inheritedParamMap?: Map<string, ts.Expression
|
|
175
|
+
inheritedParamMap?: Map<string, ts.Expression>,
|
|
176
|
+
wrapperCache?: Map<string, string | null>
|
|
36
177
|
): ScanResult {
|
|
178
|
+
wrapperCache = wrapperCache ?? new Map();
|
|
179
|
+
|
|
37
180
|
const events = new Set<string>();
|
|
38
181
|
const detectedFunctionWrappers = new Set<string>();
|
|
39
182
|
const detectedComponentWrappers = new Map<string, string>();
|
|
@@ -45,14 +188,127 @@ export function scanSource(
|
|
|
45
188
|
visited.add(source.fileName);
|
|
46
189
|
|
|
47
190
|
function visit(node: ts.Node) {
|
|
191
|
+
// JSX WRAPPERS
|
|
192
|
+
if (
|
|
193
|
+
ts.isJsxSelfClosingElement(node) ||
|
|
194
|
+
ts.isJsxOpeningElement(node)
|
|
195
|
+
) {
|
|
196
|
+
const tagName = getJsxTagName(node.tagName);
|
|
197
|
+
|
|
198
|
+
// skip html
|
|
199
|
+
if (!tagName || tagName[0] === tagName[0].toLowerCase()) {
|
|
200
|
+
ts.forEachChild(node, visit);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let wrapper = config.wrappers.find(
|
|
205
|
+
(w) => w.name === tagName
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (!wrapper) {
|
|
209
|
+
let symbol = checker.getSymbolAtLocation(node.tagName);
|
|
210
|
+
|
|
211
|
+
let cacheKey = tagName;
|
|
212
|
+
|
|
213
|
+
if (symbol) {
|
|
214
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
215
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
symbol =
|
|
219
|
+
resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
220
|
+
|
|
221
|
+
cacheKey = checker.getFullyQualifiedName(symbol);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (wrapperCache?.has(cacheKey)) {
|
|
225
|
+
const cached = wrapperCache.get(cacheKey);
|
|
226
|
+
if (cached) {
|
|
227
|
+
wrapper = { name: tagName, prop: cached };
|
|
228
|
+
}
|
|
229
|
+
} else if (symbol) {
|
|
230
|
+
let detected: string | null = null;
|
|
231
|
+
|
|
232
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
233
|
+
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
234
|
+
|
|
235
|
+
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
ts.isVariableDeclaration(decl) &&
|
|
239
|
+
decl.initializer &&
|
|
240
|
+
(ts.isArrowFunction(decl.initializer) ||
|
|
241
|
+
ts.isFunctionExpression(decl.initializer))
|
|
242
|
+
) {
|
|
243
|
+
fn = decl.initializer;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!fn) continue;
|
|
247
|
+
|
|
248
|
+
const prop = findTrackedPropName(fn, checker);
|
|
249
|
+
|
|
250
|
+
if (prop) {
|
|
251
|
+
detected = prop;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
wrapperCache?.set(cacheKey, detected);
|
|
257
|
+
|
|
258
|
+
if (detected) {
|
|
259
|
+
wrapper = { name: tagName, prop: detected };
|
|
260
|
+
detectedComponentWrappers.set(tagName, detected);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (wrapper) {
|
|
266
|
+
for (const attr of node.attributes.properties) {
|
|
267
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
268
|
+
|
|
269
|
+
const name = getJsxAttrName(attr.name);
|
|
270
|
+
if (name !== wrapper.prop) continue;
|
|
271
|
+
if (!attr.initializer) continue;
|
|
272
|
+
|
|
273
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
274
|
+
if (isValidEvent(attr.initializer.text)) {
|
|
275
|
+
events.add(attr.initializer.text);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (ts.isJsxExpression(attr.initializer)) {
|
|
280
|
+
const expr = attr.initializer.expression;
|
|
281
|
+
if (!expr) continue;
|
|
282
|
+
|
|
283
|
+
const res = resolveNodeValue(
|
|
284
|
+
expr,
|
|
285
|
+
checker,
|
|
286
|
+
inheritedParamMap
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
res.values.forEach((v) => {
|
|
290
|
+
if (isValidEvent(v)) events.add(v);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
ts.forEachChild(node, visit);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// FUNCTION TRACKING
|
|
48
301
|
if (ts.isCallExpression(node)) {
|
|
49
302
|
const name = getCallName(node.expression);
|
|
50
303
|
|
|
51
304
|
const isKnownWrapper = config.functionWrappers.some(
|
|
52
|
-
w => w.name === name
|
|
305
|
+
(w) => w.name === name
|
|
53
306
|
);
|
|
54
307
|
|
|
55
|
-
const resolvedFn = resolveFunctionFromCall(
|
|
308
|
+
const resolvedFn = resolveFunctionFromCall(
|
|
309
|
+
node.expression,
|
|
310
|
+
checker
|
|
311
|
+
);
|
|
56
312
|
|
|
57
313
|
const isAutoWrapper =
|
|
58
314
|
!!resolvedFn && findTrackParamIndex(resolvedFn) !== null;
|
|
@@ -62,14 +318,15 @@ export function scanSource(
|
|
|
62
318
|
isKnownWrapper ||
|
|
63
319
|
(isAutoWrapper &&
|
|
64
320
|
resolvedFn &&
|
|
65
|
-
!isExternalFile(
|
|
321
|
+
!isExternalFile(
|
|
322
|
+
resolvedFn.getSourceFile().fileName
|
|
323
|
+
));
|
|
66
324
|
|
|
67
325
|
if (!isTracking) {
|
|
68
326
|
ts.forEachChild(node, visit);
|
|
69
327
|
return;
|
|
70
328
|
}
|
|
71
329
|
|
|
72
|
-
// авто-детект wrapper
|
|
73
330
|
if (!isKnownWrapper && isAutoWrapper && name !== "track") {
|
|
74
331
|
detectedFunctionWrappers.add(name);
|
|
75
332
|
}
|
|
@@ -87,7 +344,6 @@ export function scanSource(
|
|
|
87
344
|
});
|
|
88
345
|
}
|
|
89
346
|
|
|
90
|
-
// ✅ ТОЛЬКО правильный аргумент
|
|
91
347
|
if (resolvedFn) {
|
|
92
348
|
const idx = findTrackParamIndex(resolvedFn);
|
|
93
349
|
|
|
@@ -98,15 +354,12 @@ export function scanSource(
|
|
|
98
354
|
paramMap
|
|
99
355
|
);
|
|
100
356
|
|
|
101
|
-
|
|
102
|
-
if (isValidEvent(v))
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
357
|
+
res.values.forEach((v) => {
|
|
358
|
+
if (isValidEvent(v)) events.add(v);
|
|
359
|
+
});
|
|
106
360
|
}
|
|
107
361
|
}
|
|
108
362
|
|
|
109
|
-
// CROSS FILE
|
|
110
363
|
if (resolvedFn) {
|
|
111
364
|
const sf = resolvedFn.getSourceFile();
|
|
112
365
|
|
|
@@ -116,10 +369,11 @@ export function scanSource(
|
|
|
116
369
|
checker,
|
|
117
370
|
config,
|
|
118
371
|
visited,
|
|
119
|
-
paramMap
|
|
372
|
+
paramMap,
|
|
373
|
+
wrapperCache
|
|
120
374
|
);
|
|
121
375
|
|
|
122
|
-
res.events.forEach(e => events.add(e));
|
|
376
|
+
res.events.forEach((e) => events.add(e));
|
|
123
377
|
}
|
|
124
378
|
}
|
|
125
379
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type ScanMode = "strict" | "hybrid" | "discovery";
|
|
2
2
|
|
|
3
|
+
export type ScanResult = {
|
|
4
|
+
events: Set<string>;
|
|
5
|
+
detectedFunctionWrappers: Set<string>;
|
|
6
|
+
detectedComponentWrappers: Map<string, string>;
|
|
7
|
+
};
|
|
8
|
+
|
|
3
9
|
export type ParseResult = {
|
|
4
10
|
code: string;
|
|
5
11
|
deps: string[];
|
|
@@ -22,7 +28,6 @@ export type EventraConfig = {
|
|
|
22
28
|
mode?: ScanMode;
|
|
23
29
|
wrappers: ComponentWrapper[];
|
|
24
30
|
functionWrappers: FunctionWrapper[];
|
|
25
|
-
aliases?: Record<string, string>;
|
|
26
31
|
sync: {
|
|
27
32
|
include: string[];
|
|
28
33
|
exclude: string[];
|
package/src/utils/config.ts
CHANGED
|
@@ -13,7 +13,6 @@ export function normalizeConfig(
|
|
|
13
13
|
endpoint: config.endpoint ?? "",
|
|
14
14
|
wrappers: config.wrappers ?? [],
|
|
15
15
|
functionWrappers: config.functionWrappers ?? [],
|
|
16
|
-
aliases: config.aliases ?? {},
|
|
17
16
|
sync: config.sync ?? {
|
|
18
17
|
include: [
|
|
19
18
|
"**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
|
package/src/utils/processFile.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parseUniversal } from "./universal";
|
|
2
2
|
|
|
3
3
|
export function processFile(file: string, content: string) {
|
|
4
|
-
const
|
|
4
|
+
const parsed = parseUniversal(content, file);
|
|
5
5
|
|
|
6
6
|
return {
|
|
7
|
-
content: code || "",
|
|
8
|
-
deps: [...new Set(deps.filter(Boolean))],
|
|
7
|
+
content: parsed.code || "",
|
|
8
|
+
deps: [...new Set(parsed.deps.filter(Boolean))],
|
|
9
9
|
};
|
|
10
10
|
}
|
package/src/utils/universal.ts
CHANGED
|
@@ -1,28 +1,53 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { ParseResult } from "../types";
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// SAFE COMMENT STRIP
|
|
5
5
|
function stripComments(input: string): string {
|
|
6
6
|
return input
|
|
7
7
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
8
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
9
|
-
.replace(/\/\/.*$/gm, "");
|
|
8
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
//
|
|
11
|
+
// TRACK FILTER
|
|
13
12
|
function looksLikeTracking(code: string) {
|
|
14
|
-
return
|
|
13
|
+
return /\btrack\w*\s*\(/.test(code);
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
//
|
|
16
|
+
// SAFE {} PARSER
|
|
17
|
+
function extractBracedExpressions(input: string): string[] {
|
|
18
|
+
const result: string[] = [];
|
|
19
|
+
|
|
20
|
+
let depth = 0;
|
|
21
|
+
let start = -1;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < input.length; i++) {
|
|
24
|
+
const char = input[i];
|
|
25
|
+
|
|
26
|
+
if (char === "{") {
|
|
27
|
+
if (depth === 0) start = i + 1;
|
|
28
|
+
depth++;
|
|
29
|
+
} else if (char === "}") {
|
|
30
|
+
depth--;
|
|
31
|
+
if (depth === 0 && start !== -1) {
|
|
32
|
+
const expr = input.slice(start, i);
|
|
33
|
+
if (looksLikeTracking(expr)) {
|
|
34
|
+
result.push(expr);
|
|
35
|
+
}
|
|
36
|
+
start = -1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// HTML handlers
|
|
18
45
|
function extractHTMLHandlers(input: string): string[] {
|
|
19
46
|
const result: string[] = [];
|
|
20
47
|
|
|
21
|
-
const
|
|
22
|
-
/\bon\w+\s*=\s*["']([^"']+)["']/g
|
|
23
|
-
);
|
|
48
|
+
const regex = /\bon\w+\s*=\s*["']([^"']+)["']/g;
|
|
24
49
|
|
|
25
|
-
for (const m of
|
|
50
|
+
for (const m of input.matchAll(regex)) {
|
|
26
51
|
if (looksLikeTracking(m[1])) {
|
|
27
52
|
result.push(m[1]);
|
|
28
53
|
}
|
|
@@ -31,15 +56,13 @@ function extractHTMLHandlers(input: string): string[] {
|
|
|
31
56
|
return result;
|
|
32
57
|
}
|
|
33
58
|
|
|
34
|
-
//
|
|
59
|
+
// VUE handlers
|
|
35
60
|
function extractVueHandlers(input: string): string[] {
|
|
36
61
|
const result: string[] = [];
|
|
37
62
|
|
|
38
|
-
const
|
|
39
|
-
/(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g
|
|
40
|
-
);
|
|
63
|
+
const regex = /(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g;
|
|
41
64
|
|
|
42
|
-
for (const m of
|
|
65
|
+
for (const m of input.matchAll(regex)) {
|
|
43
66
|
if (looksLikeTracking(m[1])) {
|
|
44
67
|
result.push(m[1]);
|
|
45
68
|
}
|
|
@@ -48,15 +71,13 @@ function extractVueHandlers(input: string): string[] {
|
|
|
48
71
|
return result;
|
|
49
72
|
}
|
|
50
73
|
|
|
51
|
-
//
|
|
74
|
+
// SVELTE handlers
|
|
52
75
|
function extractSvelteHandlers(input: string): string[] {
|
|
53
76
|
const result: string[] = [];
|
|
54
77
|
|
|
55
|
-
const
|
|
56
|
-
/\bon:[\w]+\s*=\s*{([^}]+)}/g
|
|
57
|
-
);
|
|
78
|
+
const regex = /\bon:[\w]+\s*=\s*{([^}]+)}/g;
|
|
58
79
|
|
|
59
|
-
for (const m of
|
|
80
|
+
for (const m of input.matchAll(regex)) {
|
|
60
81
|
if (looksLikeTracking(m[1])) {
|
|
61
82
|
result.push(m[1]);
|
|
62
83
|
}
|
|
@@ -65,34 +86,47 @@ function extractSvelteHandlers(input: string): string[] {
|
|
|
65
86
|
return result;
|
|
66
87
|
}
|
|
67
88
|
|
|
68
|
-
//
|
|
69
|
-
function
|
|
70
|
-
const
|
|
89
|
+
// SCRIPT EXTRACTION
|
|
90
|
+
function extractScripts(input: string, file: string) {
|
|
91
|
+
const scripts: string[] = [];
|
|
92
|
+
const deps: string[] = [];
|
|
71
93
|
|
|
72
|
-
|
|
94
|
+
// src scripts
|
|
95
|
+
const srcRegex =
|
|
96
|
+
/<script\b[^>]*?\bsrc=["'](.+?)["'][^>]*>/g;
|
|
73
97
|
|
|
74
|
-
for (const m of
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
for (const m of input.matchAll(srcRegex)) {
|
|
99
|
+
const src = m[1];
|
|
100
|
+
|
|
101
|
+
const resolved = src.startsWith("/")
|
|
102
|
+
? path.resolve(process.cwd(), "." + src)
|
|
103
|
+
: path.resolve(path.dirname(file), src);
|
|
104
|
+
|
|
105
|
+
deps.push(resolved);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// inline scripts
|
|
109
|
+
const scriptRegex =
|
|
110
|
+
/<script\b[^>]*>([\s\S]*?)<\/script>/g;
|
|
111
|
+
|
|
112
|
+
for (const m of input.matchAll(scriptRegex)) {
|
|
113
|
+
if (m[1]?.trim()) {
|
|
114
|
+
scripts.push(m[1]);
|
|
77
115
|
}
|
|
78
116
|
}
|
|
79
117
|
|
|
80
|
-
|
|
118
|
+
const noScripts = input.replace(scriptRegex, "");
|
|
119
|
+
|
|
120
|
+
return { scripts, deps, noScripts };
|
|
81
121
|
}
|
|
82
122
|
|
|
83
|
-
//
|
|
84
|
-
function extractDynamicImports(
|
|
85
|
-
input: string,
|
|
86
|
-
file: string
|
|
87
|
-
): { deps: string[]; calls: string[] } {
|
|
123
|
+
// DYNAMIC IMPORTS
|
|
124
|
+
function extractDynamicImports(input: string, file: string) {
|
|
88
125
|
const deps: string[] = [];
|
|
89
|
-
const calls: string[] = [];
|
|
90
126
|
|
|
91
|
-
const
|
|
92
|
-
/import\(\s*["'](.+?)["']\s*\)/g
|
|
93
|
-
);
|
|
127
|
+
const regex = /import\(\s*["'](.+?)["']\s*\)/g;
|
|
94
128
|
|
|
95
|
-
for (const m of
|
|
129
|
+
for (const m of input.matchAll(regex)) {
|
|
96
130
|
const mod = m[1];
|
|
97
131
|
|
|
98
132
|
const resolved = mod.startsWith("/")
|
|
@@ -100,16 +134,12 @@ function extractDynamicImports(
|
|
|
100
134
|
: path.resolve(path.dirname(file), mod);
|
|
101
135
|
|
|
102
136
|
deps.push(resolved);
|
|
103
|
-
|
|
104
|
-
if (looksLikeTracking(mod)) {
|
|
105
|
-
calls.push(`import("${mod}")`);
|
|
106
|
-
}
|
|
107
137
|
}
|
|
108
138
|
|
|
109
|
-
return
|
|
139
|
+
return deps;
|
|
110
140
|
}
|
|
111
141
|
|
|
112
|
-
//
|
|
142
|
+
// MAIN
|
|
113
143
|
export function parseUniversal(
|
|
114
144
|
content: string,
|
|
115
145
|
file: string
|
|
@@ -119,56 +149,29 @@ export function parseUniversal(
|
|
|
119
149
|
|
|
120
150
|
const clean = stripComments(content);
|
|
121
151
|
|
|
122
|
-
|
|
152
|
+
/* ---------- ASTRO FRONTMATTER ---------- */
|
|
123
153
|
const fm = clean.match(/^---([\s\S]*?)---/);
|
|
124
154
|
if (fm?.[1]) {
|
|
125
155
|
parts.push(fm[1]);
|
|
126
156
|
}
|
|
127
157
|
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
for (const s of scriptSrc) {
|
|
134
|
-
const src = s[1];
|
|
135
|
-
|
|
136
|
-
const resolved = src.startsWith("/")
|
|
137
|
-
? path.resolve(process.cwd(), "." + src)
|
|
138
|
-
: path.resolve(path.dirname(file), src);
|
|
139
|
-
|
|
140
|
-
deps.push(resolved);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// INLINE <script>
|
|
144
|
-
const scripts = clean.matchAll(
|
|
145
|
-
/<script\b([^>]*)>([\s\S]*?)<\/script>/g
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
for (const m of scripts) {
|
|
149
|
-
if (m[2].trim()) {
|
|
150
|
-
parts.push(m[2]);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
158
|
+
/* ---------- SCRIPTS ---------- */
|
|
159
|
+
const { scripts, deps: scriptDeps, noScripts } =
|
|
160
|
+
extractScripts(clean, file);
|
|
153
161
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
/<script[\s\S]*?<\/script>/g,
|
|
157
|
-
""
|
|
158
|
-
);
|
|
162
|
+
parts.push(...scripts);
|
|
163
|
+
deps.push(...scriptDeps);
|
|
159
164
|
|
|
160
|
-
|
|
165
|
+
/* ---------- HANDLERS ---------- */
|
|
161
166
|
parts.push(...extractHTMLHandlers(noScripts));
|
|
162
167
|
parts.push(...extractVueHandlers(noScripts));
|
|
163
168
|
parts.push(...extractSvelteHandlers(noScripts));
|
|
164
169
|
|
|
165
|
-
|
|
166
|
-
parts.push(...
|
|
170
|
+
/* ---------- JSX / TEMPLATE ---------- */
|
|
171
|
+
parts.push(...extractBracedExpressions(clean));
|
|
167
172
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
deps.push(...dyn.deps);
|
|
171
|
-
parts.push(...dyn.calls);
|
|
173
|
+
/* ---------- IMPORTS ---------- */
|
|
174
|
+
deps.push(...extractDynamicImports(clean, file));
|
|
172
175
|
|
|
173
176
|
return {
|
|
174
177
|
code: [...new Set(parts)].join("\n"),
|
package/tests/run.ts
CHANGED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.isTrackingCall = isTrackingCall;
|
|
7
|
-
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
-
function isTrackingCall(node) {
|
|
9
|
-
const expr = node.expression;
|
|
10
|
-
if (typescript_1.default.isIdentifier(expr)) {
|
|
11
|
-
return expr.text === "track";
|
|
12
|
-
}
|
|
13
|
-
if (typescript_1.default.isPropertyAccessExpression(expr)) {
|
|
14
|
-
return expr.name.text === "track";
|
|
15
|
-
}
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
|
|
3
|
-
export function isTrackingCall(
|
|
4
|
-
node: ts.CallExpression
|
|
5
|
-
): boolean {
|
|
6
|
-
const expr = node.expression;
|
|
7
|
-
|
|
8
|
-
if (ts.isIdentifier(expr)) {
|
|
9
|
-
return expr.text === "track";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (ts.isPropertyAccessExpression(expr)) {
|
|
13
|
-
return expr.name.text === "track";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return false;
|
|
17
|
-
}
|