@eventra_dev/eventra-cli 0.1.7 → 0.1.9
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/boundary.js +14 -0
- package/dist/engine/callResolver.js +11 -8
- package/dist/engine/engine.js +45 -1
- package/dist/engine/languageService.js +10 -0
- package/dist/engine/resolver.js +5 -2
- package/dist/engine/scanner.js +19 -29
- package/package.json +1 -1
- package/src/engine/boundary.ts +12 -0
- package/src/engine/callResolver.ts +12 -9
- package/src/engine/engine.ts +67 -3
- package/src/engine/languageService.ts +13 -0
- package/src/engine/resolver.ts +15 -4
- package/src/engine/scanner.ts +29 -30
|
@@ -0,0 +1,14 @@
|
|
|
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.isExternalFile = isExternalFile;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
function isExternalFile(file) {
|
|
9
|
+
const normalized = path_1.default.resolve(file);
|
|
10
|
+
return (normalized.includes(`${path_1.default.sep}node_modules${path_1.default.sep}`) ||
|
|
11
|
+
normalized.includes(`${path_1.default.sep}dist${path_1.default.sep}`) ||
|
|
12
|
+
normalized.includes(`${path_1.default.sep}build${path_1.default.sep}`) ||
|
|
13
|
+
normalized.endsWith(".d.ts"));
|
|
14
|
+
}
|
|
@@ -12,20 +12,23 @@ function isFunctionLike(node) {
|
|
|
12
12
|
typescript_1.default.isFunctionExpression(node) ||
|
|
13
13
|
typescript_1.default.isArrowFunction(node));
|
|
14
14
|
}
|
|
15
|
-
function resolveFromSymbol(symbol, checker, visited = new Set()) {
|
|
15
|
+
function resolveFromSymbol(symbol, checker, visited = new Set(), depth = 0) {
|
|
16
16
|
if (visited.has(symbol))
|
|
17
17
|
return null;
|
|
18
|
+
if (depth > 5)
|
|
19
|
+
return null;
|
|
18
20
|
visited.add(symbol);
|
|
19
|
-
// alias
|
|
21
|
+
// unwrap alias
|
|
20
22
|
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
21
23
|
const aliased = checker.getAliasedSymbol(symbol);
|
|
22
|
-
return resolveFromSymbol(aliased, checker, visited);
|
|
24
|
+
return resolveFromSymbol(aliased, checker, visited, depth + 1);
|
|
23
25
|
}
|
|
24
26
|
const decls = symbol.getDeclarations() ?? [];
|
|
25
27
|
for (const decl of decls) {
|
|
26
28
|
if (isFunctionLike(decl)) {
|
|
27
29
|
return decl;
|
|
28
30
|
}
|
|
31
|
+
// MethodSignature
|
|
29
32
|
if (typescript_1.default.isMethodSignature(decl)) {
|
|
30
33
|
const parent = decl.parent;
|
|
31
34
|
if (typescript_1.default.isInterfaceDeclaration(parent) || typescript_1.default.isTypeLiteralNode(parent)) {
|
|
@@ -35,16 +38,16 @@ function resolveFromSymbol(symbol, checker, visited = new Set()) {
|
|
|
35
38
|
for (const prop of props) {
|
|
36
39
|
if (prop.name !== name)
|
|
37
40
|
continue;
|
|
38
|
-
const resolved = resolveFromSymbol(prop, checker, visited);
|
|
41
|
+
const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
|
|
39
42
|
if (resolved)
|
|
40
43
|
return resolved;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
|
-
// fallback
|
|
47
|
+
// SAFE fallback
|
|
45
48
|
const s = checker.getSymbolAtLocation(decl);
|
|
46
|
-
if (s) {
|
|
47
|
-
const resolved = resolveFromSymbol(s, checker, visited);
|
|
49
|
+
if (s && s !== symbol) {
|
|
50
|
+
const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
|
|
48
51
|
if (resolved)
|
|
49
52
|
return resolved;
|
|
50
53
|
}
|
|
@@ -61,7 +64,7 @@ function resolveFunctionFromCall(expr, checker) {
|
|
|
61
64
|
if (isFunctionLike(decl)) {
|
|
62
65
|
return decl;
|
|
63
66
|
}
|
|
64
|
-
//
|
|
67
|
+
// interface method
|
|
65
68
|
if (typescript_1.default.isMethodSignature(decl)) {
|
|
66
69
|
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
67
70
|
if (symbol) {
|
package/dist/engine/engine.js
CHANGED
|
@@ -5,6 +5,7 @@ const languageService_1 = require("./languageService");
|
|
|
5
5
|
const scanner_1 = require("./scanner");
|
|
6
6
|
const graph_1 = require("./graph");
|
|
7
7
|
const hash_1 = require("./hash");
|
|
8
|
+
const boundary_1 = require("./boundary");
|
|
8
9
|
class EventraEngine {
|
|
9
10
|
ts;
|
|
10
11
|
graph;
|
|
@@ -15,8 +16,10 @@ class EventraEngine {
|
|
|
15
16
|
const { baseUrl, paths } = this.ts.getModuleResolutionConfig();
|
|
16
17
|
this.graph = new graph_1.DependencyGraph(baseUrl, paths);
|
|
17
18
|
}
|
|
19
|
+
// SCAN FILE
|
|
18
20
|
scanFile(file, content, config) {
|
|
19
21
|
const h = (0, hash_1.hash)(content);
|
|
22
|
+
// skip unchanged
|
|
20
23
|
if (this.fileHash.get(file) === h) {
|
|
21
24
|
return (this.fileResults.get(file) ?? {
|
|
22
25
|
events: new Set(),
|
|
@@ -35,24 +38,39 @@ class EventraEngine {
|
|
|
35
38
|
detectedComponentWrappers: new Map(),
|
|
36
39
|
};
|
|
37
40
|
}
|
|
41
|
+
// update dependency graph
|
|
38
42
|
this.graph.update(file, source);
|
|
39
43
|
const res = (0, scanner_1.scanSource)(source, checker, config);
|
|
40
44
|
this.fileResults.set(file, res);
|
|
41
45
|
return res;
|
|
42
46
|
}
|
|
47
|
+
// UPDATE FILE (incremental)
|
|
43
48
|
updateFile(file, content, config) {
|
|
44
49
|
const res = this.scanFile(file, content, config);
|
|
45
50
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
46
51
|
for (const dep of dependents) {
|
|
52
|
+
// skip external files
|
|
53
|
+
if ((0, boundary_1.isExternalFile)(dep))
|
|
54
|
+
continue;
|
|
47
55
|
const source = this.ts.getSourceFile(dep);
|
|
48
56
|
const checker = this.ts.getChecker();
|
|
49
57
|
if (!source || !checker)
|
|
50
58
|
continue;
|
|
59
|
+
const depContent = this.ts.getFileContent(dep);
|
|
60
|
+
if (!depContent)
|
|
61
|
+
continue;
|
|
62
|
+
const h = (0, hash_1.hash)(depContent);
|
|
63
|
+
// skip unchanged dependents
|
|
64
|
+
if (this.fileHash.get(dep) === h)
|
|
65
|
+
continue;
|
|
66
|
+
this.fileHash.set(dep, h);
|
|
67
|
+
this.graph.update(dep, source);
|
|
51
68
|
const r = (0, scanner_1.scanSource)(source, checker, config);
|
|
52
69
|
this.fileResults.set(dep, r);
|
|
53
70
|
}
|
|
54
71
|
return res;
|
|
55
72
|
}
|
|
73
|
+
// GET ALL EVENTS
|
|
56
74
|
getAllEvents() {
|
|
57
75
|
const all = new Set();
|
|
58
76
|
for (const r of this.fileResults.values()) {
|
|
@@ -60,10 +78,36 @@ class EventraEngine {
|
|
|
60
78
|
}
|
|
61
79
|
return [...all];
|
|
62
80
|
}
|
|
63
|
-
|
|
81
|
+
// REMOVE FILE
|
|
82
|
+
removeFile(file, config) {
|
|
83
|
+
this.ts.removeFile(file);
|
|
64
84
|
this.fileResults.delete(file);
|
|
65
85
|
this.fileHash.delete(file);
|
|
66
86
|
this.graph.remove(file);
|
|
87
|
+
const dependents = this.graph.getAllDependentsDeep(file);
|
|
88
|
+
for (const dep of dependents) {
|
|
89
|
+
if ((0, boundary_1.isExternalFile)(dep))
|
|
90
|
+
continue;
|
|
91
|
+
const source = this.ts.getSourceFile(dep);
|
|
92
|
+
const checker = this.ts.getChecker();
|
|
93
|
+
if (!source || !checker)
|
|
94
|
+
continue;
|
|
95
|
+
const content = this.ts.getFileContent(dep);
|
|
96
|
+
if (!content)
|
|
97
|
+
continue;
|
|
98
|
+
const h = (0, hash_1.hash)(content);
|
|
99
|
+
// skip unchanged
|
|
100
|
+
if (this.fileHash.get(dep) === h)
|
|
101
|
+
continue;
|
|
102
|
+
this.fileHash.set(dep, h);
|
|
103
|
+
const r = (0, scanner_1.scanSource)(source, checker, config ?? {
|
|
104
|
+
events: [],
|
|
105
|
+
wrappers: [],
|
|
106
|
+
functionWrappers: [],
|
|
107
|
+
sync: { include: [], exclude: [] },
|
|
108
|
+
});
|
|
109
|
+
this.fileResults.set(dep, r);
|
|
110
|
+
}
|
|
67
111
|
}
|
|
68
112
|
}
|
|
69
113
|
exports.EventraEngine = EventraEngine;
|
|
@@ -48,6 +48,9 @@ class TSService {
|
|
|
48
48
|
},
|
|
49
49
|
getScriptVersion: (file) => this.files.get(file)?.version.toString() ?? "0",
|
|
50
50
|
getScriptSnapshot: (file) => {
|
|
51
|
+
if (!this.fileNames.has(file)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
51
54
|
const f = this.files.get(file);
|
|
52
55
|
if (f) {
|
|
53
56
|
return typescript_1.default.ScriptSnapshot.fromString(f.content);
|
|
@@ -83,6 +86,13 @@ class TSService {
|
|
|
83
86
|
getSourceFile(file) {
|
|
84
87
|
return this.getProgram()?.getSourceFile(file);
|
|
85
88
|
}
|
|
89
|
+
getFileContent(file) {
|
|
90
|
+
return this.files.get(file)?.content;
|
|
91
|
+
}
|
|
92
|
+
removeFile(file) {
|
|
93
|
+
this.files.delete(file);
|
|
94
|
+
this.fileNames.delete(file);
|
|
95
|
+
}
|
|
86
96
|
getModuleResolutionConfig() {
|
|
87
97
|
return {
|
|
88
98
|
baseUrl: this.baseUrl,
|
package/dist/engine/resolver.js
CHANGED
|
@@ -12,7 +12,7 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
|
|
|
12
12
|
seen.add(node);
|
|
13
13
|
// param mapping
|
|
14
14
|
if (typescript_1.default.isIdentifier(node) && paramMap?.has(node.text)) {
|
|
15
|
-
return resolveNodeValue(paramMap.get(node.text), checker, paramMap);
|
|
15
|
+
return resolveNodeValue(paramMap.get(node.text), checker, paramMap, seen);
|
|
16
16
|
}
|
|
17
17
|
if (typescript_1.default.isStringLiteral(node)) {
|
|
18
18
|
return { values: [node.text], dynamic: false };
|
|
@@ -37,13 +37,16 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
|
|
|
37
37
|
let symbol = checker.getSymbolAtLocation(node);
|
|
38
38
|
if (!symbol)
|
|
39
39
|
return null;
|
|
40
|
+
// variable inline resolve FIRST
|
|
40
41
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
41
42
|
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
42
43
|
return resolveNodeValue(decl.initializer, checker, paramMap, seen);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
const original = symbol;
|
|
45
47
|
symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
46
|
-
|
|
48
|
+
// avoid alias loop
|
|
49
|
+
if (symbol !== original && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
|
|
47
50
|
symbol = checker.getAliasedSymbol(symbol);
|
|
48
51
|
}
|
|
49
52
|
for (const decl of symbol.getDeclarations() ?? []) {
|
package/dist/engine/scanner.js
CHANGED
|
@@ -9,6 +9,7 @@ const resolver_1 = require("./resolver");
|
|
|
9
9
|
const callResolver_1 = require("./callResolver");
|
|
10
10
|
const exportResolver_1 = require("./exportResolver");
|
|
11
11
|
const functionAnalyzer_1 = require("./functionAnalyzer");
|
|
12
|
+
const boundary_1 = require("./boundary");
|
|
12
13
|
const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
|
|
13
14
|
function getCallName(expr) {
|
|
14
15
|
if (typescript_1.default.isIdentifier(expr))
|
|
@@ -51,7 +52,7 @@ function isPropUsedInTracking(fn, prop) {
|
|
|
51
52
|
visit(fn);
|
|
52
53
|
return found;
|
|
53
54
|
}
|
|
54
|
-
function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
55
|
+
function scanSource(source, checker, config, visited = new Set(), depth = 0, inheritedParamMap) {
|
|
55
56
|
const events = new Set();
|
|
56
57
|
const detectedFunctionWrappers = new Set();
|
|
57
58
|
const detectedComponentWrappers = new Map();
|
|
@@ -68,16 +69,16 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
68
69
|
const isAutoWrapper = fn && (0, functionAnalyzer_1.findTrackParamIndex)(fn) !== null;
|
|
69
70
|
const isTracking = isKnownWrapper ||
|
|
70
71
|
TRACKING_NAMES.has(name) ||
|
|
71
|
-
isAutoWrapper
|
|
72
|
+
(isAutoWrapper &&
|
|
73
|
+
fn &&
|
|
74
|
+
!(0, boundary_1.isExternalFile)(fn.getSourceFile().fileName));
|
|
72
75
|
if (!isTracking)
|
|
73
76
|
return;
|
|
74
|
-
|
|
75
|
-
if (!isKnownWrapper &&
|
|
76
|
-
fn &&
|
|
77
|
-
!TRACKING_NAMES.has(name)) {
|
|
77
|
+
if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
|
|
78
78
|
detectedFunctionWrappers.add(name);
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
// paramMap
|
|
81
|
+
const paramMap = new Map(inheritedParamMap ?? []);
|
|
81
82
|
if (fn && typescript_1.default.isFunctionLike(fn)) {
|
|
82
83
|
fn.parameters.forEach((param, i) => {
|
|
83
84
|
const arg = node.arguments[i];
|
|
@@ -110,11 +111,11 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
|
-
// cross-file
|
|
114
|
+
// cross-file WITH paramMap
|
|
114
115
|
if (fn) {
|
|
115
116
|
const sf = fn.getSourceFile();
|
|
116
117
|
if (!visited.has(sf.fileName)) {
|
|
117
|
-
const res = scanSource(sf, checker, config, visited, depth + 1);
|
|
118
|
+
const res = scanSource(sf, checker, config, visited, depth + 1, paramMap);
|
|
118
119
|
res.events.forEach(e => events.add(e));
|
|
119
120
|
}
|
|
120
121
|
}
|
|
@@ -127,28 +128,20 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
127
128
|
: node.tagName;
|
|
128
129
|
let symbol = checker.getSymbolAtLocation(tagNode);
|
|
129
130
|
if (symbol) {
|
|
130
|
-
|
|
131
|
-
if (resolved)
|
|
132
|
-
symbol = resolved;
|
|
131
|
+
symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
133
132
|
}
|
|
134
133
|
if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
|
|
135
|
-
|
|
136
|
-
if (aliased)
|
|
137
|
-
symbol = aliased;
|
|
134
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
138
135
|
}
|
|
139
136
|
if (!symbol)
|
|
140
137
|
return;
|
|
141
138
|
const tagName = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
|
142
139
|
? node.tagName.name.getText()
|
|
143
140
|
: node.tagName.getText();
|
|
144
|
-
const
|
|
145
|
-
if (!declarations)
|
|
146
|
-
return;
|
|
147
|
-
for (const decl of declarations) {
|
|
141
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
148
142
|
let fn = null;
|
|
149
|
-
if (typescript_1.default.isFunctionDeclaration(decl))
|
|
143
|
+
if (typescript_1.default.isFunctionDeclaration(decl))
|
|
150
144
|
fn = decl;
|
|
151
|
-
}
|
|
152
145
|
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
153
146
|
const init = decl.initializer;
|
|
154
147
|
if (typescript_1.default.isArrowFunction(init) ||
|
|
@@ -159,9 +152,7 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
159
152
|
if (!fn)
|
|
160
153
|
continue;
|
|
161
154
|
const param = fn.parameters[0];
|
|
162
|
-
if (!param)
|
|
163
|
-
continue;
|
|
164
|
-
if (!typescript_1.default.isObjectBindingPattern(param.name))
|
|
155
|
+
if (!param || !typescript_1.default.isObjectBindingPattern(param.name))
|
|
165
156
|
continue;
|
|
166
157
|
for (const el of param.name.elements) {
|
|
167
158
|
const prop = el.name.getText();
|
|
@@ -181,11 +172,10 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
181
172
|
: attr.initializer;
|
|
182
173
|
if (!expr)
|
|
183
174
|
continue;
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
175
|
+
const paramMap = new Map();
|
|
176
|
+
paramMap.set(prop, expr);
|
|
177
|
+
const res = scanSource(fn.getSourceFile(), checker, config, visited, depth + 1, paramMap);
|
|
178
|
+
res.events.forEach(e => events.add(e));
|
|
189
179
|
detectedComponentWrappers.set(tagName, prop);
|
|
190
180
|
}
|
|
191
181
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
export function isExternalFile(file: string) {
|
|
4
|
+
const normalized = path.resolve(file);
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
normalized.includes(`${path.sep}node_modules${path.sep}`) ||
|
|
8
|
+
normalized.includes(`${path.sep}dist${path.sep}`) ||
|
|
9
|
+
normalized.includes(`${path.sep}build${path.sep}`) ||
|
|
10
|
+
normalized.endsWith(".d.ts")
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -15,15 +15,18 @@ function isFunctionLike(
|
|
|
15
15
|
function resolveFromSymbol(
|
|
16
16
|
symbol: ts.Symbol,
|
|
17
17
|
checker: ts.TypeChecker,
|
|
18
|
-
visited = new Set<ts.Symbol>()
|
|
18
|
+
visited = new Set<ts.Symbol>(),
|
|
19
|
+
depth = 0
|
|
19
20
|
): ts.FunctionLikeDeclaration | null {
|
|
20
21
|
if (visited.has(symbol)) return null;
|
|
22
|
+
if (depth > 5) return null;
|
|
23
|
+
|
|
21
24
|
visited.add(symbol);
|
|
22
25
|
|
|
23
|
-
// alias
|
|
26
|
+
// unwrap alias
|
|
24
27
|
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
25
28
|
const aliased = checker.getAliasedSymbol(symbol);
|
|
26
|
-
return resolveFromSymbol(aliased, checker, visited);
|
|
29
|
+
return resolveFromSymbol(aliased, checker, visited, depth + 1);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const decls = symbol.getDeclarations() ?? [];
|
|
@@ -33,6 +36,7 @@ function resolveFromSymbol(
|
|
|
33
36
|
return decl;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
// MethodSignature
|
|
36
40
|
if (ts.isMethodSignature(decl)) {
|
|
37
41
|
const parent = decl.parent;
|
|
38
42
|
|
|
@@ -45,16 +49,16 @@ function resolveFromSymbol(
|
|
|
45
49
|
for (const prop of props) {
|
|
46
50
|
if (prop.name !== name) continue;
|
|
47
51
|
|
|
48
|
-
const resolved = resolveFromSymbol(prop, checker, visited);
|
|
52
|
+
const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
|
|
49
53
|
if (resolved) return resolved;
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
// fallback
|
|
58
|
+
// SAFE fallback
|
|
55
59
|
const s = checker.getSymbolAtLocation(decl);
|
|
56
|
-
if (s) {
|
|
57
|
-
const resolved = resolveFromSymbol(s, checker, visited);
|
|
60
|
+
if (s && s !== symbol) {
|
|
61
|
+
const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
|
|
58
62
|
if (resolved) return resolved;
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -71,14 +75,13 @@ export function resolveFunctionFromCall(
|
|
|
71
75
|
|
|
72
76
|
for (const sig of signatures) {
|
|
73
77
|
const decl = sig.getDeclaration();
|
|
74
|
-
|
|
75
78
|
if (!decl) continue;
|
|
76
79
|
|
|
77
80
|
if (isFunctionLike(decl)) {
|
|
78
81
|
return decl;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
//
|
|
84
|
+
// interface method
|
|
82
85
|
if (ts.isMethodSignature(decl)) {
|
|
83
86
|
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
84
87
|
|
package/src/engine/engine.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ScanResult, scanSource } from "./scanner";
|
|
|
3
3
|
import { DependencyGraph } from "./graph";
|
|
4
4
|
import { hash } from "./hash";
|
|
5
5
|
import { EventraConfig } from "../types";
|
|
6
|
+
import { isExternalFile } from "./boundary";
|
|
6
7
|
|
|
7
8
|
export class EventraEngine {
|
|
8
9
|
private ts: TSService;
|
|
@@ -18,9 +19,15 @@ export class EventraEngine {
|
|
|
18
19
|
this.graph = new DependencyGraph(baseUrl, paths);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// SCAN FILE
|
|
23
|
+
scanFile(
|
|
24
|
+
file: string,
|
|
25
|
+
content: string,
|
|
26
|
+
config: EventraConfig
|
|
27
|
+
): ScanResult {
|
|
22
28
|
const h = hash(content);
|
|
23
29
|
|
|
30
|
+
// skip unchanged
|
|
24
31
|
if (this.fileHash.get(file) === h) {
|
|
25
32
|
return (
|
|
26
33
|
this.fileResults.get(file) ?? {
|
|
@@ -45,6 +52,7 @@ export class EventraEngine {
|
|
|
45
52
|
};
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
// update dependency graph
|
|
48
56
|
this.graph.update(file, source);
|
|
49
57
|
|
|
50
58
|
const res = scanSource(source, checker, config);
|
|
@@ -54,17 +62,37 @@ export class EventraEngine {
|
|
|
54
62
|
return res;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
// UPDATE FILE (incremental)
|
|
66
|
+
updateFile(
|
|
67
|
+
file: string,
|
|
68
|
+
content: string,
|
|
69
|
+
config: EventraConfig
|
|
70
|
+
) {
|
|
58
71
|
const res = this.scanFile(file, content, config);
|
|
59
72
|
|
|
60
73
|
const dependents = this.graph.getAllDependentsDeep(file);
|
|
61
74
|
|
|
62
75
|
for (const dep of dependents) {
|
|
76
|
+
// skip external files
|
|
77
|
+
if (isExternalFile(dep)) continue;
|
|
78
|
+
|
|
63
79
|
const source = this.ts.getSourceFile(dep);
|
|
64
80
|
const checker = this.ts.getChecker();
|
|
65
81
|
|
|
66
82
|
if (!source || !checker) continue;
|
|
67
83
|
|
|
84
|
+
const depContent = this.ts.getFileContent(dep);
|
|
85
|
+
if (!depContent) continue;
|
|
86
|
+
|
|
87
|
+
const h = hash(depContent);
|
|
88
|
+
|
|
89
|
+
// skip unchanged dependents
|
|
90
|
+
if (this.fileHash.get(dep) === h) continue;
|
|
91
|
+
|
|
92
|
+
this.fileHash.set(dep, h);
|
|
93
|
+
|
|
94
|
+
this.graph.update(dep, source);
|
|
95
|
+
|
|
68
96
|
const r = scanSource(source, checker, config);
|
|
69
97
|
this.fileResults.set(dep, r);
|
|
70
98
|
}
|
|
@@ -72,6 +100,7 @@ export class EventraEngine {
|
|
|
72
100
|
return res;
|
|
73
101
|
}
|
|
74
102
|
|
|
103
|
+
// GET ALL EVENTS
|
|
75
104
|
getAllEvents() {
|
|
76
105
|
const all = new Set<string>();
|
|
77
106
|
|
|
@@ -82,9 +111,44 @@ export class EventraEngine {
|
|
|
82
111
|
return [...all];
|
|
83
112
|
}
|
|
84
113
|
|
|
85
|
-
|
|
114
|
+
// REMOVE FILE
|
|
115
|
+
removeFile(file: string, config?: EventraConfig) {
|
|
116
|
+
this.ts.removeFile(file);
|
|
86
117
|
this.fileResults.delete(file);
|
|
87
118
|
this.fileHash.delete(file);
|
|
88
119
|
this.graph.remove(file);
|
|
120
|
+
const dependents = this.graph.getAllDependentsDeep(file);
|
|
121
|
+
|
|
122
|
+
for (const dep of dependents) {
|
|
123
|
+
if (isExternalFile(dep)) continue;
|
|
124
|
+
|
|
125
|
+
const source = this.ts.getSourceFile(dep);
|
|
126
|
+
const checker = this.ts.getChecker();
|
|
127
|
+
|
|
128
|
+
if (!source || !checker) continue;
|
|
129
|
+
|
|
130
|
+
const content = this.ts.getFileContent(dep);
|
|
131
|
+
if (!content) continue;
|
|
132
|
+
|
|
133
|
+
const h = hash(content);
|
|
134
|
+
|
|
135
|
+
// skip unchanged
|
|
136
|
+
if (this.fileHash.get(dep) === h) continue;
|
|
137
|
+
|
|
138
|
+
this.fileHash.set(dep, h);
|
|
139
|
+
|
|
140
|
+
const r = scanSource(
|
|
141
|
+
source,
|
|
142
|
+
checker,
|
|
143
|
+
config ?? {
|
|
144
|
+
events: [],
|
|
145
|
+
wrappers: [],
|
|
146
|
+
functionWrappers: [],
|
|
147
|
+
sync: { include: [], exclude: [] },
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
this.fileResults.set(dep, r);
|
|
152
|
+
}
|
|
89
153
|
}
|
|
90
154
|
}
|
|
@@ -62,6 +62,10 @@ export class TSService {
|
|
|
62
62
|
this.files.get(file)?.version.toString() ?? "0",
|
|
63
63
|
|
|
64
64
|
getScriptSnapshot: (file) => {
|
|
65
|
+
if (!this.fileNames.has(file)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
const f = this.files.get(file);
|
|
66
70
|
|
|
67
71
|
if (f) {
|
|
@@ -113,6 +117,15 @@ export class TSService {
|
|
|
113
117
|
return this.getProgram()?.getSourceFile(file);
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
getFileContent(file: string) {
|
|
121
|
+
return this.files.get(file)?.content;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
removeFile(file: string) {
|
|
125
|
+
this.files.delete(file);
|
|
126
|
+
this.fileNames.delete(file);
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
getModuleResolutionConfig() {
|
|
117
130
|
return {
|
|
118
131
|
baseUrl: this.baseUrl,
|
package/src/engine/resolver.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import {resolveExportedSymbol} from "./exportResolver";
|
|
2
|
+
import { resolveExportedSymbol } from "./exportResolver";
|
|
3
3
|
|
|
4
4
|
export type ResolveResult = {
|
|
5
5
|
values: string[];
|
|
@@ -21,7 +21,8 @@ export function resolveNodeValue(
|
|
|
21
21
|
return resolveNodeValue(
|
|
22
22
|
paramMap.get(node.text)!,
|
|
23
23
|
checker,
|
|
24
|
-
paramMap
|
|
24
|
+
paramMap,
|
|
25
|
+
seen
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -53,6 +54,8 @@ export function resolveNodeValue(
|
|
|
53
54
|
if (ts.isIdentifier(node)) {
|
|
54
55
|
let symbol = checker.getSymbolAtLocation(node);
|
|
55
56
|
if (!symbol) return null;
|
|
57
|
+
|
|
58
|
+
// variable inline resolve FIRST
|
|
56
59
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
57
60
|
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
58
61
|
return resolveNodeValue(
|
|
@@ -64,15 +67,23 @@ export function resolveNodeValue(
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
const original = symbol;
|
|
71
|
+
|
|
67
72
|
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
// avoid alias loop
|
|
75
|
+
if (symbol !== original && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
70
76
|
symbol = checker.getAliasedSymbol(symbol);
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
74
80
|
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
75
|
-
return resolveNodeValue(
|
|
81
|
+
return resolveNodeValue(
|
|
82
|
+
decl.initializer,
|
|
83
|
+
checker,
|
|
84
|
+
paramMap,
|
|
85
|
+
seen
|
|
86
|
+
);
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
if (ts.isEnumMember(decl) && decl.initializer) {
|
package/src/engine/scanner.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveFunctionFromCall } from "./callResolver";
|
|
|
4
4
|
import { resolveExportedSymbol } from "./exportResolver";
|
|
5
5
|
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
6
6
|
import { EventraConfig } from "../types";
|
|
7
|
+
import { isExternalFile } from "./boundary";
|
|
7
8
|
|
|
8
9
|
const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
|
|
9
10
|
|
|
@@ -70,7 +71,8 @@ export function scanSource(
|
|
|
70
71
|
checker: ts.TypeChecker,
|
|
71
72
|
config: EventraConfig,
|
|
72
73
|
visited = new Set<string>(),
|
|
73
|
-
depth = 0
|
|
74
|
+
depth = 0,
|
|
75
|
+
inheritedParamMap?: Map<string, ts.Expression>
|
|
74
76
|
): ScanResult {
|
|
75
77
|
const events = new Set<string>();
|
|
76
78
|
const detectedFunctionWrappers = new Set<string>();
|
|
@@ -99,20 +101,18 @@ export function scanSource(
|
|
|
99
101
|
const isTracking =
|
|
100
102
|
isKnownWrapper ||
|
|
101
103
|
TRACKING_NAMES.has(name) ||
|
|
102
|
-
isAutoWrapper
|
|
104
|
+
(isAutoWrapper &&
|
|
105
|
+
fn &&
|
|
106
|
+
!isExternalFile(fn.getSourceFile().fileName));
|
|
103
107
|
|
|
104
108
|
if (!isTracking) return;
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
!isKnownWrapper &&
|
|
109
|
-
fn &&
|
|
110
|
-
!TRACKING_NAMES.has(name)
|
|
111
|
-
) {
|
|
110
|
+
if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
|
|
112
111
|
detectedFunctionWrappers.add(name);
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
|
|
114
|
+
// paramMap
|
|
115
|
+
const paramMap = new Map(inheritedParamMap ?? []);
|
|
116
116
|
|
|
117
117
|
if (fn && ts.isFunctionLike(fn)) {
|
|
118
118
|
fn.parameters.forEach((param, i) => {
|
|
@@ -156,7 +156,7 @@ export function scanSource(
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
// cross-file
|
|
159
|
+
// cross-file WITH paramMap
|
|
160
160
|
if (fn) {
|
|
161
161
|
const sf = fn.getSourceFile();
|
|
162
162
|
|
|
@@ -166,7 +166,8 @@ export function scanSource(
|
|
|
166
166
|
checker,
|
|
167
167
|
config,
|
|
168
168
|
visited,
|
|
169
|
-
depth + 1
|
|
169
|
+
depth + 1,
|
|
170
|
+
paramMap
|
|
170
171
|
);
|
|
171
172
|
|
|
172
173
|
res.events.forEach(e => events.add(e));
|
|
@@ -187,13 +188,11 @@ export function scanSource(
|
|
|
187
188
|
let symbol = checker.getSymbolAtLocation(tagNode);
|
|
188
189
|
|
|
189
190
|
if (symbol) {
|
|
190
|
-
|
|
191
|
-
if (resolved) symbol = resolved;
|
|
191
|
+
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
195
|
-
|
|
196
|
-
if (aliased) symbol = aliased;
|
|
195
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
197
196
|
}
|
|
198
197
|
|
|
199
198
|
if (!symbol) return;
|
|
@@ -203,19 +202,13 @@ export function scanSource(
|
|
|
203
202
|
? node.tagName.name.getText()
|
|
204
203
|
: node.tagName.getText();
|
|
205
204
|
|
|
206
|
-
const
|
|
207
|
-
if (!declarations) return;
|
|
208
|
-
|
|
209
|
-
for (const decl of declarations) {
|
|
205
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
210
206
|
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
211
207
|
|
|
212
|
-
if (ts.isFunctionDeclaration(decl))
|
|
213
|
-
fn = decl;
|
|
214
|
-
}
|
|
208
|
+
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
215
209
|
|
|
216
210
|
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
217
211
|
const init = decl.initializer;
|
|
218
|
-
|
|
219
212
|
if (
|
|
220
213
|
ts.isArrowFunction(init) ||
|
|
221
214
|
ts.isFunctionExpression(init)
|
|
@@ -227,9 +220,7 @@ export function scanSource(
|
|
|
227
220
|
if (!fn) continue;
|
|
228
221
|
|
|
229
222
|
const param = fn.parameters[0];
|
|
230
|
-
if (!param) continue;
|
|
231
|
-
|
|
232
|
-
if (!ts.isObjectBindingPattern(param.name)) continue;
|
|
223
|
+
if (!param || !ts.isObjectBindingPattern(param.name)) continue;
|
|
233
224
|
|
|
234
225
|
for (const el of param.name.elements) {
|
|
235
226
|
const prop = el.name.getText();
|
|
@@ -255,11 +246,19 @@ export function scanSource(
|
|
|
255
246
|
|
|
256
247
|
if (!expr) continue;
|
|
257
248
|
|
|
258
|
-
const
|
|
249
|
+
const paramMap = new Map<string, ts.Expression>();
|
|
250
|
+
paramMap.set(prop, expr);
|
|
259
251
|
|
|
260
|
-
res
|
|
261
|
-
|
|
262
|
-
|
|
252
|
+
const res = scanSource(
|
|
253
|
+
fn.getSourceFile(),
|
|
254
|
+
checker,
|
|
255
|
+
config,
|
|
256
|
+
visited,
|
|
257
|
+
depth + 1,
|
|
258
|
+
paramMap
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
res.events.forEach(e => events.add(e));
|
|
263
262
|
|
|
264
263
|
detectedComponentWrappers.set(tagName, prop);
|
|
265
264
|
}
|