@eventra_dev/eventra-cli 0.0.18 → 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 (44) hide show
  1. package/README.md +74 -166
  2. package/dist/commands/check.js +25 -88
  3. package/dist/commands/sync.js +22 -77
  4. package/dist/commands/watch.js +135 -0
  5. package/dist/engine/callResolver.js +31 -0
  6. package/dist/engine/engine.js +33 -0
  7. package/dist/engine/extract.js +53 -0
  8. package/dist/engine/flow.js +38 -0
  9. package/dist/engine/graph.js +22 -0
  10. package/dist/engine/project.js +74 -0
  11. package/dist/engine/resolver.js +38 -0
  12. package/dist/engine/scanner.js +78 -0
  13. package/dist/engine/symbol.js +31 -0
  14. package/dist/index.js +10 -6
  15. package/dist/utils/parsers/vue.js +16 -12
  16. package/dist/utils/processFile.js +20 -0
  17. package/package.json +2 -1
  18. package/src/commands/check.ts +29 -136
  19. package/src/commands/sync.ts +24 -135
  20. package/src/commands/watch.ts +166 -0
  21. package/src/engine/callResolver.ts +37 -0
  22. package/src/engine/engine.ts +39 -0
  23. package/src/engine/extract.ts +74 -0
  24. package/src/engine/flow.ts +54 -0
  25. package/src/engine/graph.ts +23 -0
  26. package/src/engine/project.ts +75 -0
  27. package/src/engine/resolver.ts +53 -0
  28. package/src/engine/scanner.ts +90 -0
  29. package/src/engine/symbol.ts +42 -0
  30. package/src/index.ts +11 -6
  31. package/src/types.ts +3 -5
  32. package/src/utils/parsers/vue.ts +19 -20
  33. package/src/utils/processFile.ts +18 -0
  34. package/tests/fixtures/backend/express/app.ts +167 -12
  35. package/tests/fixtures/wrappers/component/test.tsx +1 -1
  36. package/tests/run.ts +4 -17
  37. package/dist/utils/extract.js +0 -136
  38. package/dist/utils/scanners/component-wrappers.js +0 -70
  39. package/dist/utils/scanners/function-wrappers.js +0 -85
  40. package/dist/utils/scanners/track.js +0 -52
  41. package/src/utils/extract.ts +0 -185
  42. package/src/utils/scanners/component-wrappers.ts +0 -107
  43. package/src/utils/scanners/function-wrappers.ts +0 -180
  44. package/src/utils/scanners/track.ts +0 -84
@@ -1,22 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import fg from "fast-glob";
3
- import { Project } from "ts-morph";
4
3
  import fs from "fs/promises";
5
4
 
6
- import { loadConfig } from "../utils/config";
5
+ import { loadConfig, saveConfig } from "../utils/config";
6
+ import { EventraEngine } from "../engine/engine";
7
+ import { processFile } from "../utils/processFile";
7
8
 
