@eventra_dev/eventra-cli 0.1.7 → 0.1.8
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 +11 -27
- 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 +11 -31
|
@@ -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))
|
|
@@ -40,8 +41,7 @@ function isPropUsedInTracking(fn, prop) {
|
|
|
40
41
|
return;
|
|
41
42
|
for (const arg of node.arguments) {
|
|
42
43
|
if ((typescript_1.default.isIdentifier(arg) && arg.text === prop) ||
|
|
43
|
-
(typescript_1.default.isPropertyAccessExpression(arg) &&
|
|
44
|
-
arg.name.text === prop)) {
|
|
44
|
+
(typescript_1.default.isPropertyAccessExpression(arg) && arg.name.text === prop)) {
|
|
45
45
|
found = true;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -68,13 +68,10 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
68
68
|
const isAutoWrapper = fn && (0, functionAnalyzer_1.findTrackParamIndex)(fn) !== null;
|
|
69
69
|
const isTracking = isKnownWrapper ||
|
|
70
70
|
TRACKING_NAMES.has(name) ||
|
|
71
|
-
isAutoWrapper;
|
|
71
|
+
(isAutoWrapper && fn && !(0, boundary_1.isExternalFile)(fn.getSourceFile().fileName));
|
|
72
72
|
if (!isTracking)
|
|
73
73
|
return;
|
|
74
|
-
|
|
75
|
-
if (!isKnownWrapper &&
|
|
76
|
-
fn &&
|
|
77
|
-
!TRACKING_NAMES.has(name)) {
|
|
74
|
+
if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
|
|
78
75
|
detectedFunctionWrappers.add(name);
|
|
79
76
|
}
|
|
80
77
|
const paramMap = new Map();
|
|
@@ -100,7 +97,6 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
100
97
|
handled = true;
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
|
-
// fallback
|
|
104
100
|
if (!handled) {
|
|
105
101
|
for (const arg of node.arguments) {
|
|
106
102
|
const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
|
|
@@ -110,7 +106,6 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
110
106
|
});
|
|
111
107
|
}
|
|
112
108
|
}
|
|
113
|
-
// cross-file traversal
|
|
114
109
|
if (fn) {
|
|
115
110
|
const sf = fn.getSourceFile();
|
|
116
111
|
if (!visited.has(sf.fileName)) {
|
|
@@ -119,7 +114,7 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
119
114
|
}
|
|
120
115
|
}
|
|
121
116
|
}
|
|
122
|
-
// JSX
|
|
117
|
+
// JSX
|
|
123
118
|
if (typescript_1.default.isJsxSelfClosingElement(node) ||
|
|
124
119
|
typescript_1.default.isJsxOpeningElement(node)) {
|
|
125
120
|
const tagNode = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
|
@@ -127,41 +122,30 @@ function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
|
127
122
|
: node.tagName;
|
|
128
123
|
let symbol = checker.getSymbolAtLocation(tagNode);
|
|
129
124
|
if (symbol) {
|
|
130
|
-
|
|
131
|
-
if (resolved)
|
|
132
|
-
symbol = resolved;
|
|
125
|
+
symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
133
126
|
}
|
|
134
127
|
if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
|
|
135
|
-
|
|
136
|
-
if (aliased)
|
|
137
|
-
symbol = aliased;
|
|
128
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
138
129
|
}
|
|
139
130
|
if (!symbol)
|
|
140
131
|
return;
|
|
141
132
|
const tagName = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
|
142
133
|
? node.tagName.name.getText()
|
|
143
134
|
: node.tagName.getText();
|
|
144
|
-
const
|
|
145
|
-
if (!declarations)
|
|
146
|
-
return;
|
|
147
|
-
for (const decl of declarations) {
|
|
135
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
148
136
|
let fn = null;
|
|
149
|
-
if (typescript_1.default.isFunctionDeclaration(decl))
|
|
137
|
+
if (typescript_1.default.isFunctionDeclaration(decl))
|
|
150
138
|
fn = decl;
|
|
151
|
-
}
|
|
152
139
|
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
153
140
|
const init = decl.initializer;
|
|
154
|
-
if (typescript_1.default.isArrowFunction(init) ||
|
|
155
|
-
typescript_1.default.isFunctionExpression(init)) {
|
|
141
|
+
if (typescript_1.default.isArrowFunction(init) || typescript_1.default.isFunctionExpression(init)) {
|
|
156
142
|
fn = init;
|
|
157
143
|
}
|
|
158
144
|
}
|
|
159
145
|
if (!fn)
|
|
160
146
|
continue;
|
|
161
147
|
const param = fn.parameters[0];
|
|
162
|
-
if (!param)
|
|
163
|
-
continue;
|
|
164
|
-
if (!typescript_1.default.isObjectBindingPattern(param.name))
|
|
148
|
+
if (!param || !typescript_1.default.isObjectBindingPattern(param.name))
|
|
165
149
|
continue;
|
|
166
150
|
for (const el of param.name.elements) {
|
|
167
151
|
const prop = el.name.getText();
|
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
|
|
|
@@ -44,8 +45,7 @@ function isPropUsedInTracking(
|
|
|
44
45
|
for (const arg of node.arguments) {
|
|
45
46
|
if (
|
|
46
47
|
(ts.isIdentifier(arg) && arg.text === prop) ||
|
|
47
|
-
(ts.isPropertyAccessExpression(arg) &&
|
|
48
|
-
arg.name.text === prop)
|
|
48
|
+
(ts.isPropertyAccessExpression(arg) && arg.name.text === prop)
|
|
49
49
|
) {
|
|
50
50
|
found = true;
|
|
51
51
|
}
|
|
@@ -99,16 +99,11 @@ export function scanSource(
|
|
|
99
99
|
const isTracking =
|
|
100
100
|
isKnownWrapper ||
|
|
101
101
|
TRACKING_NAMES.has(name) ||
|
|
102
|
-
isAutoWrapper;
|
|
102
|
+
(isAutoWrapper && fn && !isExternalFile(fn.getSourceFile().fileName));
|
|
103
103
|
|
|
104
104
|
if (!isTracking) return;
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
!isKnownWrapper &&
|
|
109
|
-
fn &&
|
|
110
|
-
!TRACKING_NAMES.has(name)
|
|
111
|
-
) {
|
|
106
|
+
if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
|
|
112
107
|
detectedFunctionWrappers.add(name);
|
|
113
108
|
}
|
|
114
109
|
|
|
@@ -145,7 +140,6 @@ export function scanSource(
|
|
|
145
140
|
}
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
// fallback
|
|
149
143
|
if (!handled) {
|
|
150
144
|
for (const arg of node.arguments) {
|
|
151
145
|
const res = resolveNodeValue(arg, checker, paramMap);
|
|
@@ -156,7 +150,6 @@ export function scanSource(
|
|
|
156
150
|
}
|
|
157
151
|
}
|
|
158
152
|
|
|
159
|
-
// cross-file traversal
|
|
160
153
|
if (fn) {
|
|
161
154
|
const sf = fn.getSourceFile();
|
|
162
155
|
|
|
@@ -174,7 +167,7 @@ export function scanSource(
|
|
|
174
167
|
}
|
|
175
168
|
}
|
|
176
169
|
|
|
177
|
-
// JSX
|
|
170
|
+
// JSX
|
|
178
171
|
if (
|
|
179
172
|
ts.isJsxSelfClosingElement(node) ||
|
|
180
173
|
ts.isJsxOpeningElement(node)
|
|
@@ -187,13 +180,11 @@ export function scanSource(
|
|
|
187
180
|
let symbol = checker.getSymbolAtLocation(tagNode);
|
|
188
181
|
|
|
189
182
|
if (symbol) {
|
|
190
|
-
|
|
191
|
-
if (resolved) symbol = resolved;
|
|
183
|
+
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
192
184
|
}
|
|
193
185
|
|
|
194
186
|
if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
|
|
195
|
-
|
|
196
|
-
if (aliased) symbol = aliased;
|
|
187
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
197
188
|
}
|
|
198
189
|
|
|
199
190
|
if (!symbol) return;
|
|
@@ -203,23 +194,14 @@ export function scanSource(
|
|
|
203
194
|
? node.tagName.name.getText()
|
|
204
195
|
: node.tagName.getText();
|
|
205
196
|
|
|
206
|
-
const
|
|
207
|
-
if (!declarations) return;
|
|
208
|
-
|
|
209
|
-
for (const decl of declarations) {
|
|
197
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
210
198
|
let fn: ts.FunctionLikeDeclaration | null = null;
|
|
211
199
|
|
|
212
|
-
if (ts.isFunctionDeclaration(decl))
|
|
213
|
-
fn = decl;
|
|
214
|
-
}
|
|
200
|
+
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
215
201
|
|
|
216
202
|
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
217
203
|
const init = decl.initializer;
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
ts.isArrowFunction(init) ||
|
|
221
|
-
ts.isFunctionExpression(init)
|
|
222
|
-
) {
|
|
204
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
223
205
|
fn = init;
|
|
224
206
|
}
|
|
225
207
|
}
|
|
@@ -227,9 +209,7 @@ export function scanSource(
|
|
|
227
209
|
if (!fn) continue;
|
|
228
210
|
|
|
229
211
|
const param = fn.parameters[0];
|
|
230
|
-
if (!param) continue;
|
|
231
|
-
|
|
232
|
-
if (!ts.isObjectBindingPattern(param.name)) continue;
|
|
212
|
+
if (!param || !ts.isObjectBindingPattern(param.name)) continue;
|
|
233
213
|
|
|
234
214
|
for (const el of param.name.elements) {
|
|
235
215
|
const prop = el.name.getText();
|