@eventra_dev/eventra-cli 0.1.34 → 0.2.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.
@@ -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
  });
@@ -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
 
@@ -21,7 +21,7 @@ export class TSService {
21
21
  let compilerOptions: ts.CompilerOptions = {
22
22
  allowJs: true,
23
23
  allowNonTsExtensions: true,
24
- jsx: ts.JsxEmit.React,
24
+ jsx: ts.JsxEmit.Preserve,
25
25
  jsxImportSource: "react",
26
26
  moduleResolution: ts.ModuleResolutionKind.Node16,
27
27
  target: ts.ScriptTarget.ESNext,
@@ -59,7 +59,15 @@ export class TSService {
59
59
 
60
60
  const allFiles = ts.sys.readDirectory(
61
61
  root,
62
- [".ts", ".tsx", ".js", ".jsx"],
62
+ [
63
+ ".ts",
64
+ ".tsx",
65
+ ".js",
66
+ ".jsx",
67
+ ".vue",
68
+ ".svelte",
69
+ ".astro"
70
+ ],
63
71
  undefined,
64
72
  undefined
65
73
  );
@@ -77,34 +85,39 @@ export class TSService {
77
85
  file = this.normalize(file);
78
86
  return this.files.get(file)?.version.toString() ?? "0";
79
87
  },
88
+ getScriptKind: (fileName) => {
89
+ fileName = this.normalize(fileName);
90
+
91
+ if (
92
+ fileName.endsWith(".tsx") ||
93
+ fileName.endsWith(".vue") ||
94
+ fileName.endsWith(".svelte") ||
95
+ fileName.endsWith(".astro")
96
+ ) {
97
+ return ts.ScriptKind.TSX;
98
+ }
99
+
100
+ if (fileName.endsWith(".ts")) {
101
+ return ts.ScriptKind.TS;
102
+ }
103
+
104
+ if (fileName.endsWith(".jsx")) {
105
+ return ts.ScriptKind.JSX;
106
+ }
107
+
108
+ if (fileName.endsWith(".js")) {
109
+ return ts.ScriptKind.JS;
110
+ }
80
111
 
112
+ return ts.ScriptKind.Unknown;
113
+ },
81
114
  getScriptSnapshot: (file) => {
82
115
  file = this.normalize(file);
83
-
84
- // из памяти
85
116
  const f = this.files.get(file);
86
117
  if (f) {
87
118
  return ts.ScriptSnapshot.fromString(f.content);
88
119
  }
89
120
 
90
- // virtual TSX (vue/svelte/astro)
91
- if (file.endsWith(".tsx")) {
92
- const original = file.replace(/\.tsx$/, "");
93
-
94
- if (fs.existsSync(original)) {
95
- const content = fs.readFileSync(original, "utf-8");
96
-
97
- this.files.set(file, {
98
- content,
99
- version: 1,
100
- });
101
-
102
- this.fileNames.add(file);
103
-
104
- return ts.ScriptSnapshot.fromString(content);
105
- }
106
- }
107
-
108
121
  try {
109
122
  if (fs.existsSync(file)) {
110
123
  const content = fs.readFileSync(file, "utf-8");
@@ -148,12 +161,6 @@ export class TSService {
148
161
  return path.resolve(file).replace(/\\/g, "/");
149
162
  }
150
163
 
151
- private ensureProgram() {
152
- try {
153
- this.service.getProgram();
154
- } catch {}
155
- }
156
-
157
164
  // PUBLIC API
158
165
  updateFile(file: string, content: string) {
159
166
  file = this.normalize(file);
@@ -166,8 +173,6 @@ export class TSService {
166
173
  });
167
174
 
168
175
  this.fileNames.add(file);
169
-
170
- this.ensureProgram();
171
176
  }
172
177
 
173
178
  removeFile(file: string) {
@@ -175,8 +180,6 @@ export class TSService {
175
180
 
176
181
  this.files.delete(file);
177
182
  this.fileNames.delete(file);
178
-
179
- this.ensureProgram();
180
183
  }
181
184
 
182
185
  getProgram() {
@@ -4,6 +4,7 @@ import { resolveFunctionFromCall } from "./callResolver";
4
4
  import { resolveExportedSymbol } from "./exportResolver";
5
5
  import { EventraConfig, ScanResult } from "../types";
6
6
 
7
+ // HELPERS
7
8
  function isValidEvent(v: string) {
8
9
  return (
9
10
  v &&
@@ -32,6 +33,7 @@ function getJsxAttrName(name: ts.JsxAttributeName): string {
32
33
  return "";
33
34
  }
34
35
 
36
+ // MAIN
35
37
  export function scanSource(
36
38
  source: ts.SourceFile,
37
39
  checker: ts.TypeChecker,
@@ -47,35 +49,52 @@ export function scanSource(
47
49
  if (visited.has(source.fileName)) {
48
50
  return { events, detectedFunctionWrappers, detectedComponentWrappers };
49
51
  }
52
+
50
53
  visited.add(source.fileName);
51
54
 
52
- function extractTrackCall(node: ts.CallExpression, paramMap: Map<string, ts.Expression>) {
53
- const expr = node.expression;
55
+ // TRACK DETECTION
56
+ function isTrackCall(expr: ts.Expression) {
57
+ if (ts.isIdentifier(expr)) {
58
+ return expr.text === "track";
59
+ }
60
+
61
+ if (ts.isPropertyAccessExpression(expr)) {
62
+ return expr.name.text === "track";
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ function extractTrackCall(
69
+ node: ts.CallExpression,
70
+ paramMap: Map<string, ts.Expression>
71
+ ) {
72
+ if (!isTrackCall(node.expression)) return;
73
+
74
+ const arg = node.arguments[0];
75
+ if (!arg) return;
54
76
 
55
- if (ts.isPropertyAccessExpression(expr) && expr.name.text === "track") {
56
- const arg = node.arguments[0];
57
- if (!arg) return;
77
+ const res = resolveNodeValue(arg, checker, paramMap);
58
78
 
59
- const res = resolveNodeValue(arg, checker, paramMap);
60
- for (const v of res.values) {
61
- if (isValidEvent(v)) events.add(v);
79
+ for (const v of res.values) {
80
+ if (isValidEvent(v)) {
81
+ events.add(v);
62
82
  }
63
83
  }
64
84
  }
65
85
 
86
+ // WRAPPER DETECTION
66
87
  function isWrapperFunction(fn: ts.FunctionLikeDeclaration) {
67
88
  let found = false;
68
89
 
69
90
  function walk(n: ts.Node) {
70
91
  if (found) return;
71
92
 
72
- if (ts.isCallExpression(n)) {
73
- const expr = n.expression;
74
- if (ts.isPropertyAccessExpression(expr) && expr.name.text === "track") {
75
- found = true;
76
- return;
77
- }
93
+ if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
94
+ found = true;
95
+ return;
78
96
  }
97
+
79
98
  ts.forEachChild(n, walk);
80
99
  }
81
100
 
@@ -83,15 +102,20 @@ export function scanSource(
83
102
  return found;
84
103
  }
85
104
 
105
+ // COMPONENT WRAPPER
86
106
  function detectComponentWrapper(tag: ts.JsxTagNameExpression): string | null {
87
107
  const tagName = getJsxTagName(tag);
88
108
 
89
- if (wrapperCache.has(tagName)) return wrapperCache.get(tagName)!;
109
+ if (wrapperCache.has(tagName)) {
110
+ return wrapperCache.get(tagName)!;
111
+ }
90
112
 
91
113
  let symbol = checker.getSymbolAtLocation(tag);
114
+
92
115
  if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
93
116
  symbol = checker.getAliasedSymbol(symbol);
94
117
  }
118
+
95
119
  if (!symbol) {
96
120
  wrapperCache.set(tagName, null);
97
121
  return null;
@@ -120,21 +144,20 @@ export function scanSource(
120
144
  function walk(n: ts.Node) {
121
145
  if (prop) return;
122
146
 
123
- if (ts.isCallExpression(n)) {
124
- const expr = n.expression;
147
+ if (ts.isCallExpression(n) && isTrackCall(n.expression)) {
148
+ const arg = n.arguments[0];
125
149
 
126
- if (ts.isPropertyAccessExpression(expr) && expr.name.text === "track") {
127
- const arg = n.arguments[0];
128
- if (ts.isPropertyAccessExpression(arg)) {
129
- prop = arg.name.text;
130
- return;
131
- }
150
+ if (ts.isPropertyAccessExpression(arg)) {
151
+ prop = arg.name.text;
152
+ return;
132
153
  }
133
154
  }
155
+
134
156
  ts.forEachChild(n, walk);
135
157
  }
136
158
 
137
159
  walk(fn.body);
160
+
138
161
  wrapperCache.set(tagName, prop);
139
162
 
140
163
  if (prop) {
@@ -147,6 +170,7 @@ export function scanSource(
147
170
  return null;
148
171
  }
149
172
 
173
+ // VISITOR
150
174
  function visit(node: ts.Node) {
151
175
  // JSX
152
176
  if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
@@ -205,29 +229,45 @@ export function scanSource(
205
229
  resolvedFn.parameters.forEach((p, i) => {
206
230
  const arg = node.arguments[i];
207
231
  if (!arg) return;
232
+
208
233
  if (ts.isIdentifier(p.name)) {
209
234
  paramMap.set(p.name.text, arg);
210
235
  }
211
236
  });
212
237
  }
213
238
 
214
- // direct track
215
239
  extractTrackCall(node, paramMap);
216
240
 
217
- // wrapper
218
241
  if (resolvedFn && isWrapperFunction(resolvedFn)) {
219
242
  const name = getCallName(node.expression);
243
+
220
244
  if (name && name !== "track") {
221
245
  detectedFunctionWrappers.add(name);
222
246
  }
247
+
223
248
  if (resolvedFn.body) {
224
249
  visit(resolvedFn.body);
225
250
  }
226
251
  }
227
252
 
228
- for (const arg of node.arguments) {
229
- visit(arg);
253
+ if (resolvedFn && resolvedFn.body) {
254
+ const sf = resolvedFn.getSourceFile();
255
+
256
+ if (!visited.has(sf.fileName)) {
257
+ const res = scanSource(
258
+ sf,
259
+ checker,
260
+ config,
261
+ visited,
262
+ paramMap,
263
+ wrapperCache
264
+ );
265
+
266
+ res.events.forEach(e => events.add(e));
267
+ }
230
268
  }
269
+
270
+ node.arguments.forEach(visit);
231
271
  visit(node.expression);
232
272
 
233
273
  return;