@eventra_dev/eventra-cli 0.1.1 → 0.1.2
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/callResolver.js +22 -19
- package/dist/engine/engine.js +20 -6
- package/dist/engine/graph.js +11 -8
- package/dist/engine/hash.js +10 -0
- package/dist/engine/languageService.js +67 -0
- package/dist/engine/resolver.js +56 -25
- package/dist/engine/scanner.js +39 -67
- package/package.json +1 -1
- package/src/engine/callResolver.ts +25 -19
- package/src/engine/engine.ts +27 -6
- package/src/engine/graph.ts +10 -10
- package/src/engine/hash.ts +5 -0
- package/src/engine/languageService.ts +73 -0
- package/src/engine/resolver.ts +59 -26
- package/src/engine/scanner.ts +39 -68
- package/src/types.ts +5 -0
- package/dist/engine/extract.js +0 -53
- package/dist/engine/flow.js +0 -38
- package/dist/engine/project.js +0 -74
- package/dist/engine/symbol.js +0 -31
- package/src/engine/extract.ts +0 -74
- package/src/engine/flow.ts +0 -54
- package/src/engine/project.ts +0 -75
- package/src/engine/symbol.ts +0 -42
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.resolveFunctionFromCall = resolveFunctionFromCall;
|
|
4
|
-
const
|
|
5
|
-
function resolveFunctionFromCall(expr) {
|
|
6
|
-
let symbol =
|
|
7
|
-
if (!symbol &&
|
|
8
|
-
symbol =
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
function resolveFunctionFromCall(expr, checker) {
|
|
9
|
+
let symbol = checker.getSymbolAtLocation(expr);
|
|
10
|
+
if (!symbol && typescript_1.default.isPropertyAccessExpression(expr)) {
|
|
11
|
+
symbol = checker.getSymbolAtLocation(expr.name);
|
|
9
12
|
}
|
|
10
13
|
if (!symbol)
|
|
11
14
|
return null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
16
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
17
|
+
}
|
|
18
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
19
|
+
if (typescript_1.default.isFunctionDeclaration(decl) ||
|
|
20
|
+
typescript_1.default.isMethodDeclaration(decl) ||
|
|
21
|
+
typescript_1.default.isFunctionExpression(decl) ||
|
|
22
|
+
typescript_1.default.isArrowFunction(decl)) {
|
|
23
|
+
return decl;
|
|
24
|
+
}
|
|
25
|
+
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
26
|
+
const init = decl.initializer;
|
|
27
|
+
if (typescript_1.default.isArrowFunction(init) ||
|
|
28
|
+
typescript_1.default.isFunctionExpression(init)) {
|
|
21
29
|
return init;
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
|
-
if (ts_morph_1.Node.isMethodDeclaration(d))
|
|
25
|
-
return d;
|
|
26
|
-
if (ts_morph_1.Node.isFunctionExpression(d) || ts_morph_1.Node.isArrowFunction(d)) {
|
|
27
|
-
return d;
|
|
28
|
-
}
|
|
29
32
|
}
|
|
30
33
|
return null;
|
|
31
34
|
}
|
package/dist/engine/engine.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EventraEngine = void 0;
|
|
4
|
-
const
|
|
4
|
+
const languageService_1 = require("./languageService");
|
|
5
5
|
const scanner_1 = require("./scanner");
|
|
6
|
+
const graph_1 = require("./graph");
|
|
7
|
+
const hash_1 = require("./hash");
|
|
6
8
|
class EventraEngine {
|
|
7
|
-
|
|
9
|
+
ts;
|
|
10
|
+
graph = new graph_1.DependencyGraph();
|
|
8
11
|
fileEvents = new Map();
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
fileHash = new Map();
|
|
13
|
+
constructor(root) {
|
|
14
|
+
this.ts = new languageService_1.TSService(root);
|
|
11
15
|
}
|
|
12
16
|
scanFile(file, content) {
|
|
13
|
-
const
|
|
14
|
-
|
|
17
|
+
const h = (0, hash_1.hash)(content);
|
|
18
|
+
if (this.fileHash.get(file) === h) {
|
|
19
|
+
return this.fileEvents.get(file) ?? new Set();
|
|
20
|
+
}
|
|
21
|
+
this.fileHash.set(file, h);
|
|
22
|
+
this.ts.updateFile(file, content);
|
|
23
|
+
const source = this.ts.getSourceFile(file);
|
|
24
|
+
const checker = this.ts.getChecker();
|
|
25
|
+
if (!source || !checker)
|
|
26
|
+
return new Set();
|
|
27
|
+
this.graph.update(file, source);
|
|
15
28
|
const events = (0, scanner_1.scanSource)(source, checker);
|
|
16
29
|
this.fileEvents.set(file, events);
|
|
17
30
|
return events;
|
|
@@ -28,6 +41,7 @@ class EventraEngine {
|
|
|
28
41
|
}
|
|
29
42
|
removeFile(file) {
|
|
30
43
|
this.fileEvents.delete(file);
|
|
44
|
+
this.fileHash.delete(file);
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
exports.EventraEngine = EventraEngine;
|
package/dist/engine/graph.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.DependencyGraph = void 0;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
4
8
|
class DependencyGraph {
|
|
5
9
|
reverse = new Map();
|
|
6
10
|
update(file, source) {
|
|
7
|
-
const
|
|
8
|
-
.
|
|
9
|
-
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.reverse.set(dep, new Set());
|
|
11
|
+
for (const stmt of source.statements) {
|
|
12
|
+
if (!typescript_1.default.isImportDeclaration(stmt))
|
|
13
|
+
continue;
|
|
14
|
+
const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
15
|
+
if (!this.reverse.has(module)) {
|
|
16
|
+
this.reverse.set(module, new Set());
|
|
14
17
|
}
|
|
15
|
-
this.reverse.get(
|
|
18
|
+
this.reverse.get(module).add(file);
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
getDependents(file) {
|
|
@@ -0,0 +1,10 @@
|
|
|
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.hash = hash;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
function hash(content) {
|
|
9
|
+
return crypto_1.default.createHash("md5").update(content).digest("hex");
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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.TSService = void 0;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
class TSService {
|
|
10
|
+
root;
|
|
11
|
+
files = new Map();
|
|
12
|
+
service;
|
|
13
|
+
constructor(root) {
|
|
14
|
+
this.root = root;
|
|
15
|
+
const host = {
|
|
16
|
+
getScriptFileNames: () => [...this.files.keys()],
|
|
17
|
+
getScriptVersion: (file) => this.files.get(file)?.version.toString() ?? "0",
|
|
18
|
+
getScriptSnapshot: (file) => {
|
|
19
|
+
const f = this.files.get(file);
|
|
20
|
+
if (f) {
|
|
21
|
+
return typescript_1.default.ScriptSnapshot.fromString(f.content);
|
|
22
|
+
}
|
|
23
|
+
if (fs_1.default.existsSync(file)) {
|
|
24
|
+
return typescript_1.default.ScriptSnapshot.fromString(fs_1.default.readFileSync(file, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
},
|
|
28
|
+
getCurrentDirectory: () => this.root,
|
|
29
|
+
getCompilationSettings: () => ({
|
|
30
|
+
allowJs: true,
|
|
31
|
+
jsx: typescript_1.default.JsxEmit.Preserve,
|
|
32
|
+
moduleResolution: typescript_1.default.ModuleResolutionKind.Node16,
|
|
33
|
+
target: typescript_1.default.ScriptTarget.ESNext,
|
|
34
|
+
esModuleInterop: true,
|
|
35
|
+
allowSyntheticDefaultImports: true,
|
|
36
|
+
}),
|
|
37
|
+
getDefaultLibFileName: (opts) => typescript_1.default.getDefaultLibFilePath(opts),
|
|
38
|
+
readFile: (fileName, encoding) => {
|
|
39
|
+
try {
|
|
40
|
+
return fs_1.default.readFileSync(fileName, encoding);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
fileExists: fs_1.default.existsSync,
|
|
47
|
+
};
|
|
48
|
+
this.service = typescript_1.default.createLanguageService(host);
|
|
49
|
+
}
|
|
50
|
+
updateFile(file, content) {
|
|
51
|
+
const prev = this.files.get(file);
|
|
52
|
+
this.files.set(file, {
|
|
53
|
+
content,
|
|
54
|
+
version: prev ? prev.version + 1 : 1,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
getProgram() {
|
|
58
|
+
return this.service.getProgram();
|
|
59
|
+
}
|
|
60
|
+
getChecker() {
|
|
61
|
+
return this.getProgram()?.getTypeChecker();
|
|
62
|
+
}
|
|
63
|
+
getSourceFile(file) {
|
|
64
|
+
return this.getProgram()?.getSourceFile(file);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.TSService = TSService;
|
package/dist/engine/resolver.js
CHANGED
|
@@ -1,36 +1,67 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.resolveNodeValue = resolveNodeValue;
|
|
4
|
-
const
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
5
8
|
function resolveNodeValue(node, checker) {
|
|
6
|
-
if (
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
if (typescript_1.default.isStringLiteral(node)) {
|
|
10
|
+
return { values: [node.text], dynamic: false };
|
|
11
|
+
}
|
|
12
|
+
if (typescript_1.default.isTemplateExpression(node)) {
|
|
13
|
+
let result = node.head.text;
|
|
14
|
+
for (const span of node.templateSpans) {
|
|
15
|
+
result += "*";
|
|
16
|
+
result += span.literal.text;
|
|
17
|
+
}
|
|
18
|
+
return { values: [result], dynamic: true };
|
|
19
|
+
}
|
|
20
|
+
if (typescript_1.default.isConditionalExpression(node)) {
|
|
21
|
+
const a = resolveNodeValue(node.whenTrue, checker);
|
|
22
|
+
const b = resolveNodeValue(node.whenFalse, checker);
|
|
23
|
+
return {
|
|
24
|
+
values: [...new Set([...(a?.values ?? []), ...(b?.values ?? [])])],
|
|
25
|
+
dynamic: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (typescript_1.default.isIdentifier(node)) {
|
|
29
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
30
|
+
if (!symbol)
|
|
31
|
+
return null;
|
|
32
|
+
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
33
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
34
|
+
}
|
|
35
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
36
|
+
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
37
|
+
return resolveNodeValue(decl.initializer, checker);
|
|
20
38
|
}
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
if (typescript_1.default.isEnumMember(decl) && decl.initializer) {
|
|
40
|
+
if (typescript_1.default.isStringLiteral(decl.initializer)) {
|
|
41
|
+
return {
|
|
42
|
+
values: [decl.initializer.text],
|
|
43
|
+
dynamic: false,
|
|
44
|
+
};
|
|
26
45
|
}
|
|
27
|
-
return { values: [result], dynamic: true };
|
|
28
46
|
}
|
|
29
47
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
}
|
|
49
|
+
if (typescript_1.default.isPropertyAccessExpression(node)) {
|
|
50
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
51
|
+
if (symbol) {
|
|
52
|
+
let s = symbol;
|
|
53
|
+
if (s.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
54
|
+
s = checker.getAliasedSymbol(s);
|
|
55
|
+
}
|
|
56
|
+
for (const decl of s.getDeclarations() ?? []) {
|
|
57
|
+
if (typescript_1.default.isEnumMember(decl) && decl.initializer) {
|
|
58
|
+
if (typescript_1.default.isStringLiteral(decl.initializer)) {
|
|
59
|
+
return {
|
|
60
|
+
values: [decl.initializer.text],
|
|
61
|
+
dynamic: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
34
65
|
}
|
|
35
66
|
}
|
|
36
67
|
}
|
package/dist/engine/scanner.js
CHANGED
|
@@ -1,78 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.scanSource = scanSource;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const resolver_1 = require("./resolver");
|
|
6
9
|
const callResolver_1 = require("./callResolver");
|
|
7
|
-
|
|
8
|
-
function getCallName(expr) {
|
|
9
|
-
if (ts_morph_1.Node.isIdentifier(expr))
|
|
10
|
-
return expr.getText();
|
|
11
|
-
if (ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
12
|
-
return expr.getName();
|
|
13
|
-
return expr.getText();
|
|
14
|
-
}
|
|
15
|
-
function isTrackLike(name) {
|
|
16
|
-
return /(track|trackFeature)/i.test(name);
|
|
17
|
-
}
|
|
18
|
-
function pickArg(args) {
|
|
19
|
-
return (args.find(ts_morph_1.Node.isStringLiteral) ??
|
|
20
|
-
args.find(ts_morph_1.Node.isTemplateExpression) ??
|
|
21
|
-
args.find(ts_morph_1.Node.isObjectLiteralExpression) ??
|
|
22
|
-
args[0]);
|
|
23
|
-
}
|
|
24
|
-
function scanSource(source, checker) {
|
|
10
|
+
function scanSource(source, checker, visited = new Set()) {
|
|
25
11
|
const events = new Set();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
12
|
+
if (visited.has(source.fileName))
|
|
13
|
+
return events;
|
|
14
|
+
visited.add(source.fileName);
|
|
15
|
+
function visit(node) {
|
|
16
|
+
if (typescript_1.default.isCallExpression(node)) {
|
|
17
|
+
const name = getCallName(node.expression);
|
|
18
|
+
if (/(track|event|capture|send)/i.test(name)) {
|
|
19
|
+
let target = node.arguments[0];
|
|
20
|
+
const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
|
|
21
|
+
// 🔥 cross-file
|
|
22
|
+
if (fn) {
|
|
23
|
+
const sf = fn.getSourceFile();
|
|
24
|
+
if (!visited.has(sf.fileName)) {
|
|
25
|
+
scanSource(sf, checker, visited).forEach(e => events.add(e));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (target) {
|
|
29
|
+
const res = (0, resolver_1.resolveNodeValue)(target, checker);
|
|
30
|
+
res?.values.forEach(v => {
|
|
31
|
+
if (v && v.length > 1 && !v.includes(" ")) {
|
|
32
|
+
events.add(v);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
41
36
|
}
|
|
42
37
|
}
|
|
43
|
-
|
|
44
|
-
if (isValid(value))
|
|
45
|
-
events.add(value);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
// JSX
|
|
49
|
-
const jsx = [
|
|
50
|
-
...source.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxOpeningElement),
|
|
51
|
-
...source.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxSelfClosingElement),
|
|
52
|
-
];
|
|
53
|
-
for (const el of jsx) {
|
|
54
|
-
for (const attr of el.getAttributes()) {
|
|
55
|
-
if (!ts_morph_1.Node.isJsxAttribute(attr))
|
|
56
|
-
continue;
|
|
57
|
-
const name = attr.getNameNode().getText();
|
|
58
|
-
if (!/(event|name|action)/i.test(name))
|
|
59
|
-
continue;
|
|
60
|
-
const init = attr.getInitializer();
|
|
61
|
-
if (!init)
|
|
62
|
-
continue;
|
|
63
|
-
const expr = ts_morph_1.Node.isJsxExpression(init)
|
|
64
|
-
? init.getExpression()
|
|
65
|
-
: init;
|
|
66
|
-
if (!expr)
|
|
67
|
-
continue;
|
|
68
|
-
(0, extract_1.extractEventsFromArgs)([expr], checker).forEach(({ value }) => {
|
|
69
|
-
if (isValid(value))
|
|
70
|
-
events.add(value);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
38
|
+
typescript_1.default.forEachChild(node, visit);
|
|
73
39
|
}
|
|
40
|
+
visit(source);
|
|
74
41
|
return events;
|
|
75
42
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
43
|
+
function getCallName(expr) {
|
|
44
|
+
if (typescript_1.default.isIdentifier(expr))
|
|
45
|
+
return expr.text;
|
|
46
|
+
if (typescript_1.default.isPropertyAccessExpression(expr)) {
|
|
47
|
+
return expr.name.text;
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
78
50
|
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from "typescript";
|
|
2
2
|
|
|
3
|
-
export function resolveFunctionFromCall(
|
|
4
|
-
|
|
3
|
+
export function resolveFunctionFromCall(
|
|
4
|
+
expr: ts.Expression,
|
|
5
|
+
checker: ts.TypeChecker
|
|
6
|
+
): ts.FunctionLikeDeclaration | null {
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
let symbol = checker.getSymbolAtLocation(expr);
|
|
9
|
+
|
|
10
|
+
if (!symbol && ts.isPropertyAccessExpression(expr)) {
|
|
11
|
+
symbol = checker.getSymbolAtLocation(expr.name);
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
if (!symbol) return null;
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
17
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
18
|
+
}
|
|
13
19
|
|
|
14
|
-
for (const
|
|
15
|
-
if (
|
|
20
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
21
|
+
if (
|
|
22
|
+
ts.isFunctionDeclaration(decl) ||
|
|
23
|
+
ts.isMethodDeclaration(decl) ||
|
|
24
|
+
ts.isFunctionExpression(decl) ||
|
|
25
|
+
ts.isArrowFunction(decl)
|
|
26
|
+
) {
|
|
27
|
+
return decl;
|
|
28
|
+
}
|
|
16
29
|
|
|
17
|
-
if (
|
|
18
|
-
const init =
|
|
30
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
31
|
+
const init = decl.initializer;
|
|
19
32
|
|
|
20
33
|
if (
|
|
21
|
-
init
|
|
22
|
-
|
|
23
|
-
Node.isFunctionExpression(init))
|
|
34
|
+
ts.isArrowFunction(init) ||
|
|
35
|
+
ts.isFunctionExpression(init)
|
|
24
36
|
) {
|
|
25
37
|
return init;
|
|
26
38
|
}
|
|
27
39
|
}
|
|
28
|
-
|
|
29
|
-
if (Node.isMethodDeclaration(d)) return d;
|
|
30
|
-
|
|
31
|
-
if (Node.isFunctionExpression(d) || Node.isArrowFunction(d)) {
|
|
32
|
-
return d;
|
|
33
|
-
}
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
return null;
|
package/src/engine/engine.ts
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TSService } from "./languageService";
|
|
2
2
|
import { scanSource } from "./scanner";
|
|
3
|
+
import { DependencyGraph } from "./graph";
|
|
4
|
+
import { hash } from "./hash";
|
|
3
5
|
|
|
4
6
|
export class EventraEngine {
|
|
5
|
-
private
|
|
7
|
+
private ts: TSService;
|
|
8
|
+
private graph = new DependencyGraph();
|
|
9
|
+
|
|
6
10
|
private fileEvents = new Map<string, Set<string>>();
|
|
11
|
+
private fileHash = new Map<string, string>();
|
|
7
12
|
|
|
8
|
-
constructor(
|
|
9
|
-
this.
|
|
13
|
+
constructor(root: string) {
|
|
14
|
+
this.ts = new TSService(root);
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
scanFile(file: string, content: string) {
|
|
13
|
-
const
|
|
14
|
-
|
|
18
|
+
const h = hash(content);
|
|
19
|
+
|
|
20
|
+
if (this.fileHash.get(file) === h) {
|
|
21
|
+
return this.fileEvents.get(file) ?? new Set();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.fileHash.set(file, h);
|
|
25
|
+
|
|
26
|
+
this.ts.updateFile(file, content);
|
|
27
|
+
|
|
28
|
+
const source = this.ts.getSourceFile(file);
|
|
29
|
+
const checker = this.ts.getChecker();
|
|
30
|
+
|
|
31
|
+
if (!source || !checker) return new Set();
|
|
32
|
+
|
|
33
|
+
this.graph.update(file, source);
|
|
15
34
|
|
|
16
35
|
const events = scanSource(source, checker);
|
|
17
36
|
|
|
18
37
|
this.fileEvents.set(file, events);
|
|
38
|
+
|
|
19
39
|
return events;
|
|
20
40
|
}
|
|
21
41
|
|
|
@@ -35,5 +55,6 @@ export class EventraEngine {
|
|
|
35
55
|
|
|
36
56
|
removeFile(file: string) {
|
|
37
57
|
this.fileEvents.delete(file);
|
|
58
|
+
this.fileHash.delete(file);
|
|
38
59
|
}
|
|
39
60
|
}
|
package/src/engine/graph.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from "typescript";
|
|
2
2
|
|
|
3
3
|
export class DependencyGraph {
|
|
4
4
|
private reverse = new Map<string, Set<string>>();
|
|
5
5
|
|
|
6
|
-
update(file: string, source: SourceFile) {
|
|
7
|
-
const
|
|
8
|
-
.
|
|
9
|
-
.map(i => i.getModuleSpecifierSourceFile()?.getFilePath())
|
|
10
|
-
.filter(Boolean) as string[];
|
|
6
|
+
update(file: string, source: ts.SourceFile) {
|
|
7
|
+
for (const stmt of source.statements) {
|
|
8
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
11
|
+
|
|
12
|
+
if (!this.reverse.has(module)) {
|
|
13
|
+
this.reverse.set(module, new Set());
|
|
15
14
|
}
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
this.reverse.get(module)!.add(file);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
export class TSService {
|
|
5
|
+
private files = new Map<string, { version: number; content: string }>();
|
|
6
|
+
private service: ts.LanguageService;
|
|
7
|
+
|
|
8
|
+
constructor(private root: string) {
|
|
9
|
+
const host: ts.LanguageServiceHost = {
|
|
10
|
+
getScriptFileNames: () => [...this.files.keys()],
|
|
11
|
+
getScriptVersion: (file) =>
|
|
12
|
+
this.files.get(file)?.version.toString() ?? "0",
|
|
13
|
+
getScriptSnapshot: (file) => {
|
|
14
|
+
const f = this.files.get(file);
|
|
15
|
+
|
|
16
|
+
if (f) {
|
|
17
|
+
return ts.ScriptSnapshot.fromString(f.content);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(file)) {
|
|
21
|
+
return ts.ScriptSnapshot.fromString(
|
|
22
|
+
fs.readFileSync(file, "utf-8" as BufferEncoding)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return undefined;
|
|
27
|
+
},
|
|
28
|
+
getCurrentDirectory: () => this.root,
|
|
29
|
+
getCompilationSettings: () => ({
|
|
30
|
+
allowJs: true,
|
|
31
|
+
jsx: ts.JsxEmit.Preserve,
|
|
32
|
+
moduleResolution: ts.ModuleResolutionKind.Node16,
|
|
33
|
+
target: ts.ScriptTarget.ESNext,
|
|
34
|
+
esModuleInterop: true,
|
|
35
|
+
allowSyntheticDefaultImports: true,
|
|
36
|
+
}),
|
|
37
|
+
getDefaultLibFileName: (opts) => ts.getDefaultLibFilePath(opts),
|
|
38
|
+
|
|
39
|
+
readFile: (fileName: string, encoding?: string) => {
|
|
40
|
+
try {
|
|
41
|
+
return fs.readFileSync(fileName, encoding as BufferEncoding);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
fileExists: fs.existsSync,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.service = ts.createLanguageService(host);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
updateFile(file: string, content: string) {
|
|
54
|
+
const prev = this.files.get(file);
|
|
55
|
+
|
|
56
|
+
this.files.set(file, {
|
|
57
|
+
content,
|
|
58
|
+
version: prev ? prev.version + 1 : 1,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getProgram() {
|
|
63
|
+
return this.service.getProgram();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getChecker() {
|
|
67
|
+
return this.getProgram()?.getTypeChecker();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getSourceFile(file: string) {
|
|
71
|
+
return this.getProgram()?.getSourceFile(file);
|
|
72
|
+
}
|
|
73
|
+
}
|