@eventra_dev/eventra-cli 0.1.29 → 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/functionAnalyzer.js +15 -50
- package/dist/engine/resolver.js +57 -138
- package/dist/engine/scanner.js +205 -54
- package/dist/utils/config.js +0 -1
- package/dist/utils/processFile.js +3 -3
- package/dist/utils/universal.js +91 -94
- package/package.json +1 -1
- package/src/engine/engine.ts +26 -7
- package/src/engine/functionAnalyzer.ts +14 -54
- package/src/engine/resolver.ts +83 -184
- package/src/engine/scanner.ts +263 -75
- 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 +108 -119
- package/tests/run.ts +0 -1
- package/dist/engine/trackingDetector.js +0 -68
- package/src/engine/trackingDetector.ts +0 -94
package/dist/engine/scanner.js
CHANGED
|
@@ -7,10 +7,25 @@ exports.scanSource = scanSource;
|
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
8
|
const resolver_1 = require("./resolver");
|
|
9
9
|
const callResolver_1 = require("./callResolver");
|
|
10
|
-
const exportResolver_1 = require("./exportResolver");
|
|
11
10
|
const functionAnalyzer_1 = require("./functionAnalyzer");
|
|
11
|
+
const exportResolver_1 = require("./exportResolver");
|
|
12
12
|
const boundary_1 = require("./boundary");
|
|
13
|
-
|
|
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
|
+
}
|
|
14
29
|
function getCallName(expr) {
|
|
15
30
|
if (typescript_1.default.isIdentifier(expr))
|
|
16
31
|
return expr.text;
|
|
@@ -25,31 +40,104 @@ function isValidEvent(v) {
|
|
|
25
40
|
!v.includes(" ") &&
|
|
26
41
|
/^[a-zA-Z0-9._:-]+$/.test(v));
|
|
27
42
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
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, "*");
|
|
37
61
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
42
89
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
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
|
+
}
|
|
48
131
|
}
|
|
132
|
+
typescript_1.default.forEachChild(node, visit);
|
|
49
133
|
}
|
|
50
|
-
|
|
134
|
+
if (fn.body)
|
|
135
|
+
visit(fn.body);
|
|
136
|
+
return result;
|
|
51
137
|
}
|
|
52
|
-
|
|
138
|
+
// MAIN SCANNER
|
|
139
|
+
function scanSource(source, checker, config, visited = new Set(), inheritedParamMap, wrapperCache) {
|
|
140
|
+
wrapperCache = wrapperCache ?? new Map();
|
|
53
141
|
const events = new Set();
|
|
54
142
|
const detectedFunctionWrappers = new Set();
|
|
55
143
|
const detectedComponentWrappers = new Map();
|
|
@@ -58,42 +146,107 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
|
|
|
58
146
|
}
|
|
59
147
|
visited.add(source.fileName);
|
|
60
148
|
function visit(node) {
|
|
61
|
-
//
|
|
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
|
|
62
233
|
if (typescript_1.default.isCallExpression(node)) {
|
|
63
234
|
const name = getCallName(node.expression);
|
|
64
|
-
const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
isTrackingByType ||
|
|
70
|
-
name.startsWith("$") ||
|
|
235
|
+
const isKnownWrapper = config.functionWrappers.some((w) => w.name === name);
|
|
236
|
+
const resolvedFn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
|
|
237
|
+
const isAutoWrapper = !!resolvedFn && (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn) !== null;
|
|
238
|
+
const isTracking = name === "track" ||
|
|
239
|
+
isKnownWrapper ||
|
|
71
240
|
(isAutoWrapper &&
|
|
72
241
|
resolvedFn &&
|
|
73
|
-
resolvedFn.getSourceFile &&
|
|
74
242
|
!(0, boundary_1.isExternalFile)(resolvedFn.getSourceFile().fileName));
|
|
75
|
-
if (!isTracking) {
|
|
76
|
-
if (typescript_1.default.isPropertyAccessExpression(node.expression) &&
|
|
77
|
-
node.expression.name.text === "track") {
|
|
78
|
-
isTracking = true;
|
|
79
|
-
}
|
|
80
|
-
else if (typescript_1.default.isIdentifier(node.expression) &&
|
|
81
|
-
node.expression.text === "track") {
|
|
82
|
-
isTracking = true;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
243
|
if (!isTracking) {
|
|
86
244
|
typescript_1.default.forEachChild(node, visit);
|
|
87
245
|
return;
|
|
88
246
|
}
|
|
89
|
-
|
|
90
|
-
if (!isKnownWrapper &&
|
|
91
|
-
isAutoWrapper &&
|
|
92
|
-
!isTrackingByType &&
|
|
93
|
-
!name.startsWith("track")) {
|
|
247
|
+
if (!isKnownWrapper && isAutoWrapper && name !== "track") {
|
|
94
248
|
detectedFunctionWrappers.add(name);
|
|
95
249
|
}
|
|
96
|
-
// PARAM MAP
|
|
97
250
|
const paramMap = new Map(inheritedParamMap ?? []);
|
|
98
251
|
if (resolvedFn && typescript_1.default.isFunctionLike(resolvedFn)) {
|
|
99
252
|
resolvedFn.parameters.forEach((param, i) => {
|
|
@@ -105,23 +258,21 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0, inh
|
|
|
105
258
|
}
|
|
106
259
|
});
|
|
107
260
|
}
|
|
108
|
-
// STRICT EVENT RESOLVE
|
|
109
261
|
if (resolvedFn) {
|
|
110
262
|
const idx = (0, functionAnalyzer_1.findTrackParamIndex)(resolvedFn);
|
|
111
263
|
if (idx !== null && node.arguments[idx]) {
|
|
112
264
|
const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
|
|
113
|
-
res.values.forEach(v => {
|
|
265
|
+
res.values.forEach((v) => {
|
|
114
266
|
if (isValidEvent(v))
|
|
115
267
|
events.add(v);
|
|
116
268
|
});
|
|
117
269
|
}
|
|
118
270
|
}
|
|
119
|
-
// CROSS-FILE
|
|
120
271
|
if (resolvedFn) {
|
|
121
|
-
const sf = resolvedFn.getSourceFile
|
|
122
|
-
if (
|
|
123
|
-
const res = scanSource(sf, checker, config, visited,
|
|
124
|
-
res.events.forEach(e => events.add(e));
|
|
272
|
+
const sf = resolvedFn.getSourceFile();
|
|
273
|
+
if (!visited.has(sf.fileName)) {
|
|
274
|
+
const res = scanSource(sf, checker, config, visited, paramMap, wrapperCache);
|
|
275
|
+
res.events.forEach((e) => events.add(e));
|
|
125
276
|
}
|
|
126
277
|
}
|
|
127
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,135 +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
|
-
//
|
|
16
|
-
function
|
|
14
|
+
// TRACK FILTER
|
|
15
|
+
function looksLikeTracking(code) {
|
|
16
|
+
return /\btrack\w*\s*\(/.test(code);
|
|
17
|
+
}
|
|
18
|
+
// SAFE {} PARSER
|
|
19
|
+
function extractBracedExpressions(input) {
|
|
17
20
|
const result = [];
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
escape = false;
|
|
27
|
-
else if (ch === "\\")
|
|
28
|
-
escape = true;
|
|
29
|
-
else if (ch === inString)
|
|
30
|
-
inString = null;
|
|
31
|
-
i++;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
35
|
-
inString = ch;
|
|
36
|
-
i++;
|
|
37
|
-
continue;
|
|
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++;
|
|
38
29
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
depth++;
|
|
45
|
-
else if (input[j] === "}")
|
|
46
|
-
depth--;
|
|
47
|
-
j++;
|
|
48
|
-
}
|
|
49
|
-
if (depth === 0) {
|
|
50
|
-
const expr = input.slice(i + 1, j - 1).trim();
|
|
51
|
-
if (expr && /[a-zA-Z0-9_$]\s*\(/.test(expr)) {
|
|
30
|
+
else if (char === "}") {
|
|
31
|
+
depth--;
|
|
32
|
+
if (depth === 0 && start !== -1) {
|
|
33
|
+
const expr = input.slice(start, i);
|
|
34
|
+
if (looksLikeTracking(expr)) {
|
|
52
35
|
result.push(expr);
|
|
53
36
|
}
|
|
54
|
-
|
|
55
|
-
continue;
|
|
37
|
+
start = -1;
|
|
56
38
|
}
|
|
57
39
|
}
|
|
58
|
-
i++;
|
|
59
40
|
}
|
|
60
41
|
return result;
|
|
61
42
|
}
|
|
62
|
-
//
|
|
63
|
-
function
|
|
43
|
+
// HTML handlers
|
|
44
|
+
function extractHTMLHandlers(input) {
|
|
45
|
+
const result = [];
|
|
46
|
+
const regex = /\bon\w+\s*=\s*["']([^"']+)["']/g;
|
|
47
|
+
for (const m of input.matchAll(regex)) {
|
|
48
|
+
if (looksLikeTracking(m[1])) {
|
|
49
|
+
result.push(m[1]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
// VUE handlers
|
|
55
|
+
function extractVueHandlers(input) {
|
|
64
56
|
const result = [];
|
|
65
|
-
const
|
|
66
|
-
for (const m of
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
result.push(code);
|
|
57
|
+
const regex = /(?:@|v-on:)[\w:-]+\s*=\s*["']([^"']+)["']/g;
|
|
58
|
+
for (const m of input.matchAll(regex)) {
|
|
59
|
+
if (looksLikeTracking(m[1])) {
|
|
60
|
+
result.push(m[1]);
|
|
70
61
|
}
|
|
71
62
|
}
|
|
72
63
|
return result;
|
|
73
64
|
}
|
|
74
|
-
//
|
|
65
|
+
// SVELTE handlers
|
|
66
|
+
function extractSvelteHandlers(input) {
|
|
67
|
+
const result = [];
|
|
68
|
+
const regex = /\bon:[\w]+\s*=\s*{([^}]+)}/g;
|
|
69
|
+
for (const m of input.matchAll(regex)) {
|
|
70
|
+
if (looksLikeTracking(m[1])) {
|
|
71
|
+
result.push(m[1]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
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]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const noScripts = input.replace(scriptRegex, "");
|
|
97
|
+
return { scripts, deps, noScripts };
|
|
98
|
+
}
|
|
99
|
+
// DYNAMIC IMPORTS
|
|
75
100
|
function extractDynamicImports(input, file) {
|
|
76
101
|
const deps = [];
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
for (const m of matches) {
|
|
102
|
+
const regex = /import\(\s*["'](.+?)["']\s*\)/g;
|
|
103
|
+
for (const m of input.matchAll(regex)) {
|
|
80
104
|
const mod = m[1];
|
|
81
105
|
const resolved = mod.startsWith("/")
|
|
82
106
|
? path_1.default.resolve(process.cwd(), "." + mod)
|
|
83
107
|
: path_1.default.resolve(path_1.default.dirname(file), mod);
|
|
84
108
|
deps.push(resolved);
|
|
85
|
-
calls.push(`import("${mod}")`);
|
|
86
109
|
}
|
|
87
|
-
return
|
|
110
|
+
return deps;
|
|
88
111
|
}
|
|
89
|
-
//
|
|
90
|
-
function isProbablyTracking(code) {
|
|
91
|
-
return /track|event|analytics/i.test(code);
|
|
92
|
-
}
|
|
93
|
-
// UNIVERSAL PARSER
|
|
112
|
+
// MAIN
|
|
94
113
|
function parseUniversal(content, file) {
|
|
95
114
|
const parts = [];
|
|
96
115
|
const deps = [];
|
|
97
116
|
const clean = stripComments(content);
|
|
98
|
-
|
|
117
|
+
/* ---------- ASTRO FRONTMATTER ---------- */
|
|
99
118
|
const fm = clean.match(/^---([\s\S]*?)---/);
|
|
100
119
|
if (fm?.[1]) {
|
|
101
120
|
parts.push(fm[1]);
|
|
102
121
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const attrs = m[1];
|
|
116
|
-
if (/type=["']application\/json/.test(attrs))
|
|
117
|
-
continue;
|
|
118
|
-
if (m[2].trim()) {
|
|
119
|
-
parts.push(m[2]);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// REMOVE SCRIPTS → TEMPLATE PART
|
|
123
|
-
const noScripts = clean.replace(/<script[\s\S]*?<\/script>/g, "");
|
|
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));
|
|
130
|
-
// EXPRESSIONS
|
|
131
|
-
const extracted = extractJS(clean);
|
|
132
|
-
for (const e of extracted) {
|
|
133
|
-
if (isProbablyTracking(e)) {
|
|
134
|
-
parts.push(e + ";");
|
|
135
|
-
}
|
|
136
|
-
}
|
|
122
|
+
/* ---------- SCRIPTS ---------- */
|
|
123
|
+
const { scripts, deps: scriptDeps, noScripts } = extractScripts(clean, file);
|
|
124
|
+
parts.push(...scripts);
|
|
125
|
+
deps.push(...scriptDeps);
|
|
126
|
+
/* ---------- HANDLERS ---------- */
|
|
127
|
+
parts.push(...extractHTMLHandlers(noScripts));
|
|
128
|
+
parts.push(...extractVueHandlers(noScripts));
|
|
129
|
+
parts.push(...extractSvelteHandlers(noScripts));
|
|
130
|
+
/* ---------- JSX / TEMPLATE ---------- */
|
|
131
|
+
parts.push(...extractBracedExpressions(clean));
|
|
132
|
+
/* ---------- IMPORTS ---------- */
|
|
133
|
+
deps.push(...extractDynamicImports(clean, file));
|
|
137
134
|
return {
|
|
138
135
|
code: [...new Set(parts)].join("\n"),
|
|
139
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);
|