@eventra_dev/eventra-cli 0.1.6 → 0.1.7
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/callResolver.js +58 -5
- package/dist/engine/engine.js +26 -19
- package/dist/engine/graph.js +39 -20
- package/dist/engine/resolver.js +5 -0
- package/dist/engine/scanner.js +163 -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/callResolver.ts +75 -8
- package/src/engine/engine.ts +30 -21
- package/src/engine/graph.ts +49 -24
- package/src/engine/resolver.ts +10 -0
- package/src/engine/scanner.ts +223 -72
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}`));
|
|
@@ -1,25 +1,92 @@
|
|
|
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
|
+
): ts.FunctionLikeDeclaration | null {
|
|
20
|
+
if (visited.has(symbol)) return null;
|
|
21
|
+
visited.add(symbol);
|
|
22
|
+
|
|
23
|
+
// alias unwrap
|
|
24
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
25
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
26
|
+
return resolveFromSymbol(aliased, checker, visited);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const decls = symbol.getDeclarations() ?? [];
|
|
30
|
+
|
|
31
|
+
for (const decl of decls) {
|
|
32
|
+
if (isFunctionLike(decl)) {
|
|
33
|
+
return decl;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (ts.isMethodSignature(decl)) {
|
|
37
|
+
const parent = decl.parent;
|
|
38
|
+
|
|
39
|
+
if (ts.isInterfaceDeclaration(parent) || ts.isTypeLiteralNode(parent)) {
|
|
40
|
+
const name = decl.name.getText();
|
|
41
|
+
|
|
42
|
+
const type = checker.getTypeAtLocation(parent);
|
|
43
|
+
const props = type.getProperties();
|
|
44
|
+
|
|
45
|
+
for (const prop of props) {
|
|
46
|
+
if (prop.name !== name) continue;
|
|
47
|
+
|
|
48
|
+
const resolved = resolveFromSymbol(prop, checker, visited);
|
|
49
|
+
if (resolved) return resolved;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// fallback
|
|
55
|
+
const s = checker.getSymbolAtLocation(decl);
|
|
56
|
+
if (s) {
|
|
57
|
+
const resolved = resolveFromSymbol(s, checker, visited);
|
|
58
|
+
if (resolved) return resolved;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
3
65
|
export function resolveFunctionFromCall(
|
|
4
66
|
expr: ts.Expression,
|
|
5
67
|
checker: ts.TypeChecker
|
|
6
68
|
): ts.FunctionLikeDeclaration | null {
|
|
7
|
-
|
|
8
69
|
const type = checker.getTypeAtLocation(expr);
|
|
9
70
|
const signatures = type.getCallSignatures();
|
|
10
71
|
|
|
11
72
|
for (const sig of signatures) {
|
|
12
73
|
const decl = sig.getDeclaration();
|
|
13
74
|
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
ts.isMethodDeclaration(decl) ||
|
|
18
|
-
ts.isFunctionExpression(decl) ||
|
|
19
|
-
ts.isArrowFunction(decl))
|
|
20
|
-
) {
|
|
75
|
+
if (!decl) continue;
|
|
76
|
+
|
|
77
|
+
if (isFunctionLike(decl)) {
|
|
21
78
|
return decl;
|
|
22
79
|
}
|
|
80
|
+
|
|
81
|
+
// MethodSignature case
|
|
82
|
+
if (ts.isMethodSignature(decl)) {
|
|
83
|
+
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
84
|
+
|
|
85
|
+
if (symbol) {
|
|
86
|
+
const resolved = resolveFromSymbol(symbol, checker);
|
|
87
|
+
if (resolved) return resolved;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
23
90
|
}
|
|
24
91
|
|
|
25
92
|
return null;
|
package/src/engine/engine.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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";
|
|
5
6
|
|
|
6
7
|
export class EventraEngine {
|
|
7
8
|
private ts: TSService;
|
|
8
9
|
private graph: DependencyGraph;
|
|
9
10
|
|
|
10
|
-
private
|
|
11
|
+
private fileResults = new Map<string, ScanResult>();
|
|
11
12
|
private fileHash = new Map<string, string>();
|
|
12
13
|
|
|
13
14
|
constructor(root: string) {
|
|
@@ -17,37 +18,46 @@ export class EventraEngine {
|
|
|
17
18
|
this.graph = new DependencyGraph(baseUrl, paths);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
scanFile(file: string, content: string) {
|
|
21
|
+
scanFile(file: string, content: string, config: EventraConfig): ScanResult {
|
|
21
22
|
const h = hash(content);
|
|
22
23
|
|
|
23
|
-
// cache
|
|
24
24
|
if (this.fileHash.get(file) === h) {
|
|
25
|
-
return
|
|
25
|
+
return (
|
|
26
|
+
this.fileResults.get(file) ?? {
|
|
27
|
+
events: new Set(),
|
|
28
|
+
detectedFunctionWrappers: new Set(),
|
|
29
|
+
detectedComponentWrappers: new Map(),
|
|
30
|
+
}
|
|
31
|
+
);
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
this.fileHash.set(file, h);
|
|
29
|
-
|
|
30
35
|
this.ts.updateFile(file, content);
|
|
31
36
|
|
|
32
37
|
const source = this.ts.getSourceFile(file);
|
|
33
38
|
const checker = this.ts.getChecker();
|
|
34
39
|
|
|
35
|
-
if (!source || !checker)
|
|
40
|
+
if (!source || !checker) {
|
|
41
|
+
return {
|
|
42
|
+
events: new Set(),
|
|
43
|
+
detectedFunctionWrappers: new Set(),
|
|
44
|
+
detectedComponentWrappers: new Map(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
36
47
|
|
|
37
|
-
// обновляем graph
|
|
38
48
|
this.graph.update(file, source);
|
|
39
49
|
|
|
40
|
-
const
|
|
50
|
+
const res = scanSource(source, checker, config);
|
|
41
51
|
|
|
42
|
-
this.
|
|
52
|
+
this.fileResults.set(file, res);
|
|
43
53
|
|
|
44
|
-
return
|
|
54
|
+
return res;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
updateFile(file: string, content: string) {
|
|
48
|
-
const
|
|
57
|
+
updateFile(file: string, content: string, config: EventraConfig) {
|
|
58
|
+
const res = this.scanFile(file, content, config);
|
|
49
59
|
|
|
50
|
-
const dependents = this.graph.
|
|
60
|
+
const dependents = this.graph.getAllDependentsDeep(file);
|
|
51
61
|
|
|
52
62
|
for (const dep of dependents) {
|
|
53
63
|
const source = this.ts.getSourceFile(dep);
|
|
@@ -55,26 +65,25 @@ export class EventraEngine {
|
|
|
55
65
|
|
|
56
66
|
if (!source || !checker) continue;
|
|
57
67
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
this.fileEvents.set(dep, ev);
|
|
68
|
+
const r = scanSource(source, checker, config);
|
|
69
|
+
this.fileResults.set(dep, r);
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
return
|
|
72
|
+
return res;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
getAllEvents() {
|
|
67
76
|
const all = new Set<string>();
|
|
68
77
|
|
|
69
|
-
for (const
|
|
70
|
-
|
|
78
|
+
for (const r of this.fileResults.values()) {
|
|
79
|
+
r.events.forEach(e => all.add(e));
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
return [...all];
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
removeFile(file: string) {
|
|
77
|
-
this.
|
|
86
|
+
this.fileResults.delete(file);
|
|
78
87
|
this.fileHash.delete(file);
|
|
79
88
|
this.graph.remove(file);
|
|
80
89
|
}
|
package/src/engine/graph.ts
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "fs";
|
|
|
5
5
|
export class DependencyGraph {
|
|
6
6
|
private reverse = new Map<string, Set<string>>();
|
|
7
7
|
private exports = new Map<string, Set<string>>();
|
|
8
|
+
private fileCache = new Map<string, string | null>();
|
|
8
9
|
|
|
9
10
|
constructor(
|
|
10
11
|
private baseUrl: string,
|
|
@@ -15,42 +16,43 @@ export class DependencyGraph {
|
|
|
15
16
|
return path.resolve(file);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
// TS-like resolver
|
|
19
19
|
private resolveModule(module: string, from: string): string | null {
|
|
20
|
-
// relative
|
|
21
20
|
if (module.startsWith(".")) {
|
|
22
21
|
const abs = path.resolve(path.dirname(from), module);
|
|
23
22
|
return this.resolveFile(abs);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
// paths mapping
|
|
27
25
|
for (const key in this.paths) {
|
|
28
|
-
const pattern = key.replace(
|
|
26
|
+
const pattern = key.replace(/\*/g, "(.*)");
|
|
29
27
|
const regex = new RegExp("^" + pattern + "$");
|
|
30
28
|
const match = module.match(regex);
|
|
31
29
|
|
|
32
30
|
if (!match) continue;
|
|
33
31
|
|
|
34
|
-
const wildcard = match
|
|
32
|
+
const wildcard = match.slice(1);
|
|
35
33
|
|
|
36
34
|
for (const target of this.paths[key]) {
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
let replaced = target;
|
|
36
|
+
|
|
37
|
+
wildcard.forEach(w => {
|
|
38
|
+
replaced = replaced.replace("*", w);
|
|
39
|
+
});
|
|
39
40
|
|
|
41
|
+
const abs = path.resolve(this.baseUrl, replaced);
|
|
40
42
|
const resolved = this.resolveFile(abs);
|
|
41
43
|
if (resolved) return resolved;
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
// baseUrl fallback
|
|
46
47
|
const abs = path.resolve(this.baseUrl, module);
|
|
47
|
-
|
|
48
|
-
if (resolved) return resolved;
|
|
49
|
-
|
|
50
|
-
return null;
|
|
48
|
+
return this.resolveFile(abs);
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
private resolveFile(base: string): string | null {
|
|
52
|
+
if (this.fileCache.has(base)) {
|
|
53
|
+
return this.fileCache.get(base)!;
|
|
54
|
+
}
|
|
55
|
+
|
|
54
56
|
const tryFiles = [
|
|
55
57
|
base,
|
|
56
58
|
base + ".ts",
|
|
@@ -64,10 +66,13 @@ export class DependencyGraph {
|
|
|
64
66
|
|
|
65
67
|
for (const f of tryFiles) {
|
|
66
68
|
if (fs.existsSync(f)) {
|
|
67
|
-
|
|
69
|
+
const normalized = this.normalize(f);
|
|
70
|
+
this.fileCache.set(base, normalized);
|
|
71
|
+
return normalized;
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
this.fileCache.set(base, null);
|
|
71
76
|
return null;
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -77,11 +82,10 @@ export class DependencyGraph {
|
|
|
77
82
|
this.exports.set(normalizedFile, new Set());
|
|
78
83
|
|
|
79
84
|
for (const stmt of source.statements) {
|
|
80
|
-
// IMPORT
|
|
81
85
|
if (ts.isImportDeclaration(stmt)) {
|
|
82
86
|
const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
83
|
-
|
|
84
87
|
const resolved = this.resolveModule(module, normalizedFile);
|
|
88
|
+
|
|
85
89
|
if (!resolved) continue;
|
|
86
90
|
|
|
87
91
|
if (!this.reverse.has(resolved)) {
|
|
@@ -91,18 +95,13 @@ export class DependencyGraph {
|
|
|
91
95
|
this.reverse.get(resolved)!.add(normalizedFile);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
// EXPORT *
|
|
95
98
|
if (ts.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
|
|
96
99
|
const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
97
|
-
|
|
98
100
|
const resolved = this.resolveModule(module, normalizedFile);
|
|
99
|
-
if (!resolved) continue;
|
|
100
101
|
|
|
101
|
-
if (!
|
|
102
|
-
this.exports.set(normalizedFile, new Set());
|
|
103
|
-
}
|
|
102
|
+
if (!resolved) continue;
|
|
104
103
|
|
|
105
|
-
this.exports.get(normalizedFile)
|
|
104
|
+
this.exports.get(normalizedFile)?.add(resolved);
|
|
106
105
|
|
|
107
106
|
if (!this.reverse.has(resolved)) {
|
|
108
107
|
this.reverse.set(resolved, new Set());
|
|
@@ -113,17 +112,43 @@ export class DependencyGraph {
|
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
getAllDependentsDeep(file: string): string[] {
|
|
116
|
+
const start = this.normalize(file);
|
|
117
|
+
const visited = new Set<string>();
|
|
118
|
+
const result = new Set<string>();
|
|
119
|
+
|
|
120
|
+
const walk = (f: string) => {
|
|
121
|
+
if (visited.has(f)) return;
|
|
122
|
+
visited.add(f);
|
|
123
|
+
|
|
124
|
+
const deps = this.reverse.get(f);
|
|
125
|
+
deps?.forEach(d => {
|
|
126
|
+
result.add(d);
|
|
127
|
+
walk(d);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const ex = this.exports.get(f);
|
|
131
|
+
ex?.forEach(e => walk(e));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
walk(start);
|
|
135
|
+
|
|
136
|
+
return [...result];
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
remove(file: string) {
|
|
121
140
|
const normalized = this.normalize(file);
|
|
122
141
|
|
|
142
|
+
this.reverse.delete(normalized);
|
|
123
143
|
this.exports.delete(normalized);
|
|
144
|
+
this.fileCache.delete(normalized);
|
|
124
145
|
|
|
125
146
|
for (const set of this.reverse.values()) {
|
|
126
147
|
set.delete(normalized);
|
|
127
148
|
}
|
|
149
|
+
|
|
150
|
+
for (const set of this.exports.values()) {
|
|
151
|
+
set.delete(normalized);
|
|
152
|
+
}
|
|
128
153
|
}
|
|
129
154
|
}
|
package/src/engine/resolver.ts
CHANGED
|
@@ -53,6 +53,16 @@ export function resolveNodeValue(
|
|
|
53
53
|
if (ts.isIdentifier(node)) {
|
|
54
54
|
let symbol = checker.getSymbolAtLocation(node);
|
|
55
55
|
if (!symbol) return null;
|
|
56
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
57
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
58
|
+
return resolveNodeValue(
|
|
59
|
+
decl.initializer,
|
|
60
|
+
checker,
|
|
61
|
+
paramMap,
|
|
62
|
+
seen
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
56
66
|
|
|
57
67
|
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
58
68
|
|