@eventra_dev/eventra-cli 0.1.33 → 0.2.1

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.
@@ -9,92 +9,39 @@ import { EventraEngine } from "../engine/engine";
9
9
  import { processFile } from "../utils/processFile";
10
10
  import { getVirtualFile } from "../utils/getVirtualFile";
11
11
 
12
- type QueueItem = {
13
- real: string;
14
- virtual: string;
15
- };
16
-
17
- function normalize(file: string) {
18
- return path.resolve(file).replace(/\\/g, "/");
19
- }
20
-
21
12
  export async function watch() {
22
13
  const config = await loadConfig();
23
14
  if (!config) return;
24
15
 
25
16
  console.log(chalk.blue("Watching...\n"));
26
17
 
27
- const files = await fg(config.sync.include, {
28
- ignore: config.sync.exclude,
29
- absolute: true,
30
- });
31
-
32
18
  const engine = new EventraEngine(process.cwd());
19
+ const fileDeps = new Map<string, Set<string>>();
20
+ const cache = new Map<string, { content: string; deps: string[] }>();
33
21
  const watchedDeps = new Set<string>();
34
22
 
35
- const cache = new Map<
36
- string,
37
- { content: string; deps: string[] }
38
- >();
23
+ const normalize = (f: string) =>
24
+ path.resolve(f).replace(/\\/g, "/");
39
25
 
40
- const toScan = new Set<string>();
26
+ // INITIAL LOAD
27
+ const files = await fg(config.sync.include, {
28
+ ignore: config.sync.exclude,
29
+ absolute: true,
30
+ });
41
31
 
42
- // COLLECT + CACHE
43
32
  for (const file of files) {
44
33
  try {
45
34
  const abs = normalize(file);
46
35
  const raw = await fs.readFile(abs, "utf-8");
47
36
 
48
- if (raw.length > 2_000_000) continue;
49
-
50
37
  const parsed = processFile(abs, raw);
51
38
 
52
39
  cache.set(abs, parsed);
53
- toScan.add(abs);
54
40
 
55
- for (const dep of parsed.deps) {
56
- toScan.add(normalize(dep));
57
- }
41
+ engine.preloadFile(getVirtualFile(abs), parsed.content);
42
+ engine.updateFile(getVirtualFile(abs), parsed.content, config);
58
43
 
59
- } catch {
60
- console.log(chalk.gray(`skip: ${file}`));
61
- }
62
- }
63
-
64
- // PRELOAD
65
- for (const file of toScan) {
66
- try {
67
- let parsed = cache.get(file);
68
-
69
- if (!parsed) {
70
- const raw = await fs.readFile(file, "utf-8");
71
- parsed = processFile(file, raw);
72
- cache.set(file, parsed);
73
- }
74
-
75
- const virtual = getVirtualFile(file);
76
- engine.preloadFile?.(virtual, parsed.content);
77
-
78
- } catch {}
79
- }
80
-
81
- // INITIAL SCAN
82
- for (const file of toScan) {
83
- try {
84
- let parsed = cache.get(file);
85
-
86
- if (!parsed) {
87
- const raw = await fs.readFile(file, "utf-8");
88
- parsed = processFile(file, raw);
89
- }
90
-
91
- const virtual = getVirtualFile(file);
92
-
93
- engine.scanFile(virtual, parsed.content, config);
94
-
95
- for (const dep of parsed.deps) {
96
- watchedDeps.add(normalize(dep));
97
- }
44
+ fileDeps.set(abs, new Set(parsed.deps.map(d => normalize(d))));
98
45
 
99
46
  } catch {}
100
47
  }
@@ -102,152 +49,95 @@ export async function watch() {
102
49
  config.events = engine.getAllEvents().sort();
103
50
  await saveConfig(config);
104
51
 
105
- console.log(
106
- chalk.gray(`Initial: ${config.events.length} events\n`)
52
+ console.log(chalk.gray(`Initial: ${config.events.length}\n`));
53
+
54
+ // WATCHER
55
+ const watcher = chokidar.watch(
56
+ [...files.map(normalize), ...watchedDeps],
57
+ { ignoreInitial: true }
107
58
  );
108
59
 
109
- // QUEUE
110
- let locked = false;
111
- const queued = new Map<string, QueueItem>();
60
+ const queue = new Map<string, boolean>();
112
61
  let timer: NodeJS.Timeout | null = null;
113
62
 
114
63
  const run = async () => {
115
- if (locked) return;
116
- locked = true;
64
+ const batch = [...queue.keys()];
65
+ queue.clear();
117
66
 
118
- const batch = [...queued.values()];
119
- queued.clear();
120
-
121
- for (const item of batch) {
67
+ for (const file of batch) {
122
68
  try {
123
- const raw = await fs.readFile(item.real, "utf-8");
124
-
125
- if (raw.length > 2_000_000) continue;
69
+ const raw = await fs.readFile(file, "utf-8");
70
+ const parsed = processFile(file, raw);
126
71
 
127
- const parsed = processFile(item.real, raw);
128
- cache.set(item.real, parsed);
72
+ cache.set(file, parsed);
129
73
 
130
- engine.updateFile(item.virtual, parsed.content, config);
74
+ engine.updateFile(
75
+ getVirtualFile(file),
76
+ parsed.content,
77
+ config
78
+ );
131
79
 
132
- // DEPENDENCY DIFF
133
- const nextDeps = new Set<string>();
80
+ //dependency diff
81
+ const abs = normalize(file);
134
82
 
135
- for (const dep of parsed.deps) {
136
- nextDeps.add(normalize(dep));
137
- }
83
+ const prevDeps = fileDeps.get(abs) ?? new Set();
84
+ const nextDeps = new Set(parsed.deps.map(d => normalize(d)));
138
85
 
139
86
  // add new deps
140
87
  for (const dep of nextDeps) {
141
- if (!watchedDeps.has(dep)) {
88
+ if (!prevDeps.has(dep)) {
142
89
  watcher.add(dep);
143
90
  watchedDeps.add(dep);
144
91
  }
145
92
  }
146
93
 
147
94
  // remove old deps
148
- for (const dep of [...watchedDeps]) {
95
+ for (const dep of prevDeps) {
149
96
  if (!nextDeps.has(dep)) {
150
97
  watcher.unwatch(dep);
151
98
  watchedDeps.delete(dep);
152
99
  }
153
100
  }
154
101
 
155
- // preload deps
156
- for (const dep of nextDeps) {
157
- try {
158
- const raw = await fs.readFile(dep, "utf-8");
159
- const parsedDep = processFile(dep, raw);
160
-
161
- cache.set(dep, parsedDep);
162
-
163
- const virtual = getVirtualFile(dep);
164
-
165
- engine.updateFile(virtual, parsedDep.content, config);
166
- } catch {}
167
- }
102
+ fileDeps.set(abs, nextDeps);
168
103
 
169
104
  } catch {
170
- console.log(chalk.gray(`skip: ${item.real}`));
105
+ console.log(chalk.gray(`skip: ${file}`));
171
106
  }
172
107
  }
173
108
 
174
- // DIFF EVENTS
175
- const next = engine.getAllEvents();
176
-
177
- const added = next.filter(e => !config.events.includes(e));
178
- const removed = config.events.filter(e => !next.includes(e));
179
-
180
- if (added.length || removed.length) {
181
- added.forEach(e => console.log(chalk.green(`+ ${e}`)));
182
- removed.forEach(e => console.log(chalk.red(`- ${e}`)));
109
+ const next = engine.getAllEvents().sort();
110
+ config.events = next;
183
111
 
184
- config.events = next.sort();
185
- await saveConfig(config);
186
-
187
- console.log(
188
- chalk.gray(`Total: ${config.events.length} events\n`)
189
- );
190
- }
112
+ await saveConfig(config);
191
113
 
192
- locked = false;
114
+ console.log(chalk.green(`Updated: ${next.length}`));
193
115
  };
194
116
 
195
- // SCHEDULER
196
117
  const schedule = (file: string) => {
197
118
  const abs = normalize(file);
198
- const virtual = getVirtualFile(abs);
199
-
200
- console.log(chalk.gray(`change: ${file}`));
201
-
202
- queued.set(abs, { real: abs, virtual });
119
+ queue.set(abs, true);
203
120
 
204
121
  if (timer) clearTimeout(timer);
205
122
  timer = setTimeout(run, 200);
206
123
  };
207
124
 
208
- // WATCHER
209
- const watcher = chokidar.watch(config.sync.include, {
210
- ignored: config.sync.exclude,
211
- ignoreInitial: true,
212
- persistent: true,
213
- });
214
-
215
125
  watcher
216
- .on("add", schedule)
217
126
  .on("change", schedule)
127
+ .on("add", schedule)
218
128
  .on("unlink", async (file) => {
219
129
  const abs = normalize(file);
220
130
 
221
- console.log(chalk.gray(`remove: ${file}`));
222
-
223
- const virtual = getVirtualFile(abs);
224
-
225
- engine.removeFile(virtual, config);
131
+ engine.removeFile(getVirtualFile(abs), config);
226
132
 
227
- watcher.unwatch(abs);
228
- watchedDeps.delete(abs);
229
-
230
- const next = engine.getAllEvents();
231
-
232
- config.events = next.sort();
133
+ const next = engine.getAllEvents().sort();
134
+ config.events = next;
233
135
  await saveConfig(config);
234
136
 
235
- console.log(
236
- chalk.gray(`Total: ${config.events.length} events\n`)
237
- );
238
- })
239
- .on("error", (err) => {
240
- console.error(chalk.red("Watcher error:"), err);
137
+ console.log(chalk.red(`Removed → ${next.length}`));
241
138
  });
242
139
 
243
- // WATCH DEPS INIT
244
- for (const dep of watchedDeps) {
245
- watcher.add(dep);
246
- }
247
-
248
- // EXIT
249
140
  process.on("SIGINT", async () => {
250
- console.log(chalk.yellow("\nStopping watcher..."));
251
141
  await watcher.close();
252
142
  process.exit(0);
253
143
  });
@@ -1,200 +1,65 @@
1
1
  import ts from "typescript";
2
2
 
3
- // HELPERS
4
- function isFunctionLike(
5
- node: ts.Node
6
- ): node is ts.FunctionLikeDeclaration {
3
+ function isFn(node: ts.Node): node is ts.FunctionLikeDeclaration {
7
4
  return (
8
5
  ts.isFunctionDeclaration(node) ||
9
6
  ts.isMethodDeclaration(node) ||
10
- ts.isConstructorDeclaration(node) ||
11
7
  ts.isFunctionExpression(node) ||
12
8
  ts.isArrowFunction(node)
13
9
  );
14
10
  }
15
11
 
16
- // symbol → function
17
12
  function resolveFromSymbol(
18
13
  symbol: ts.Symbol,
19
14
  checker: ts.TypeChecker,
20
- visited = new Set<ts.Symbol>(),
21
- depth = 0
15
+ seen = new Set<ts.Symbol>()
22
16
  ): ts.FunctionLikeDeclaration | null {
23
- if (!symbol) return null;
24
- if (visited.has(symbol)) return null;
25
- if (depth > 10) return null;
17
+ if (seen.has(symbol)) return null;
18
+ seen.add(symbol);
26
19
 
27
- visited.add(symbol);
28
-
29
- // Alias unwrap (import/export)
30
20
  if (symbol.flags & ts.SymbolFlags.Alias) {
31
- try {
32
- const aliased = checker.getAliasedSymbol(symbol);
33
- return resolveFromSymbol(aliased, checker, visited, depth + 1);
34
- } catch {}
21
+ return resolveFromSymbol(checker.getAliasedSymbol(symbol), checker, seen);
35
22
  }
36
23
 
37
- // Declarations
38
- const decls = symbol.getDeclarations() ?? [];
24
+ for (const d of symbol.getDeclarations() ?? []) {
25
+ if (isFn(d)) return d;
39
26
 
40
- for (const decl of decls) {
41
- /**
42
- * direct function
43
- */
44
- if (isFunctionLike(decl)) {
45
- return decl;
46
- }
27
+ if (ts.isVariableDeclaration(d) && d.initializer) {
28
+ if (isFn(d.initializer)) return d.initializer;
47
29
 
48
- // VARIABLE → FUNCTION const fn = () => {}
49
- if (ts.isVariableDeclaration(decl) && decl.initializer) {
50
- if (isFunctionLike(decl.initializer)) {
51
- return decl.initializer;
30
+ const s = checker.getSymbolAtLocation(d.initializer);
31
+ if (s) {
32
+ const r = resolveFromSymbol(s, checker, seen);
33
+ if (r) return r;
52
34
  }
53
-
54
- const innerSymbol = checker.getSymbolAtLocation(decl.initializer);
55
- if (innerSymbol && innerSymbol !== symbol) {
56
- const resolved = resolveFromSymbol(
57
- innerSymbol,
58
- checker,
59
- visited,
60
- depth + 1
61
- );
62
- if (resolved) return resolved;
63
- }
64
- }
65
-
66
- // CLASS METHOD
67
- if (ts.isMethodDeclaration(decl)) {
68
- return decl;
69
- }
70
-
71
- // interface / type method
72
- if (ts.isMethodSignature(decl)) {
73
- const parent = decl.parent;
74
-
75
- if (
76
- ts.isInterfaceDeclaration(parent) ||
77
- ts.isTypeLiteralNode(parent)
78
- ) {
79
- const type = checker.getTypeAtLocation(parent);
80
- const props = type.getProperties();
81
-
82
- for (const prop of props) {
83
- const resolved = resolveFromSymbol(
84
- prop,
85
- checker,
86
- visited,
87
- depth + 1
88
- );
89
- if (resolved) return resolved;
90
- }
91
- }
92
- }
93
-
94
- // fallback recursion
95
- const inner = checker.getSymbolAtLocation(decl);
96
- if (inner && inner !== symbol) {
97
- const resolved = resolveFromSymbol(
98
- inner,
99
- checker,
100
- visited,
101
- depth + 1
102
- );
103
- if (resolved) return resolved;
104
- }
105
- }
106
-
107
- // Exports (re-export)
108
- const exports = (symbol as any).exports;
109
- if (exports) {
110
- for (const exp of exports.values()) {
111
- const resolved = resolveFromSymbol(
112
- exp,
113
- checker,
114
- visited,
115
- depth + 1
116
- );
117
- if (resolved) return resolved;
118
35
  }
119
36
  }
120
37
 
121
38
  return null;
122
39
  }
123
40
 
124
- // MAIN FUNCTION
125
41
  export function resolveFunctionFromCall(
126
42
  expr: ts.Expression,
127
43
  checker: ts.TypeChecker
128
44
  ): ts.FunctionLikeDeclaration | null {
129
- // obj.method()
45
+ // foo.bar()
130
46
  if (ts.isPropertyAccessExpression(expr)) {
131
- let symbol = checker.getSymbolAtLocation(expr.name);
132
-
133
- if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
134
- try {
135
- symbol = checker.getAliasedSymbol(symbol);
136
- } catch {}
137
- }
138
-
139
- if (symbol) {
140
- const resolved = resolveFromSymbol(symbol, checker);
141
- if (resolved) return resolved;
47
+ const s = checker.getSymbolAtLocation(expr.name);
48
+ if (s) {
49
+ const r = resolveFromSymbol(s, checker);
50
+ if (r) return r;
142
51
  }
143
52
  }
144
53
 
145
- // obj["method"]()
146
- if (ts.isElementAccessExpression(expr)) {
147
- const symbol = checker.getSymbolAtLocation(expr.argumentExpression);
148
-
149
- if (symbol) {
150
- const resolved = resolveFromSymbol(symbol, checker);
151
- if (resolved) return resolved;
152
- }
153
- }
154
-
155
- // Call signatures
54
+ // direct call
156
55
  const type = checker.getTypeAtLocation(expr);
157
- const signatures = type.getCallSignatures();
158
-
159
- for (const sig of signatures) {
160
- const decl = sig.getDeclaration();
161
- if (!decl) continue;
162
-
163
- if (isFunctionLike(decl)) {
164
- return decl;
165
- }
166
-
167
- if (ts.isMethodSignature(decl)) {
168
- const symbol = checker.getSymbolAtLocation(decl.name);
169
-
170
- if (symbol) {
171
- const resolved = resolveFromSymbol(symbol, checker);
172
- if (resolved) return resolved;
173
- }
174
- }
56
+ for (const sig of type.getCallSignatures()) {
57
+ const d = sig.getDeclaration();
58
+ if (d && isFn(d)) return d;
175
59
  }
176
60
 
177
- // Identifier fallback
178
- if (ts.isIdentifier(expr)) {
179
- let symbol = checker.getSymbolAtLocation(expr);
180
-
181
- if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
182
- try {
183
- symbol = checker.getAliasedSymbol(symbol);
184
- } catch {}
185
- }
186
-
187
- if (symbol) {
188
- const resolved = resolveFromSymbol(symbol, checker);
189
- if (resolved) return resolved;
190
- }
191
- }
192
-
193
- // LAST fallback
194
- const symbol = checker.getSymbolAtLocation(expr);
195
- if (symbol) {
196
- return resolveFromSymbol(symbol, checker);
197
- }
61
+ const s = checker.getSymbolAtLocation(expr);
62
+ if (s) return resolveFromSymbol(s, checker);
198
63
 
199
64
  return null;
200
65
  }
@@ -5,7 +5,7 @@ import { TSService } from "./languageService";
5
5
  import { scanSource } from "./scanner";
6
6
  import { DependencyGraph } from "./graph";
7
7
  import { hash } from "./hash";
8
- import {EventraConfig, ScanResult} from "../types";
8
+ import { EventraConfig, ScanResult } from "../types";
9
9
  import { isExternalFile } from "./boundary";
10
10
 
11
11
  export class EventraEngine {
@@ -46,7 +46,7 @@ export class EventraEngine {
46
46
  }
47
47
  }
48
48
 
49
- // SCAN FILE
49
+ // SCAN FILE (cached)
50
50
  scanFile(
51
51
  file: string,
52
52
  content: string,
@@ -71,7 +71,6 @@ export class EventraEngine {
71
71
  return this.empty();
72
72
  }
73
73
 
74
- // update graph
75
74
  this.graph.update(file, source);
76
75
 
77
76
  const res = scanSource(
@@ -82,6 +81,7 @@ export class EventraEngine {
82
81
  undefined,
83
82
  this.wrapperCache
84
83
  );
84
+
85
85
  this.fileResults.set(file, res);
86
86
 
87
87
  return res;
@@ -92,10 +92,17 @@ export class EventraEngine {
92
92
  file: string,
93
93
  content: string,
94
94
  config: EventraConfig
95
- ) {
96
- this.wrapperCache.clear();
95
+ ): ScanResult {
97
96
  file = this.normalize(file);
98
97
 
98
+ // EARLY EXIT
99
+ if (this.fileHash.get(file) === hash(content)) {
100
+ return this.fileResults.get(file) ?? this.empty();
101
+ }
102
+
103
+ // invalidate wrapper cache
104
+ this.wrapperCache.clear();
105
+
99
106
  const res = this.scanFile(file, content, config);
100
107
 
101
108
  const dependents = this.graph.getAllDependentsDeep(file);
@@ -103,6 +110,7 @@ export class EventraEngine {
103
110
  for (const dep of dependents) {
104
111
  const abs = this.normalize(dep);
105
112
 
113
+ if (abs === file) continue; // self-skip
106
114
  if (isExternalFile(abs)) continue;
107
115
 
108
116
  let depContent = this.ts.getFileContent(abs);
@@ -140,6 +148,7 @@ export class EventraEngine {
140
148
  undefined,
141
149
  this.wrapperCache
142
150
  );
151
+
143
152
  this.fileResults.set(abs, r);
144
153
  }
145
154
 
@@ -159,19 +168,22 @@ export class EventraEngine {
159
168
 
160
169
  // REMOVE FILE
161
170
  removeFile(file: string, config?: EventraConfig) {
162
- this.wrapperCache.clear();
163
171
  file = this.normalize(file);
164
172
 
173
+ this.wrapperCache.clear();
174
+
165
175
  const dependents = this.graph.getAllDependentsDeep(file);
166
176
 
177
+ this.graph.remove(file);
178
+
167
179
  this.ts.removeFile(file);
168
180
  this.fileResults.delete(file);
169
181
  this.fileHash.delete(file);
170
- this.graph.remove(file);
171
182
 
172
183
  for (const dep of dependents) {
173
184
  const abs = this.normalize(dep);
174
185
 
186
+ if (abs === file) continue;
175
187
  if (isExternalFile(abs)) continue;
176
188
 
177
189
  let content = this.ts.getFileContent(abs);
@@ -184,11 +196,7 @@ export class EventraEngine {
184
196
  }
185
197
  }
186
198
 
187
- const h = hash(content);
188
-
189
- if (this.fileHash.get(abs) === h) continue;
190
-
191
- this.fileHash.set(abs, h);
199
+ this.fileHash.delete(abs);
192
200
 
193
201
  this.ts.updateFile(abs, content);
194
202