@eventra_dev/eventra-cli 0.0.19 → 0.1.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.
Files changed (45) hide show
  1. package/dist/commands/check.js +17 -117
  2. package/dist/commands/sync.js +20 -115
  3. package/dist/commands/watch.js +107 -69
  4. package/dist/engine/callResolver.js +31 -0
  5. package/dist/engine/engine.js +33 -0
  6. package/dist/engine/extract.js +53 -0
  7. package/dist/engine/flow.js +38 -0
  8. package/dist/engine/graph.js +22 -0
  9. package/dist/engine/project.js +74 -0
  10. package/dist/engine/resolver.js +38 -0
  11. package/dist/engine/scanner.js +78 -0
  12. package/dist/engine/symbol.js +31 -0
  13. package/dist/utils/parsers/vue.js +16 -12
  14. package/dist/utils/processFile.js +20 -0
  15. package/package.json +1 -1
  16. package/src/commands/check.ts +19 -151
  17. package/src/commands/sync.ts +23 -194
  18. package/src/commands/watch.ts +119 -66
  19. package/src/engine/callResolver.ts +37 -0
  20. package/src/engine/engine.ts +39 -0
  21. package/src/engine/extract.ts +74 -0
  22. package/src/engine/flow.ts +54 -0
  23. package/src/engine/graph.ts +23 -0
  24. package/src/engine/project.ts +75 -0
  25. package/src/engine/resolver.ts +53 -0
  26. package/src/engine/scanner.ts +90 -0
  27. package/src/engine/symbol.ts +42 -0
  28. package/src/types.ts +3 -9
  29. package/src/utils/parsers/vue.ts +19 -20
  30. package/src/utils/processFile.ts +18 -0
  31. package/tests/fixtures/backend/express/app.ts +167 -12
  32. package/tests/fixtures/wrappers/component/test.tsx +1 -1
  33. package/tests/run.ts +4 -17
  34. package/dist/utils/detect-aliases.js +0 -39
  35. package/dist/utils/detect-wrappers.js +0 -84
  36. package/dist/utils/extract.js +0 -56
  37. package/dist/utils/scanners/component-wrappers.js +0 -73
  38. package/dist/utils/scanners/function-wrappers.js +0 -83
  39. package/dist/utils/scanners/track.js +0 -64
  40. package/src/utils/detect-aliases.ts +0 -46
  41. package/src/utils/detect-wrappers.ts +0 -119
  42. package/src/utils/extract.ts +0 -72
  43. package/src/utils/scanners/component-wrappers.ts +0 -109
  44. package/src/utils/scanners/function-wrappers.ts +0 -137
  45. package/src/utils/scanners/track.ts +0 -99
@@ -1,217 +1,46 @@
1
- import chalk from "chalk";
2
1
  import fg from "fast-glob";
3
- import {Project} from "ts-morph";
4
2
  import fs from "fs/promises";
5
- import inquirer from "inquirer";
6
-
7
- import {loadConfig, saveConfig} from "../utils/config";
8
-
9
- import {detectWrappers} from "../utils/detect-wrappers";
10
- import {detectAliases} from "../utils/detect-aliases";
11
-
12
- import {detectParser} from "../utils/parsers/router";
13
- import {parseVue} from "../utils/parsers/vue";
14
- import {parseSvelte} from "../utils/parsers/svelte";
15
- import {parseAstro} from "../utils/parsers/astro";
16
-
17
- import {scanTrack} from "../utils/scanners/track";
18
- import {scanFunctionWrappers} from "../utils/scanners/function-wrappers";
19
- import {scanComponentWrappers} from "../utils/scanners/component-wrappers";
3
+ import chalk from "chalk";
20
4
 
21
- import {ExtractedEvent} from "../types";
5
+ import { loadConfig, saveConfig } from "../utils/config";
6
+ import { EventraEngine } from "../engine/engine";
7
+ import { processFile } from "../utils/processFile";
22
8
 
