@eventra_dev/eventra-cli 0.1.6 → 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/commands/check.js +1 -1
- package/dist/commands/sync.js +36 -2
- package/dist/commands/watch.js +2 -2
- package/dist/engine/boundary.js +14 -0
- package/dist/engine/callResolver.js +61 -5
- package/dist/engine/engine.js +71 -20
- package/dist/engine/graph.js +39 -20
- package/dist/engine/languageService.js +10 -0
- package/dist/engine/resolver.js +10 -2
- package/dist/engine/scanner.js +147 -62
- package/package.json +1 -1
- package/src/commands/check.ts +1 -1
- package/src/commands/sync.ts +62 -2
- package/src/commands/watch.ts +2 -2
- package/src/engine/boundary.ts +12 -0
- package/src/engine/callResolver.ts +78 -8
- package/src/engine/engine.ts +94 -21
- package/src/engine/graph.ts +49 -24
- package/src/engine/languageService.ts +13 -0
- package/src/engine/resolver.ts +25 -4
- package/src/engine/scanner.ts +203 -72
package/dist/engine/scanner.js
CHANGED
|
@@ -7,94 +7,179 @@ 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");
|
|
10
11
|
const functionAnalyzer_1 = require("./functionAnalyzer");
|
|
12
|
+
const boundary_1 = require("./boundary");
|
|
13
|
+
const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
|
|
11
14
|
function getCallName(expr) {
|
|
12
15
|
if (typescript_1.default.isIdentifier(expr))
|
|
13
16
|
return expr.text;
|
|
14
|
-
if (typescript_1.default.isPropertyAccessExpression(expr))
|
|
17
|
+
if (typescript_1.default.isPropertyAccessExpression(expr))
|
|
15
18
|
return expr.name.text;
|
|
16
|
-
}
|
|
17
19
|
return "";
|
|
18
20
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
function getJsxAttrName(name) {
|
|
22
|
+
if (typescript_1.default.isIdentifier(name))
|
|
23
|
+
return name.text;
|
|
24
|
+
if (typescript_1.default.isJsxNamespacedName(name))
|
|
25
|
+
return name.name.text;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function isValidEvent(v) {
|
|
29
|
+
return (v &&
|
|
30
|
+
v.length > 1 &&
|
|
31
|
+
v.length < 100 &&
|
|
32
|
+
!v.includes(" ") &&
|
|
33
|
+
/^[a-zA-Z0-9._:-]+$/.test(v));
|
|
34
|
+
}
|
|
35
|
+
function isPropUsedInTracking(fn, prop) {
|
|
36
|
+
let found = false;
|
|
37
|
+
function visit(node) {
|
|
38
|
+
if (typescript_1.default.isCallExpression(node)) {
|
|
39
|
+
const name = getCallName(node.expression);
|
|
40
|
+
if (!TRACKING_NAMES.has(name))
|
|
41
|
+
return;
|
|
42
|
+
for (const arg of node.arguments) {
|
|
43
|
+
if ((typescript_1.default.isIdentifier(arg) && arg.text === prop) ||
|
|
44
|
+
(typescript_1.default.isPropertyAccessExpression(arg) && arg.name.text === prop)) {
|
|
45
|
+
found = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
}
|
|
49
|
+
typescript_1.default.forEachChild(node, visit);
|
|
35
50
|
}
|
|
36
|
-
|
|
51
|
+
visit(fn);
|
|
52
|
+
return found;
|
|
37
53
|
}
|
|
38
|
-
function scanSource(source, checker, visited = new Set(), depth = 0) {
|
|
39
|
-
if (depth > 5)
|
|
40
|
-
return new Set();
|
|
54
|
+
function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
41
55
|
const events = new Set();
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
const detectedFunctionWrappers = new Set();
|
|
57
|
+
const detectedComponentWrappers = new Map();
|
|
58
|
+
if (depth > 5 || visited.has(source.fileName)) {
|
|
59
|
+
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
60
|
+
}
|
|
44
61
|
visited.add(source.fileName);
|
|
45
62
|
function visit(node) {
|
|
63
|
+
// CALL EXPRESSIONS
|
|
46
64
|
if (typescript_1.default.isCallExpression(node)) {
|
|
47
65
|
const name = getCallName(node.expression);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
|
|
67
|
+
const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
|
|
68
|
+
const isAutoWrapper = fn && (0, functionAnalyzer_1.findTrackParamIndex)(fn) !== null;
|
|
69
|
+
const isTracking = isKnownWrapper ||
|
|
70
|
+
TRACKING_NAMES.has(name) ||
|
|
71
|
+
(isAutoWrapper && fn && !(0, boundary_1.isExternalFile)(fn.getSourceFile().fileName));
|
|
72
|
+
if (!isTracking)
|
|
73
|
+
return;
|
|
74
|
+
if (!isKnownWrapper && isAutoWrapper && !TRACKING_NAMES.has(name)) {
|
|
75
|
+
detectedFunctionWrappers.add(name);
|
|
76
|
+
}
|
|
77
|
+
const paramMap = new Map();
|
|
78
|
+
if (fn && typescript_1.default.isFunctionLike(fn)) {
|
|
79
|
+
fn.parameters.forEach((param, i) => {
|
|
80
|
+
const arg = node.arguments[i];
|
|
81
|
+
if (!arg)
|
|
82
|
+
return;
|
|
83
|
+
if (typescript_1.default.isIdentifier(param.name)) {
|
|
84
|
+
paramMap.set(param.name.text, arg);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
let handled = false;
|
|
89
|
+
if (fn) {
|
|
90
|
+
const idx = (0, functionAnalyzer_1.findTrackParamIndex)(fn);
|
|
91
|
+
if (idx !== null && node.arguments[idx]) {
|
|
92
|
+
const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
|
|
93
|
+
res?.values.forEach(v => {
|
|
94
|
+
if (isValidEvent(v))
|
|
95
|
+
events.add(v);
|
|
59
96
|
});
|
|
97
|
+
handled = true;
|
|
60
98
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
}
|
|
100
|
+
if (!handled) {
|
|
101
|
+
for (const arg of node.arguments) {
|
|
102
|
+
const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
|
|
103
|
+
res?.values.forEach(v => {
|
|
104
|
+
if (isValidEvent(v))
|
|
105
|
+
events.add(v);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (fn) {
|
|
110
|
+
const sf = fn.getSourceFile();
|
|
111
|
+
if (!visited.has(sf.fileName)) {
|
|
112
|
+
const res = scanSource(sf, checker, config, visited, depth + 1);
|
|
113
|
+
res.events.forEach(e => events.add(e));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// JSX
|
|
118
|
+
if (typescript_1.default.isJsxSelfClosingElement(node) ||
|
|
119
|
+
typescript_1.default.isJsxOpeningElement(node)) {
|
|
120
|
+
const tagNode = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
|
121
|
+
? node.tagName.name
|
|
122
|
+
: node.tagName;
|
|
123
|
+
let symbol = checker.getSymbolAtLocation(tagNode);
|
|
124
|
+
if (symbol) {
|
|
125
|
+
symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
126
|
+
}
|
|
127
|
+
if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
|
|
128
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
129
|
+
}
|
|
130
|
+
if (!symbol)
|
|
131
|
+
return;
|
|
132
|
+
const tagName = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
|
133
|
+
? node.tagName.name.getText()
|
|
134
|
+
: node.tagName.getText();
|
|
135
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
136
|
+
let fn = null;
|
|
137
|
+
if (typescript_1.default.isFunctionDeclaration(decl))
|
|
138
|
+
fn = decl;
|
|
139
|
+
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
140
|
+
const init = decl.initializer;
|
|
141
|
+
if (typescript_1.default.isArrowFunction(init) || typescript_1.default.isFunctionExpression(init)) {
|
|
142
|
+
fn = init;
|
|
78
143
|
}
|
|
79
144
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
145
|
+
if (!fn)
|
|
146
|
+
continue;
|
|
147
|
+
const param = fn.parameters[0];
|
|
148
|
+
if (!param || !typescript_1.default.isObjectBindingPattern(param.name))
|
|
149
|
+
continue;
|
|
150
|
+
for (const el of param.name.elements) {
|
|
151
|
+
const prop = el.name.getText();
|
|
152
|
+
const isEvent = isPropUsedInTracking(fn, prop) ||
|
|
153
|
+
config.wrappers.some(w => w.name === tagName && w.prop === prop);
|
|
154
|
+
if (!isEvent)
|
|
155
|
+
continue;
|
|
156
|
+
const attr = node.attributes.properties.find(p => {
|
|
157
|
+
if (!typescript_1.default.isJsxAttribute(p))
|
|
158
|
+
return false;
|
|
159
|
+
return getJsxAttrName(p.name) === prop;
|
|
160
|
+
});
|
|
161
|
+
if (!attr || !attr.initializer)
|
|
162
|
+
continue;
|
|
163
|
+
const expr = typescript_1.default.isJsxExpression(attr.initializer)
|
|
164
|
+
? attr.initializer.expression
|
|
165
|
+
: attr.initializer;
|
|
166
|
+
if (!expr)
|
|
167
|
+
continue;
|
|
168
|
+
const res = (0, resolver_1.resolveNodeValue)(expr, checker);
|
|
84
169
|
res?.values.forEach(v => {
|
|
85
|
-
if (v
|
|
86
|
-
v.length > 1 &&
|
|
87
|
-
v.length < 100 &&
|
|
88
|
-
!v.includes(" ") &&
|
|
89
|
-
/^[a-zA-Z0-9._:-]+$/.test(v)) {
|
|
170
|
+
if (isValidEvent(v))
|
|
90
171
|
events.add(v);
|
|
91
|
-
}
|
|
92
172
|
});
|
|
173
|
+
detectedComponentWrappers.set(tagName, prop);
|
|
93
174
|
}
|
|
94
175
|
}
|
|
95
176
|
}
|
|
96
177
|
typescript_1.default.forEachChild(node, visit);
|
|
97
178
|
}
|
|
98
179
|
visit(source);
|
|
99
|
-
return
|
|
180
|
+
return {
|
|
181
|
+
events,
|
|
182
|
+
detectedFunctionWrappers,
|
|
183
|
+
detectedComponentWrappers,
|
|
184
|
+
};
|
|
100
185
|
}
|
package/package.json
CHANGED
package/src/commands/check.ts
CHANGED
|
@@ -28,7 +28,7 @@ export async function check({ fix = false }: { fix?: boolean }) {
|
|
|
28
28
|
|
|
29
29
|
const { content, virtualFile } = processFile(file, raw);
|
|
30
30
|
|
|
31
|
-
engine.scanFile(virtualFile, content);
|
|
31
|
+
engine.scanFile(virtualFile, content, config);
|
|
32
32
|
|
|
33
33
|
} catch (err) {
|
|
34
34
|
console.log(chalk.yellow(`Skipped: ${file}`));
|
package/src/commands/sync.ts
CHANGED
|
@@ -23,13 +23,24 @@ export async function sync() {
|
|
|
23
23
|
|
|
24
24
|
const engine = new EventraEngine(process.cwd());
|
|
25
25
|
|
|
26
|
+
const detectedFn = new Set<string>();
|
|
27
|
+
const detectedCmp = new Map<string, string>();
|
|
28
|
+
|
|
26
29
|
for (const file of files) {
|
|
27
30
|
try {
|
|
28
31
|
const raw = await fs.readFile(file, "utf-8");
|
|
29
32
|
|
|
30
33
|
const { content, virtualFile } = processFile(file, raw);
|
|
31
34
|
|
|
32
|
-
engine.scanFile(virtualFile, content);
|
|
35
|
+
const res = engine.scanFile(virtualFile, content, config);
|
|
36
|
+
|
|
37
|
+
for (const w of res.detectedFunctionWrappers) {
|
|
38
|
+
detectedFn.add(w);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const [k, v] of res.detectedComponentWrappers) {
|
|
42
|
+
detectedCmp.set(k, v);
|
|
43
|
+
}
|
|
33
44
|
|
|
34
45
|
} catch (err) {
|
|
35
46
|
console.log(chalk.yellow(`Skipped: ${file}`));
|
|
@@ -40,10 +51,59 @@ export async function sync() {
|
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
// EVENTS
|
|
43
55
|
const events = engine.getAllEvents().sort();
|
|
44
56
|
|
|
45
|
-
config.events
|
|
57
|
+
const mergedEvents = new Set(config.events ?? []);
|
|
58
|
+
events.forEach(e => mergedEvents.add(e));
|
|
59
|
+
|
|
60
|
+
config.events = [...mergedEvents].sort();
|
|
61
|
+
|
|
62
|
+
// FUNCTION WRAPPERS
|
|
63
|
+
config.functionWrappers = mergeUnique(
|
|
64
|
+
config.functionWrappers ?? [],
|
|
65
|
+
[...detectedFn].map(name => ({ name })),
|
|
66
|
+
w => w.name
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// COMPONENT WRAPPERS
|
|
70
|
+
config.wrappers = mergeUnique(
|
|
71
|
+
config.wrappers ?? [],
|
|
72
|
+
[...detectedCmp.entries()].map(([name, prop]) => ({
|
|
73
|
+
name,
|
|
74
|
+
prop,
|
|
75
|
+
})),
|
|
76
|
+
w => `${w.name}:${w.prop}`
|
|
77
|
+
);
|
|
78
|
+
|
|
46
79
|
await saveConfig(config);
|
|
47
80
|
|
|
48
81
|
console.log(chalk.green(`Found ${events.length} events`));
|
|
82
|
+
|
|
83
|
+
if (detectedFn.size) {
|
|
84
|
+
console.log(chalk.blue("\nDetected function wrappers:"));
|
|
85
|
+
detectedFn.forEach(w => console.log(chalk.gray(`• ${w}`)));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (detectedCmp.size) {
|
|
89
|
+
console.log(chalk.blue("\nDetected component wrappers:"));
|
|
90
|
+
detectedCmp.forEach((prop, name) =>
|
|
91
|
+
console.log(chalk.gray(`• ${name} (${prop})`))
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// HELPERS
|
|
97
|
+
function mergeUnique<T>(
|
|
98
|
+
a: T[],
|
|
99
|
+
b: T[],
|
|
100
|
+
key: (v: T) => string
|
|
101
|
+
): T[] {
|
|
102
|
+
const map = new Map<string, T>();
|
|
103
|
+
|
|
104
|
+
[...b, ...a].forEach(item => {
|
|
105
|
+
map.set(key(item), item);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return [...map.values()];
|
|
49
109
|
}
|
package/src/commands/watch.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function watch() {
|
|
|
25
25
|
|
|
26
26
|
const { content, virtualFile } = processFile(file, raw);
|
|
27
27
|
|
|
28
|
-
engine.scanFile(virtualFile, content);
|
|
28
|
+
engine.scanFile(virtualFile, content, config);
|
|
29
29
|
|
|
30
30
|
} catch {
|
|
31
31
|
console.log(chalk.gray(`skip: ${file}`));
|
|
@@ -56,7 +56,7 @@ export async function watch() {
|
|
|
56
56
|
|
|
57
57
|
const { content, virtualFile } = processFile(file, raw);
|
|
58
58
|
|
|
59
|
-
engine.updateFile(virtualFile, content);
|
|
59
|
+
engine.updateFile(virtualFile, content, config);
|
|
60
60
|
|
|
61
61
|
} catch {
|
|
62
62
|
console.log(chalk.gray(`skip: ${file}`));
|
|
@@ -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
|
+
}
|
|
@@ -1,25 +1,95 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
|
|
3
|
+
function isFunctionLike(
|
|
4
|
+
node: ts.Node
|
|
5
|
+
): node is ts.FunctionLikeDeclaration {
|
|
6
|
+
return (
|
|
7
|
+
ts.isFunctionDeclaration(node) ||
|
|
8
|
+
ts.isMethodDeclaration(node) ||
|
|
9
|
+
ts.isConstructorDeclaration(node) ||
|
|
10
|
+
ts.isFunctionExpression(node) ||
|
|
11
|
+
ts.isArrowFunction(node)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveFromSymbol(
|
|
16
|
+
symbol: ts.Symbol,
|
|
17
|
+
checker: ts.TypeChecker,
|
|
18
|
+
visited = new Set<ts.Symbol>(),
|
|
19
|
+
depth = 0
|
|
20
|
+
): ts.FunctionLikeDeclaration | null {
|
|
21
|
+
if (visited.has(symbol)) return null;
|
|
22
|
+
if (depth > 5) return null;
|
|
23
|
+
|
|
24
|
+
visited.add(symbol);
|
|
25
|
+
|
|
26
|
+
// unwrap alias
|
|
27
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
28
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
29
|
+
return resolveFromSymbol(aliased, checker, visited, depth + 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const decls = symbol.getDeclarations() ?? [];
|
|
33
|
+
|
|
34
|
+
for (const decl of decls) {
|
|
35
|
+
if (isFunctionLike(decl)) {
|
|
36
|
+
return decl;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MethodSignature
|
|
40
|
+
if (ts.isMethodSignature(decl)) {
|
|
41
|
+
const parent = decl.parent;
|
|
42
|
+
|
|
43
|
+
if (ts.isInterfaceDeclaration(parent) || ts.isTypeLiteralNode(parent)) {
|
|
44
|
+
const name = decl.name.getText();
|
|
45
|
+
|
|
46
|
+
const type = checker.getTypeAtLocation(parent);
|
|
47
|
+
const props = type.getProperties();
|
|
48
|
+
|
|
49
|
+
for (const prop of props) {
|
|
50
|
+
if (prop.name !== name) continue;
|
|
51
|
+
|
|
52
|
+
const resolved = resolveFromSymbol(prop, checker, visited, depth + 1);
|
|
53
|
+
if (resolved) return resolved;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// SAFE fallback
|
|
59
|
+
const s = checker.getSymbolAtLocation(decl);
|
|
60
|
+
if (s && s !== symbol) {
|
|
61
|
+
const resolved = resolveFromSymbol(s, checker, visited, depth + 1);
|
|
62
|
+
if (resolved) return resolved;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
3
69
|
export function resolveFunctionFromCall(
|
|
4
70
|
expr: ts.Expression,
|
|
5
71
|
checker: ts.TypeChecker
|
|
6
72
|
): ts.FunctionLikeDeclaration | null {
|
|
7
|
-
|
|
8
73
|
const type = checker.getTypeAtLocation(expr);
|
|
9
74
|
const signatures = type.getCallSignatures();
|
|
10
75
|
|
|
11
76
|
for (const sig of signatures) {
|
|
12
77
|
const decl = sig.getDeclaration();
|
|
78
|
+
if (!decl) continue;
|
|
13
79
|
|
|
14
|
-
if (
|
|
15
|
-
decl &&
|
|
16
|
-
(ts.isFunctionDeclaration(decl) ||
|
|
17
|
-
ts.isMethodDeclaration(decl) ||
|
|
18
|
-
ts.isFunctionExpression(decl) ||
|
|
19
|
-
ts.isArrowFunction(decl))
|
|
20
|
-
) {
|
|
80
|
+
if (isFunctionLike(decl)) {
|
|
21
81
|
return decl;
|
|
22
82
|
}
|
|
83
|
+
|
|
84
|
+
// interface method
|
|
85
|
+
if (ts.isMethodSignature(decl)) {
|
|
86
|
+
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
87
|
+
|
|
88
|
+
if (symbol) {
|
|
89
|
+
const resolved = resolveFromSymbol(symbol, checker);
|
|
90
|
+
if (resolved) return resolved;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
23
93
|
}
|
|
24
94
|
|
|
25
95
|
return null;
|
package/src/engine/engine.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { TSService } from "./languageService";
|
|
2
|
-
import { scanSource } from "./scanner";
|
|
2
|
+
import { ScanResult, scanSource } from "./scanner";
|
|
3
3
|
import { DependencyGraph } from "./graph";
|
|
4
4
|
import { hash } from "./hash";
|
|
5
|
+
import { EventraConfig } from "../types";
|
|
6
|
+
import { isExternalFile } from "./boundary";
|
|
5
7
|
|
|
6
8
|
export class EventraEngine {
|
|
7
9
|
private ts: TSService;
|
|
8
10
|
private graph: DependencyGraph;
|
|
9
11
|
|
|
10
|
-
private
|
|
12
|
+
private fileResults = new Map<string, ScanResult>();
|
|
11
13
|
private fileHash = new Map<string, string>();
|
|
12
14
|
|
|
13
15
|
constructor(root: string) {
|
|
@@ -17,65 +19,136 @@ export class EventraEngine {
|
|
|
17
19
|
this.graph = new DependencyGraph(baseUrl, paths);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
// SCAN FILE
|
|
23
|
+
scanFile(
|
|
24
|
+
file: string,
|
|
25
|
+
content: string,
|
|
26
|
+
config: EventraConfig
|
|
27
|
+
): ScanResult {
|
|
21
28
|
const h = hash(content);
|
|
22
29
|
|
|
23
|
-
//
|
|
30
|
+
// skip unchanged
|
|
24
31
|
if (this.fileHash.get(file) === h) {
|
|
25
|
-
return
|
|
32
|
+
return (
|
|
33
|
+
this.fileResults.get(file) ?? {
|
|
34
|
+
events: new Set(),
|
|
35
|
+
detectedFunctionWrappers: new Set(),
|
|
36
|
+
detectedComponentWrappers: new Map(),
|
|
37
|
+
}
|
|
38
|
+
);
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
this.fileHash.set(file, h);
|
|
29
|
-
|
|
30
42
|
this.ts.updateFile(file, content);
|
|
31
43
|
|
|
32
44
|
const source = this.ts.getSourceFile(file);
|
|
33
45
|
const checker = this.ts.getChecker();
|
|
34
46
|
|
|
35
|
-
if (!source || !checker)
|
|
47
|
+
if (!source || !checker) {
|
|
48
|
+
return {
|
|
49
|
+
events: new Set(),
|
|
50
|
+
detectedFunctionWrappers: new Set(),
|
|
51
|
+
detectedComponentWrappers: new Map(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
36
54
|
|
|
37
|
-
//
|
|
55
|
+
// update dependency graph
|
|
38
56
|
this.graph.update(file, source);
|
|
39
57
|
|
|
40
|
-
const
|
|
58
|
+
const res = scanSource(source, checker, config);
|
|
41
59
|
|
|
42
|
-
this.
|
|
60
|
+
this.fileResults.set(file, res);
|
|
43
61
|
|
|
44
|
-
return
|
|
62
|
+
return res;
|
|
45
63
|
}
|
|
46
64
|
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
// UPDATE FILE (incremental)
|
|
66
|
+
updateFile(
|
|
67
|
+
file: string,
|
|
68
|
+
content: string,
|
|
69
|
+
config: EventraConfig
|
|
70
|
+
) {
|
|
71
|
+
const res = this.scanFile(file, content, config);
|
|
49
72
|
|
|
50
|
-
const dependents = this.graph.
|
|
73
|
+
const dependents = this.graph.getAllDependentsDeep(file);
|
|
51
74
|
|
|
52
75
|
for (const dep of dependents) {
|
|
76
|
+
// skip external files
|
|
77
|
+
if (isExternalFile(dep)) continue;
|
|
78
|
+
|
|
53
79
|
const source = this.ts.getSourceFile(dep);
|
|
54
80
|
const checker = this.ts.getChecker();
|
|
55
81
|
|
|
56
82
|
if (!source || !checker) continue;
|
|
57
83
|
|
|
58
|
-
const
|
|
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);
|
|
59
93
|
|
|
60
|
-
this.
|
|
94
|
+
this.graph.update(dep, source);
|
|
95
|
+
|
|
96
|
+
const r = scanSource(source, checker, config);
|
|
97
|
+
this.fileResults.set(dep, r);
|
|
61
98
|
}
|
|
62
99
|
|
|
63
|
-
return
|
|
100
|
+
return res;
|
|
64
101
|
}
|
|
65
102
|
|
|
103
|
+
// GET ALL EVENTS
|
|
66
104
|
getAllEvents() {
|
|
67
105
|
const all = new Set<string>();
|
|
68
106
|
|
|
69
|
-
for (const
|
|
70
|
-
|
|
107
|
+
for (const r of this.fileResults.values()) {
|
|
108
|
+
r.events.forEach(e => all.add(e));
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
return [...all];
|
|
74
112
|
}
|
|
75
113
|
|
|
76
|
-
|
|
77
|
-
|
|
114
|
+
// REMOVE FILE
|
|
115
|
+
removeFile(file: string, config?: EventraConfig) {
|
|
116
|
+
this.ts.removeFile(file);
|
|
117
|
+
this.fileResults.delete(file);
|
|
78
118
|
this.fileHash.delete(file);
|
|
79
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
|
+
}
|
|
80
153
|
}
|
|
81
154
|
}
|