8
- import { detectParser } from "../utils/parsers/router";
9
- import { parseVue } from "../utils/parsers/vue";
10
- import { parseSvelte } from "../utils/parsers/svelte";
11
- import { parseAstro } from "../utils/parsers/astro";
12
-
13
- import { scanTrack } from "../utils/scanners/track";
14
- import { scanFunctionWrappers } from "../utils/scanners/function-wrappers";
15
- import { scanComponentWrappers } from "../utils/scanners/component-wrappers";
16
-
17
- import { ExtractedEvent } from "../types";
18
-
19
- export async function check() {
9
+ export async function check({ fix = false }: { fix?: boolean }) {
20
10
  const config = await loadConfig();
21
11
 
22
12
  if (!config) {
@@ -26,147 +16,50 @@ export async function check() {
26
16
 
27
17
  console.log(chalk.blue("Checking events..."));
28
18
 
29
- const project = new Project();
30
-
31
- const knownEvents = new Set(config.events ?? []);
32
- const aliases = config.aliases ?? {};
33
-
34
- const foundEvents = new Set<string>();
35
- const newEvents = new Set<string>();
36
- const unresolvedDynamic = new Set<string>();
37
-
38
19
  const files = await fg(config.sync.include, {
39
- ignore: config.sync.exclude
20
+ ignore: config.sync.exclude,
40
21
  });
41
22
 
23
+ const engine = new EventraEngine(process.cwd());
24
+
42
25
  for (const file of files) {
43
26
  try {
44
- // skip huge files
45
- const stat = await fs.stat(file);
46
- if (stat.size > 2 * 1024 * 1024) {
47
- console.log(chalk.gray(`Skipped large file: ${file}`));
48
- continue;
49
- }
50
-
51
- const parser = detectParser(file);
52
-
53
- let content = await fs.readFile(file, "utf-8");
54
-
55
- if (parser === "vue") content = parseVue(content);
56
- if (parser === "svelte") content = parseSvelte(content);
57
- if (parser === "astro") content = parseAstro(content);
58
-
59
- const virtualFile =
60
- parser === "ts" ? file : file + ".tsx";
61
-
62
- const source = project.createSourceFile(
63
- virtualFile,
64
- content,
65
- { overwrite: true }
66
- );
67
-
68
- const found: ExtractedEvent[] = [
69
- ...scanTrack(source, config),
70
- ...scanFunctionWrappers(
71
- source,
72
- config.functionWrappers ?? []
73
- ),
74
- ...scanComponentWrappers(
75
- source,
76
- config.wrappers ?? []
77
- )
78
- ];
79
-
80
- for (const event of found) {
81
- const { value, dynamic } = event;
82
-
83
- if (!value) continue;
84
-
85
- // alias skip
86
- if (aliases[value] === "__skip__") {
87
- continue;
88
- }
89
-
90
- // alias resolve
91
- if (aliases[value]) {
92
- foundEvents.add(aliases[value]);
93
- continue;
94
- }
95
-
96
- // dynamic without alias
97
- if (dynamic) {
98
- unresolvedDynamic.add(value);
99
- continue;
100
- }
101
-
102
- // static
103
- foundEvents.add(value);
104
- }
105
-
106
- // clear memory
107
- project.removeSourceFile(source);
27
+ const raw = await fs.readFile(file, "utf-8");
108
28
 
109
- } catch {
110
- console.log(
111
- chalk.gray(`Skipped file (error): ${file}`)
112
- );
113
- }
114
- }
29
+ const { content, virtualFile } = processFile(file, raw);
115
30
 
116
- // find new events
117
- for (const event of foundEvents) {
118
- if (!knownEvents.has(event)) {
119
- newEvents.add(event);
31
+ engine.scanFile(virtualFile, content);
32
+
33
+ } catch {
34
+ console.log(chalk.gray(`Skipped: ${file}`));
120
35
  }
121
36
  }
122
37
 
123
- let hasError = false;
38
+ const found = new Set(engine.getAllEvents());
39
+ const known = new Set(config.events ?? []);
124
40
 
125
- // dynamic
126
- if (unresolvedDynamic.size > 0) {
127
- hasError = true;
41
+ const newEvents = [...found].filter(e => !known.has(e));
128
42
 
129
- console.log(
130
- chalk.red("\nUnresolved dynamic events:")
131
- );
43
+ if (fix) {
44
+ newEvents.forEach(e => {
45
+ config.events.push(e);
46
+ console.log(chalk.green(`+ ${e}`));
47
+ });
132
48
 
133
- [...unresolvedDynamic]
134
- .sort()
135
- .forEach((e) =>
136
- console.log(chalk.red(`- ${e}`))
137
- );
138
- }
139
-
140
- // new events
141
- if (newEvents.size > 0) {
142
- hasError = true;
49
+ config.events = [...new Set(config.events)].sort();
143
50
 
144
- console.log(
145
- chalk.red("\nNew events detected:")
146
- );
51
+ await saveConfig(config);
147
52
 
148
- [...newEvents]
149
- .sort()
150
- .forEach((e) =>
151
- console.log(chalk.red(`+ ${e}`))
152
- );
53
+ console.log(chalk.green("\nFixed"));
54
+ return;
153
55
  }
154
56
 
155
- if (hasError) {
156
- console.log("");
157
-
158
- console.log(chalk.red("Event check failed"));
159
-
160
- console.log(
161
- chalk.red(
162
- `dynamic: ${unresolvedDynamic.size}, new: ${newEvents.size}`
163
- )
164
- );
57
+ if (newEvents.length) {
58
+ console.log(chalk.red("\nNew events:"));
59
+ newEvents.forEach(e => console.log(chalk.red(`+ ${e}`)));
165
60
 
166
61
  process.exit(1);
167
62
  }
168
63
 
169
- console.log(
170
- chalk.green("\nAll events are valid")
171
- );
64
+ console.log(chalk.green("\nAll good"));
172
65
  }
@@ -1,157 +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 {
8
- loadConfig,
9
- saveConfig
10
- } from "../utils/config";
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(
28
- chalk.red("Run 'eventra init'")
29
- );
30
- return;
31
- }
32
-
33
- console.log(
34
- chalk.blue("Scanning project...")
35
- );
13
+ console.log(chalk.blue("Scanning..."));
36
14
 
37
- const project = new Project();
38
- const events = new Set<string>();
15
+ const files = await fg(config.sync.include, {
16
+ ignore: config.sync.exclude,
17
+ });
39
18
 
40
- const aliases = config.aliases ?? {};
41
- const asked = new Set<string>();
19
+ if (!files.length) {
20
+ console.log(chalk.yellow("No files found"));
21
+ return;
22
+ }
42
23
 
43
- const files = await fg(
44
- config.sync.include,
45
- {
46
- ignore: config.sync.exclude
47
- }
48
- );
24
+ const engine = new EventraEngine(process.cwd());
49
25
 
50
26
  for (const file of files) {
51
- const parser = detectParser(file);
52
-
53
- let content = await fs.readFile(
54
- file,
55
- "utf-8"
56
- );
57
-
58
- if (parser === "vue")
59
- content = parseVue(content);
60
-
61
- if (parser === "svelte")
62
- content = parseSvelte(content);
27
+ try {
28
+ const raw = await fs.readFile(file, "utf-8");
63
29
 
64
- if (parser === "astro")
65
- content = parseAstro(content);
30
+ const { content, virtualFile } = processFile(file, raw);
66
31
 
67
- const virtualFile =
68
- parser === "ts"
69
- ? file
70
- : file + ".tsx";
32
+ engine.scanFile(virtualFile, content);
71
33
 
72
- const source = project.createSourceFile(
73
- virtualFile,
74
- content,
75
- { overwrite: true }
76
- );
77
-
78
- const found: ExtractedEvent[] = [
79
- ...scanTrack(source, config),
80
- ...scanFunctionWrappers(
81
- source,
82
- config.functionWrappers ?? []
83
- ),
84
- ...scanComponentWrappers(
85
- source,
86
- config.wrappers ?? []
87
- )
88
- ];
89
-
90
- for (const event of found) {
91
- const { value, dynamic } = event;
92
-
93
- if (!value) continue;
94
-
95
- // alias
96
- if (aliases[value]) {
97
- console.log(
98
- chalk.gray(`alias: ${value} → ${aliases[value]}`)
99
- );
100
- events.add(aliases[value]);
101
- continue;
102
- }
103
-
104
- // static
105
- if (!dynamic) {
106
- events.add(value);
107
- continue;
108
- }
109
-
110
- // already asked
111
- if (asked.has(value)) {
112
- continue;
113
- }
114
-
115
- // ask user
116
- console.log(
117
- chalk.yellow("\nDynamic event detected:")
118
- );
119
- console.log(chalk.gray(value));
120
-
121
- const { name } = await inquirer.prompt([
122
- {
123
- type: "input",
124
- name: "name",
125
- message:
126
- "Enter event name (or type 'skip'):"
127
- }
128
- ]);
129
-
130
- const normalized = name?.trim();
131
-
132
- asked.add(value);
133
-
134
- if (!normalized || normalized === "skip") {
135
- continue;
136
- }
137
-
138
- // save alias
139
- aliases[value] = normalized;
140
-
141
- events.add(normalized);
34
+ } catch (e) {
35
+ console.log(chalk.red(`ERROR: ${file}`));
36
+ console.error(e);
142
37
  }
143
38
  }
144
39
 
145
- config.aliases = aliases;
146
-
147
- const list = [...events].sort();
148
- config.events = list;
40
+ const events = engine.getAllEvents().sort();
149
41
 
42
+ config.events = events;
150
43
  await saveConfig(config);
151
44
 
152
- console.log(
153
- chalk.green(
154
- `Found ${list.length} events`
155
- )
156
- );
45
+ console.log(chalk.green(`Found ${events.length} events`));
157
46
  }
@@ -0,0 +1,166 @@
1
+ import chokidar from "chokidar";
2
+ import chalk from "chalk";
3
+ import fs from "fs/promises";
4
+ import fg from "fast-glob";
5
+
6
+ import { loadConfig, saveConfig } from "../utils/config";
7
+ import { EventraEngine } from "../engine/engine";
8
+
9
+ import { detectParser } from "../utils/parsers/router";
10
+ import { parseVue } from "../utils/parsers/vue";
11
+ import { parseSvelte } from "../utils/parsers/svelte";
12
+ import { parseAstro } from "../utils/parsers/astro";
13
+
14
+ // PREPROCESS (CONTENT)
15
+ function preprocess(file: string, content: string) {
16
+ const parser = detectParser(file);
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
33
+ export async function watch() {
34
+ const config = await loadConfig();
35
+ if (!config) return;
36
+
37
+ console.log(chalk.blue("Watching...\n"));
38
+
39
+ const files = await fg(config.sync.include, {
40
+ ignore: config.sync.exclude,
41
+ });
42
+
43
+ if (!files.length) {
44
+ console.log(chalk.yellow("No files found"));
45
+ return;
46
+ }
47
+
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");
54
+
55
+ const content = preprocess(file, raw);
56
+ const virtualFile = toVirtualFile(file);
57
+
58
+ engine.scanFile(virtualFile, content);
59
+
60
+ } catch {
61
+ console.log(chalk.gray(`skip: ${file}`));
62
+ }
63
+ }
64
+
65
+ config.events = engine.getAllEvents().sort();
66
+ await saveConfig(config);
67
+
68
+ console.log(
69
+ chalk.gray(`Initial: ${config.events.length} events\n`)
70
+ );
71
+
72
+ // STATE
73
+ let locked = false;
74
+ let queued = new Set<string>();
75
+
76
+ const run = async () => {
77
+ if (locked) return;
78
+ locked = true;
79
+
80
+ const batch = [...queued];
81
+ queued.clear();
82
+
83
+ for (const file of batch) {
84
+ try {
85
+ const raw = await fs.readFile(file, "utf-8");
86
+
87
+ const content = preprocess(file, raw);
88
+ const virtualFile = toVirtualFile(file);
89
+
90
+ engine.updateFile(virtualFile, content);
91
+
92
+ } catch {
93
+ console.log(chalk.gray(`skip: ${file}`));
94
+ }
95
+ }
96
+
97
+ const next = engine.getAllEvents();
98
+
99
+ const added = next.filter(e => !config.events.includes(e));
100
+ const removed = config.events.filter(e => !next.includes(e));
101
+
102
+ if (added.length || removed.length) {
103
+ added.forEach(e => console.log(chalk.green(`+ ${e}`)));
104
+ removed.forEach(e => console.log(chalk.red(`- ${e}`)));
105
+
106
+ config.events = next.sort();
107
+ await saveConfig(config);
108
+
109
+ console.log(
110
+ chalk.gray(`Total: ${config.events.length} events\n`)
111
+ );
112
+ }
113
+
114
+ locked = false;
115
+ };
116
+
117
+ const schedule = (file: string) => {
118
+ queued.add(file);
119
+ setTimeout(run, 50);
120
+ };
121
+
122
+ // WATCHER
123
+ const watcher = chokidar.watch(config.sync.include, {
124
+ ignored: config.sync.exclude,
125
+ ignoreInitial: true,
126
+ awaitWriteFinish: {
127
+ stabilityThreshold: 50,
128
+ pollInterval: 10,
129
+ },
130
+ });
131
+
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);
147
+
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
+ });
159
+
160
+ // EXIT
161
+ process.on("SIGINT", async () => {
162
+ console.log(chalk.yellow("\nStopping watcher..."));
163
+ await watcher.close();
164
+ process.exit(0);
165
+ });
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
+ }