23
9
  export async function sync() {
24
10
  const config = await loadConfig();
11
+ if (!config) return;
25
12
 
26
- if (!config) {
27
- console.log(chalk.red("Run 'eventra init'"));
28
- return;
29
- }
30
-
31
- console.log(chalk.blue("Scanning project..."));
32
-
33
- const project = new Project();
34
-
35
- const events = new Set<string>();
36
- const aliases = config.aliases ?? {};
37
- const asked = new Set<string>();
13
+ console.log(chalk.blue("Scanning..."));
38
14
 
39
15
  const files = await fg(config.sync.include, {
40
- ignore: config.sync.exclude
16
+ ignore: config.sync.exclude,
41
17
  });
42
18
 
43
- for (const file of files) {
44
- try {
45
- const parser = detectParser(file);
46
-
47
- let content = await fs.readFile(file, "utf-8");
48
-
49
- if (parser === "vue") content = parseVue(content);
50
- if (parser === "svelte") content = parseSvelte(content);
51
- if (parser === "astro") content = parseAstro(content);
52
-
53
- const virtualFile =
54
- parser === "ts" ? file : file + ".tsx";
55
-
56
- const source = project.createSourceFile(
57
- virtualFile,
58
- content,
59
- {overwrite: true}
60
- );
61
-
62
-
63
- // AUTO ALIASES
64
- const detectedAliases = detectAliases(source);
65
-
66
- for (const key in detectedAliases) {
67
- if (!(key in aliases)) {
68
- aliases[key] = detectedAliases[key];
69
-
70
- console.log(
71
- chalk.green(
72
- `Detected alias: ${key} → ${detectedAliases[key]}`
73
- )
74
- );
75
- }
76
- }
77
-
78
-
79
- // AUTO WRAPPERS
80
- const detected = detectWrappers(source);
81
-
82
- // function wrappers
83
- for (const fn of detected.functions) {
84
- if (
85
- !config.functionWrappers?.some(
86
- (w) => w.name === fn.name
87
- )
88
- ) {
89
- config.functionWrappers.push(fn);
90
-
91
- console.log(
92
- chalk.green(
93
- `Detected function wrapper: ${fn.name}`
94
- )
95
- );
96
- }
97
- }
98
-
99
- // component wrappers
100
- for (const comp of detected.components) {
101
- if (
102
- !config.wrappers?.some(
103
- (w) => w.name === comp.name
104
- )
105
- ) {
106
- config.wrappers.push(comp);
107
-
108
- console.log(
109
- chalk.green(
110
- `Detected component wrapper: ${comp.name} (prop: ${comp.prop})`
111
- )
112
- );
113
- }
114
- }
115
-
116
- // SCAN EVENTS
117
- const found: ExtractedEvent[] = [
118
- ...scanTrack(source, config, aliases),
119
- ...scanFunctionWrappers(
120
- source,
121
- config.functionWrappers ?? [],
122
- aliases
123
- ),
124
- ...scanComponentWrappers(
125
- source,
126
- config.wrappers ?? [],
127
- aliases
128
- )
129
- ];
130
-
131
- for (const event of found) {
132
- const {value, dynamic} = event;
133
-
134
- if (!value) continue;
135
-
136
- // skip explicit ignore
137
- if (value in aliases && aliases[value] === "__skip__") {
138
- continue;
139
- }
140
-
141
- // resolve alias
142
- if (value in aliases) {
143
- events.add(aliases[value]);
144
- continue;
145
- }
146
-
147
- // static
148
- if (!dynamic) {
149
- events.add(value);
150
- continue;
151
- }
152
-
153
- // already handled
154
- if (asked.has(value)) continue;
155
-
156
- // ASK ONLY IF NEEDED
157
- console.log(
158
- chalk.yellow("\nDynamic event detected:")
159
- );
160
- console.log(chalk.gray(value));
161
-
162
- const {name} = await inquirer.prompt([
163
- {
164
- type: "input",
165
- name: "name",
166
- message:
167
- "Enter event name (or type 'skip'):"
168
- }
169
- ]);
170
-
171
- const normalized = name?.trim();
172
- asked.add(value);
173
-
174
- if (!normalized || normalized === "skip") {
175
- continue;
176
- }
19
+ if (!files.length) {
20
+ console.log(chalk.yellow("No files found"));
21
+ return;
22
+ }
177
23
 
178
- // save alias
179
- aliases[value] = normalized;
24
+ const engine = new EventraEngine(process.cwd());
180
25
 
181
- console.log(
182
- chalk.green(
183
- `Saved alias: ${value} → ${normalized}`
184
- )
185
- );
26
+ for (const file of files) {
27
+ try {
28
+ const raw = await fs.readFile(file, "utf-8");
186
29
 
187
- events.add(normalized);
188
- }
30
+ const { content, virtualFile } = processFile(file, raw);
189
31
 
190
- // free memory
191
- project.removeSourceFile(source);
32
+ engine.scanFile(virtualFile, content);
192
33
 
193
- } catch {
194
- console.log(
195
- chalk.gray(`Skipped file: ${file}`)
196
- );
34
+ } catch (e) {
35
+ console.log(chalk.red(`ERROR: ${file}`));
36
+ console.error(e);
197
37
  }
198
38
  }
199
- // SAVE CONFIG
200
- config.aliases = Object.fromEntries(
201
- Object.entries(aliases).sort()
202
- );
203
-
204
- // MERGE
205
- const merged = new Set([
206
- ...config.events,
207
- ...events
208
- ]);
209
39
 
210
- config.events = [...merged].sort();
40
+ const events = engine.getAllEvents().sort();
211
41
 
42
+ config.events = events;
212
43
  await saveConfig(config);
213
44
 
214
- console.log(
215
- chalk.green(`\nFound ${config.events.length} events`)
216
- );
45
+ console.log(chalk.green(`Found ${events.length} events`));
217
46
  }
@@ -1,113 +1,166 @@
1
1
  import chokidar from "chokidar";
2
2
  import chalk from "chalk";
3
- import { Project } from "ts-morph";
4
3
  import fs from "fs/promises";
4
+ import fg from "fast-glob";
5
5
 
6
6
  import { loadConfig, saveConfig } from "../utils/config";
7
+ import { EventraEngine } from "../engine/engine";
7
8
 
8
9
  import { detectParser } from "../utils/parsers/router";
9
10
  import { parseVue } from "../utils/parsers/vue";
10
11
  import { parseSvelte } from "../utils/parsers/svelte";
11
12
  import { parseAstro } from "../utils/parsers/astro";
12
13
 
13
- import { scanTrack } from "../utils/scanners/track";
14
- import { scanFunctionWrappers } from "../utils/scanners/function-wrappers";
15
- import { scanComponentWrappers } from "../utils/scanners/component-wrappers";
14
+ // PREPROCESS (CONTENT)
15
+ function preprocess(file: string, content: string) {
16
+ const parser = detectParser(file);
16
17
 
18
+ if (parser === "vue") return parseVue(content);
19
+ if (parser === "svelte") return parseSvelte(content);
20
+ if (parser === "astro") return parseAstro(content);
21
+
22
+ return content;
23
+ }
24
+
25
+ // VIRTUAL FILE (PATH)
26
+ function toVirtualFile(file: string) {
27
+ return file.endsWith(".vue")
28
+ ? file + ".tsx"
29
+ : file;
30
+ }
31
+
32
+ // WATCH
17
33
  export async function watch() {
18
34
  const config = await loadConfig();
19
-
20
- if (!config) {
21
- console.log(chalk.red("Run init first"));
22
- return;
23
- }
35
+ if (!config) return;
24
36
 
25
37
  console.log(chalk.blue("Watching...\n"));
26
38
 
27
- const project = new Project();
39
+ const files = await fg(config.sync.include, {
40
+ ignore: config.sync.exclude,
41
+ });
28
42
 
29
- const aliases = config.aliases ?? {};
43
+ if (!files.length) {
44
+ console.log(chalk.yellow("No files found"));
45
+ return;
46
+ }
30
47
 
31
- // уже известные события
32
- const seen = new Set(config.events);
48
+ const engine = new EventraEngine(process.cwd());
49
+
50
+ // INITIAL SCAN
51
+ for (const file of files) {
52
+ try {
53
+ const raw = await fs.readFile(file, "utf-8");
33
54
 
34
- const queue = new Set<string>();
35
- let timer: NodeJS.Timeout;
55
+ const content = preprocess(file, raw);
56
+ const virtualFile = toVirtualFile(file);
36
57
 
37
- const processQueue = async () => {
38
- const files = [...queue];
39
- queue.clear();
58
+ engine.scanFile(virtualFile, content);
40
59
 
41
- for (const file of files) {
42
- await handle(file);
60
+ } catch {
61
+ console.log(chalk.gray(`skip: ${file}`));
43
62
  }
63
+ }
44
64
 
45
- config.events = [...new Set(config.events)].sort();
65
+ config.events = engine.getAllEvents().sort();
66
+ await saveConfig(config);
46
67
 
47
- await saveConfig(config);
48
- };
68
+ console.log(
69
+ chalk.gray(`Initial: ${config.events.length} events\n`)
70
+ );
49
71
 
50
- const handle = async (file: string) => {
51
- try {
52
- let content = await fs.readFile(file, "utf-8");
53
-
54
- const parser = detectParser(file);
72
+ // STATE
73
+ let locked = false;
74
+ let queued = new Set<string>();
55
75
 
56
- if (parser === "vue") content = parseVue(content);
57
- if (parser === "svelte") content = parseSvelte(content);
58
- if (parser === "astro") content = parseAstro(content);
76
+ const run = async () => {
77
+ if (locked) return;
78
+ locked = true;
59
79
 
60
- const source = project.createSourceFile(file, content, {
61
- overwrite: true
62
- });
80
+ const batch = [...queued];
81
+ queued.clear();
63
82
 
64
- const found = [
65
- ...scanTrack(source, config, aliases),
66
- ...scanFunctionWrappers(source, config.functionWrappers ?? [], aliases),
67
- ...scanComponentWrappers(source, config.wrappers ?? [], aliases)
68
- ];
83
+ for (const file of batch) {
84
+ try {
85
+ const raw = await fs.readFile(file, "utf-8");
69
86
 
70
- for (const e of found) {
71
- if (!e.value) continue;
87
+ const content = preprocess(file, raw);
88
+ const virtualFile = toVirtualFile(file);
72
89
 
73
- if (aliases[e.value] === "__skip__") continue;
90
+ engine.updateFile(virtualFile, content);
74
91
 
75
- if (e.value in aliases) continue;
92
+ } catch {
93
+ console.log(chalk.gray(`skip: ${file}`));
94
+ }
95
+ }
76
96
 
77
- if (seen.has(e.value)) continue;
97
+ const next = engine.getAllEvents();
78
98
 
79
- seen.add(e.value);
99
+ const added = next.filter(e => !config.events.includes(e));
100
+ const removed = config.events.filter(e => !next.includes(e));
80
101
 
81
- if (!e.dynamic) {
82
- config.events.push(e.value);
83
- console.log(chalk.green(`+ ${e.value}`));
84
- } else {
85
- console.log(chalk.yellow(`Dynamic: ${e.value}`));
86
- }
87
- }
102
+ if (added.length || removed.length) {
103
+ added.forEach(e => console.log(chalk.green(`+ ${e}`)));
104
+ removed.forEach(e => console.log(chalk.red(`- ${e}`)));
88
105
 
89
- project.removeSourceFile(source);
106
+ config.events = next.sort();
107
+ await saveConfig(config);
90
108
 
91
- } catch {
92
- console.log(chalk.gray(`skip: ${file}`));
109
+ console.log(
110
+ chalk.gray(`Total: ${config.events.length} events\n`)
111
+ );
93
112
  }
113
+
114
+ locked = false;
94
115
  };
95
116
 
117
+ const schedule = (file: string) => {
118
+ queued.add(file);
119
+ setTimeout(run, 50);
120
+ };
121
+
122
+ // WATCHER
96
123
  const watcher = chokidar.watch(config.sync.include, {
97
- ignored: [
98
- ...config.sync.exclude,
99
- "**/*.log",
100
- "**/*.tmp",
101
- "**/*.swp"
102
- ],
103
- ignoreInitial: true
124
+ ignored: config.sync.exclude,
125
+ ignoreInitial: true,
126
+ awaitWriteFinish: {
127
+ stabilityThreshold: 50,
128
+ pollInterval: 10,
129
+ },
104
130
  });
105
131
 
106
- watcher.on("change", (file) => {
107
- queue.add(file);
132
+ watcher
133
+ .on("add", (file) => {
134
+ console.log(chalk.gray(`add: ${file}`));
135
+ schedule(file);
136
+ })
137
+ .on("change", (file) => {
138
+ console.log(chalk.gray(`change: ${file}`));
139
+ schedule(file);
140
+ })
141
+ .on("unlink", async (file) => {
142
+ console.log(chalk.gray(`remove: ${file}`));
143
+
144
+ const virtualFile = toVirtualFile(file);
145
+
146
+ engine.removeFile?.(virtualFile);
108
147
 
109
- clearTimeout(timer);
148
+ const next = engine.getAllEvents();
149
+
150
+ const removed = config.events.filter(e => !next.includes(e));
151
+
152
+ if (removed.length) {
153
+ removed.forEach(e => console.log(chalk.red(`- ${e}`)));
154
+
155
+ config.events = next.sort();
156
+ await saveConfig(config);
157
+ }
158
+ });
110
159
 
111
- timer = setTimeout(processQueue, 200);
160
+ // EXIT
161
+ process.on("SIGINT", async () => {
162
+ console.log(chalk.yellow("\nStopping watcher..."));
163
+ await watcher.close();
164
+ process.exit(0);
112
165
  });
113
166
  }
@@ -0,0 +1,37 @@
1
+ import { Node } from "ts-morph";
2
+
3
+ export function resolveFunctionFromCall(expr: Node) {
4
+ let symbol = expr.getSymbol();
5
+
6
+ if (!symbol && Node.isPropertyAccessExpression(expr)) {
7
+ symbol = expr.getNameNode().getSymbol();
8
+ }
9
+
10
+ if (!symbol) return null;
11
+
12
+ const decls = symbol.getDeclarations() ?? [];
13
+
14
+ for (const d of decls) {
15
+ if (Node.isFunctionDeclaration(d)) return d;
16
+
17
+ if (Node.isVariableDeclaration(d)) {
18
+ const init = d.getInitializer();
19
+
20
+ if (
21
+ init &&
22
+ (Node.isArrowFunction(init) ||
23
+ Node.isFunctionExpression(init))
24
+ ) {
25
+ return init;
26
+ }
27
+ }
28
+
29
+ if (Node.isMethodDeclaration(d)) return d;
30
+
31
+ if (Node.isFunctionExpression(d) || Node.isArrowFunction(d)) {
32
+ return d;
33
+ }
34
+ }
35
+
36
+ return null;
37
+ }
@@ -0,0 +1,39 @@
1
+ import { ProjectGraph } from "./project";
2
+ import { scanSource } from "./scanner";
3
+
4
+ export class EventraEngine {
5
+ private project: ProjectGraph;
6
+ private fileEvents = new Map<string, Set<string>>();
7
+
8
+ constructor(entry: string) {
9
+ this.project = new ProjectGraph(entry);
10
+ }
11
+
12
+ scanFile(file: string, content: string) {
13
+ const source = this.project.getSource(file, content);
14
+ const checker = this.project.getChecker();
15
+
16
+ const events = scanSource(source, checker);
17
+
18
+ this.fileEvents.set(file, events);
19
+ return events;
20
+ }
21
+
22
+ updateFile(file: string, content: string) {
23
+ return this.scanFile(file, content);
24
+ }
25
+
26
+ getAllEvents() {
27
+ const all = new Set<string>();
28
+
29
+ for (const set of this.fileEvents.values()) {
30
+ set.forEach(e => all.add(e));
31
+ }
32
+
33
+ return [...all];
34
+ }
35
+
36
+ removeFile(file: string) {
37
+ this.fileEvents.delete(file);
38
+ }
39
+ }
@@ -0,0 +1,74 @@
1
+ import { Node, ObjectLiteralExpression, ts } from "ts-morph";
2
+ import { resolveNodeValue } from "./resolver";
3
+
4
+ export type Extracted = {
5
+ value: string;
6
+ dynamic: boolean;
7
+ };
8
+
9
+ export function extractEventsFromArgs(
10
+ args: Node[],
11
+ checker: ts.TypeChecker
12
+ ): Extracted[] {
13
+ return args.flatMap(arg => extract(arg, checker));
14
+ }
15
+
16
+ function extract(node: Node, checker: ts.TypeChecker): Extracted[] {
17
+ if (Node.isStringLiteral(node)) {
18
+ return [{ value: node.getLiteralText(), dynamic: false }];
19
+ }
20
+
21
+ if (Node.isNoSubstitutionTemplateLiteral(node)) {
22
+ return [{ value: node.getLiteralText(), dynamic: false }];
23
+ }
24
+
25
+ if (Node.isTemplateExpression(node)) {
26
+ let result = node.getHead().getLiteralText();
27
+
28
+ for (const span of node.getTemplateSpans()) {
29
+ result += "*";
30
+ result += span.getLiteral().getLiteralText();
31
+ }
32
+
33
+ return [{ value: result, dynamic: true }];
34
+ }
35
+
36
+ if (Node.isObjectLiteralExpression(node)) {
37
+ return extractObject(node, checker);
38
+ }
39
+
40
+ const resolved = resolveNodeValue(node, checker);
41
+ if (resolved) {
42
+ return resolved.values.map(v => ({
43
+ value: v,
44
+ dynamic: resolved.dynamic,
45
+ }));
46
+ }
47
+
48
+ return [];
49
+ }
50
+
51
+ function extractObject(
52
+ obj: ObjectLiteralExpression,
53
+ checker: ts.TypeChecker
54
+ ): Extracted[] {
55
+ const res: Extracted[] = [];
56
+
57
+ for (const prop of obj.getProperties()) {
58
+ if (!Node.isPropertyAssignment(prop)) continue;
59
+
60
+ const name = prop.getName();
61
+ const init = prop.getInitializer();
62
+ if (!init) continue;
63
+
64
+ if (["event", "name", "action", "type"].includes(name)) {
65
+ res.push(...extract(init, checker));
66
+ }
67
+
68
+ if (Node.isObjectLiteralExpression(init)) {
69
+ res.push(...extractObject(init, checker));
70
+ }
71
+ }
72
+
73
+ return res;
74
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ Node,
3
+ SyntaxKind,
4
+ FunctionDeclaration,
5
+ ArrowFunction,
6
+ FunctionExpression
7
+ } from "ts-morph";
8
+
9
+ type Fn =
10
+ | FunctionDeclaration
11
+ | ArrowFunction
12
+ | FunctionExpression;
13
+
14
+ function getBodyNodes(fn: Fn) {
15
+ const body = fn.getBody();
16
+ if (!body) return [];
17
+
18
+ if (Node.isBlock(body)) return body.getStatements();
19
+ return [body];
20
+ }
21
+
22
+ export function getForwardedParamIndex(fn: Node): number | null {
23
+ if (
24
+ !Node.isFunctionDeclaration(fn) &&
25
+ !Node.isArrowFunction(fn) &&
26
+ !Node.isFunctionExpression(fn)
27
+ ) return null;
28
+
29
+ const f = fn as Fn;
30
+ const params = f.getParameters();
31
+
32
+ for (const node of getBodyNodes(f)) {
33
+ const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
34
+
35
+ for (const call of calls) {
36
+ const name = call.getExpression().getText();
37
+
38
+ if (!/(track|event|log|analytics|capture|send)/i.test(name)) continue;
39
+
40
+ const args = call.getArguments();
41
+
42
+ for (let i = 0; i < args.length; i++) {
43
+ const arg = args[i];
44
+
45
+ if (Node.isIdentifier(arg)) {
46
+ const idx = params.findIndex(p => p.getName() === arg.getText());
47
+ if (idx !== -1) return idx;
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }