@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.
@@ -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 = 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
  }
@@ -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
- decl &&
16
- (ts.isFunctionDeclaration(decl) ||
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;
@@ -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 fileEvents = new Map<string, Set<string>>();
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 this.fileEvents.get(file) ?? new Set();
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) return new Set();
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 events = scanSource(source, checker);
50
+ const res = scanSource(source, checker, config);
41
51
 
42
- this.fileEvents.set(file, events);
52
+ this.fileResults.set(file, res);
43
53
 
44
- return events;
54
+ return res;
45
55
  }
46
56
 
47
- updateFile(file: string, content: string) {
48
- const events = this.scanFile(file, content);
57
+ updateFile(file: string, content: string, config: EventraConfig) {
58
+ const res = this.scanFile(file, content, config);
49
59
 
50
- const dependents = this.graph.getDependents(file);
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 ev = scanSource(source, checker);
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 events;
72
+ return res;
64
73
  }
65
74
 
66
75
  getAllEvents() {
67
76
  const all = new Set<string>();
68
77
 
69
- for (const set of this.fileEvents.values()) {
70
- set.forEach(e => all.add(e));
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.fileEvents.delete(file);
86
+ this.fileResults.delete(file);
78
87
  this.fileHash.delete(file);
79
88
  this.graph.remove(file);
80
89
  }
@@ -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[1] || "";
32
+ const wildcard = match.slice(1);
35
33
 
36
34
  for (const target of this.paths[key]) {
37
- const replaced = target.replace("*", wildcard);
38
- const abs = path.resolve(this.baseUrl, replaced);
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
- const resolved = this.resolveFile(abs);
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
- return this.normalize(f);
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 (!this.exports.has(normalizedFile)) {
102
- this.exports.set(normalizedFile, new Set());
103
- }
102
+ if (!resolved) continue;
104
103
 
105
- this.exports.get(normalizedFile)!.add(resolved);
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
- getDependents(file: string) {
117
- return [...(this.reverse.get(this.normalize(file)) ?? [])];
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
  }
@@ -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