@eventra_dev/eventra-cli 0.0.18 → 0.0.19

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.
@@ -1,157 +1,217 @@
1
1
  import chalk from "chalk";
2
2
  import fg from "fast-glob";
3
- import { Project } from "ts-morph";
3
+ import {Project} from "ts-morph";
4
4
  import fs from "fs/promises";
5
5
  import inquirer from "inquirer";
6
6
 
7
- import {
8
- loadConfig,
9
- saveConfig
10
- } from "../utils/config";
7
+ import {loadConfig, saveConfig} from "../utils/config";
11
8
 
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";
9
+ import {detectWrappers} from "../utils/detect-wrappers";
10
+ import {detectAliases} from "../utils/detect-aliases";
16
11
 
17
- import { scanTrack } from "../utils/scanners/track";
18
- import { scanFunctionWrappers } from "../utils/scanners/function-wrappers";
19
- import { scanComponentWrappers } from "../utils/scanners/component-wrappers";
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";
20
16
 
21
- import { ExtractedEvent } from "../types";
17
+ import {scanTrack} from "../utils/scanners/track";
18
+ import {scanFunctionWrappers} from "../utils/scanners/function-wrappers";
19
+ import {scanComponentWrappers} from "../utils/scanners/component-wrappers";
20
+
21
+ import {ExtractedEvent} from "../types";
22
22
 
23
23
  export async function sync() {
24
24
  const config = await loadConfig();
25
25
 
26
26
  if (!config) {
27
- console.log(
28
- chalk.red("Run 'eventra init'")
29
- );
27
+ console.log(chalk.red("Run 'eventra init'"));
30
28
  return;
31
29
  }
32
30
 
33
- console.log(
34
- chalk.blue("Scanning project...")
35
- );
31
+ console.log(chalk.blue("Scanning project..."));
36
32
 
37
33
  const project = new Project();
38
- const events = new Set<string>();
39
34
 
35
+ const events = new Set<string>();
40
36
  const aliases = config.aliases ?? {};
41
37
  const asked = new Set<string>();
42
38
 
43
- const files = await fg(
44
- config.sync.include,
45
- {
46
- ignore: config.sync.exclude
47
- }
48
- );
39
+ const files = await fg(config.sync.include, {
40
+ ignore: config.sync.exclude
41
+ });
49
42
 
50
43
  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);
63
-
64
- if (parser === "astro")
65
- content = parseAstro(content);
66
-
67
- const virtualFile =
68
- parser === "ts"
69
- ? file
70
- : file + ".tsx";
71
-
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;
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
+ }
102
76
  }
103
77
 
104
- // static
105
- if (!dynamic) {
106
- events.add(value);
107
- continue;
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
+ }
108
97
  }
109
98
 
110
- // already asked
111
- if (asked.has(value)) {
112
- continue;
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
+ }
113
114
  }
114
115
 
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'):"
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;
127
151
  }
128
- ]);
129
152
 
130
- const normalized = name?.trim();
153
+ // already handled
154
+ if (asked.has(value)) continue;
131
155
 
132
- asked.add(value);
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
+ }
177
+
178
+ // save alias
179
+ aliases[value] = normalized;
180
+
181
+ console.log(
182
+ chalk.green(
183
+ `Saved alias: ${value} → ${normalized}`
184
+ )
185
+ );
133
186
 
134
- if (!normalized || normalized === "skip") {
135
- continue;
187
+ events.add(normalized);
136
188
  }
137
189
 
138
- // save alias
139
- aliases[value] = normalized;
190
+ // free memory
191
+ project.removeSourceFile(source);
140
192
 
141
- events.add(normalized);
193
+ } catch {
194
+ console.log(
195
+ chalk.gray(`Skipped file: ${file}`)
196
+ );
142
197
  }
143
198
  }
199
+ // SAVE CONFIG
200
+ config.aliases = Object.fromEntries(
201
+ Object.entries(aliases).sort()
202
+ );
144
203
 
145
- config.aliases = aliases;
204
+ // MERGE
205
+ const merged = new Set([
206
+ ...config.events,
207
+ ...events
208
+ ]);
146
209
 
147
- const list = [...events].sort();
148
- config.events = list;
210
+ config.events = [...merged].sort();
149
211
 
150
212
  await saveConfig(config);
151
213
 
152
214
  console.log(
153
- chalk.green(
154
- `Found ${list.length} events`
155
- )
215
+ chalk.green(`\nFound ${config.events.length} events`)
156
216
  );
157
217
  }
@@ -0,0 +1,113 @@
1
+ import chokidar from "chokidar";
2
+ import chalk from "chalk";
3
+ import { Project } from "ts-morph";
4
+ import fs from "fs/promises";
5
+
6
+ import { loadConfig, saveConfig } from "../utils/config";
7
+
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
+ export async function watch() {
18
+ const config = await loadConfig();
19
+
20
+ if (!config) {
21
+ console.log(chalk.red("Run init first"));
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.blue("Watching...\n"));
26
+
27
+ const project = new Project();
28
+
29
+ const aliases = config.aliases ?? {};
30
+
31
+ // уже известные события
32
+ const seen = new Set(config.events);
33
+
34
+ const queue = new Set<string>();
35
+ let timer: NodeJS.Timeout;
36
+
37
+ const processQueue = async () => {
38
+ const files = [...queue];
39
+ queue.clear();
40
+
41
+ for (const file of files) {
42
+ await handle(file);
43
+ }
44
+
45
+ config.events = [...new Set(config.events)].sort();
46
+
47
+ await saveConfig(config);
48
+ };
49
+
50
+ const handle = async (file: string) => {
51
+ try {
52
+ let content = await fs.readFile(file, "utf-8");
53
+
54
+ const parser = detectParser(file);
55
+
56
+ if (parser === "vue") content = parseVue(content);
57
+ if (parser === "svelte") content = parseSvelte(content);
58
+ if (parser === "astro") content = parseAstro(content);
59
+
60
+ const source = project.createSourceFile(file, content, {
61
+ overwrite: true
62
+ });
63
+
64
+ const found = [
65
+ ...scanTrack(source, config, aliases),
66
+ ...scanFunctionWrappers(source, config.functionWrappers ?? [], aliases),
67
+ ...scanComponentWrappers(source, config.wrappers ?? [], aliases)
68
+ ];
69
+
70
+ for (const e of found) {
71
+ if (!e.value) continue;
72
+
73
+ if (aliases[e.value] === "__skip__") continue;
74
+
75
+ if (e.value in aliases) continue;
76
+
77
+ if (seen.has(e.value)) continue;
78
+
79
+ seen.add(e.value);
80
+
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
+ }
88
+
89
+ project.removeSourceFile(source);
90
+
91
+ } catch {
92
+ console.log(chalk.gray(`skip: ${file}`));
93
+ }
94
+ };
95
+
96
+ const watcher = chokidar.watch(config.sync.include, {
97
+ ignored: [
98
+ ...config.sync.exclude,
99
+ "**/*.log",
100
+ "**/*.tmp",
101
+ "**/*.swp"
102
+ ],
103
+ ignoreInitial: true
104
+ });
105
+
106
+ watcher.on("change", (file) => {
107
+ queue.add(file);
108
+
109
+ clearTimeout(timer);
110
+
111
+ timer = setTimeout(processQueue, 200);
112
+ });
113
+ }
package/src/index.ts CHANGED
@@ -11,7 +11,6 @@ program
11
11
 
12
12
  program
13
13
  .command("init")
14
- .description("Initialize Eventra")
15
14
  .action(async () => {
16
15
  const { init } = await import("./commands/init");
17
16
  await init();
@@ -19,7 +18,6 @@ program
19
18
 
20
19
  program
21
20
  .command("sync")
22
- .description("Sync events")
23
21
  .action(async () => {
24
22
  const { sync } = await import("./commands/sync");
25
23
  await sync();
@@ -27,15 +25,22 @@ program
27
25
 
28
26
  program
29
27
  .command("check")
30
- .description("Validate events (CI mode)")
31
- .action(async () => {
28
+ .option("--fix", "auto fix issues")
29
+ .action(async (opts) => {
32
30
  const { check } = await import("./commands/check");
33
- await check();
31
+ await check({ fix: opts.fix });
32
+ });
33
+
34
+ program
35
+ .command("watch")
36
+ .description("Watch files and detect events in real-time")
37
+ .action(async () => {
38
+ const { watch } = await import("./commands/watch");
39
+ await watch();
34
40
  });
35
41
 
36
42
  program
37
43
  .command("send")
38
- .description("Send events")
39
44
  .action(async () => {
40
45
  const { send } = await import("./commands/send");
41
46
  await send();
package/src/types.ts CHANGED
@@ -6,6 +6,8 @@ export type ComponentWrapper = {
6
6
  export type FunctionWrapper = {
7
7
  name: string;
8
8
  event?: string;
9
+ file?: string;
10
+ line?: number;
9
11
  };
10
12
 
11
13
  export type EventraConfig = {
@@ -24,4 +26,6 @@ export type EventraConfig = {
24
26
  export type ExtractedEvent = {
25
27
  value: string;
26
28
  dynamic: boolean;
29
+ file?: string;
30
+ line?: number;
27
31
  };
@@ -0,0 +1,46 @@
1
+ import {
2
+ SourceFile,
3
+ SyntaxKind,
4
+ Node
5
+ } from "ts-morph";
6
+
7
+ export function detectAliases(source: SourceFile) {
8
+ const aliases: Record<string, string> = {};
9
+
10
+ const vars = source.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
11
+
12
+ for (const v of vars) {
13
+ const name = v.getName();
14
+ const init = v.getInitializer();
15
+ if (!init) continue;
16
+
17
+ // const A = "event"
18
+ if (Node.isStringLiteral(init)) {
19
+ aliases[name] = init.getLiteralText();
20
+ continue;
21
+ }
22
+
23
+ // const A = `event`
24
+ if (init.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral) {
25
+ aliases[name] = init.getText().replace(/`/g, "");
26
+ continue;
27
+ }
28
+
29
+ // const EVENTS = { CLICK: "click" }
30
+ if (Node.isObjectLiteralExpression(init)) {
31
+ for (const p of init.getProperties()) {
32
+ if (!Node.isPropertyAssignment(p)) continue;
33
+
34
+ const key = p.getName();
35
+ const val = p.getInitializer();
36
+ if (!val) continue;
37
+
38
+ if (Node.isStringLiteral(val)) {
39
+ aliases[`${name}.${key}`] = val.getLiteralText();
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ return aliases;
46
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ SourceFile,
3
+ SyntaxKind,
4
+ Node,
5
+ CallExpression
6
+ } from "ts-morph";
7
+
8
+ export type FunctionWrapper = {
9
+ name: string;
10
+ event?: string;
11
+ };
12
+
13
+ export type ComponentWrapper = {
14
+ name: string;
15
+ prop: string;
16
+ };
17
+
18
+ export function detectWrappers(source: SourceFile) {
19
+ return {
20
+ functions: detectFunctionWrappers(source),
21
+ components: detectComponentWrappers(source)
22
+ };
23
+ }
24
+
25
+ // FUNCTIONS
26
+ function detectFunctionWrappers(source: SourceFile): FunctionWrapper[] {
27
+ const result = new Map<string, FunctionWrapper>();
28
+
29
+ const functions = [
30
+ ...source.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
31
+ ...source.getDescendantsOfKind(SyntaxKind.ArrowFunction),
32
+ ...source.getDescendantsOfKind(SyntaxKind.FunctionExpression)
33
+ ];
34
+
35
+ for (const fn of functions) {
36
+ const body = fn.getBody();
37
+ if (!body) continue;
38
+
39
+ const calls = body.getDescendantsOfKind(SyntaxKind.CallExpression);
40
+
41
+ for (const call of calls) {
42
+ if (!isTrackCall(call)) continue;
43
+
44
+ const name = getFunctionName(fn);
45
+ if (!name) continue;
46
+
47
+ const arg = call.getArguments()[0];
48
+
49
+ let eventField: string | undefined;
50
+
51
+ if (arg && Node.isObjectLiteralExpression(arg)) {
52
+ const prop = arg.getProperties()[0];
53
+ if (prop && Node.isPropertyAssignment(prop)) {
54
+ eventField = prop.getName();
55
+ }
56
+ }
57
+
58
+ result.set(name, { name, event: eventField });
59
+ }
60
+ }
61
+
62
+ return [...result.values()];
63
+ }
64
+
65
+ function isTrackCall(call: CallExpression) {
66
+ const expr = call.getExpression();
67
+
68
+ if (Node.isIdentifier(expr)) {
69
+ return expr.getText() === "track";
70
+ }
71
+
72
+ if (Node.isPropertyAccessExpression(expr)) {
73
+ return expr.getName() === "track";
74
+ }
75
+
76
+ return false;
77
+ }
78
+
79
+ function getFunctionName(fn: Node): string | null {
80
+ if (Node.isFunctionDeclaration(fn)) {
81
+ return fn.getName() ?? null;
82
+ }
83
+
84
+ const varDecl = fn.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
85
+ return varDecl?.getName() ?? null;
86
+ }
87
+
88
+ // COMPONENTS
89
+ function detectComponentWrappers(source: SourceFile): ComponentWrapper[] {
90
+ const result = new Map<string, ComponentWrapper>();
91
+
92
+ const elements = [
93
+ ...source.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
94
+ ...source.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
95
+ ];
96
+
97
+ for (const el of elements) {
98
+ const name = el.getTagNameNode().getText();
99
+
100
+ for (const attr of el.getAttributes()) {
101
+ const a = attr.asKind(SyntaxKind.JsxAttribute);
102
+ if (!a) continue;
103
+
104
+ const prop = a.getNameNode().getText();
105
+ const val = a.getInitializer();
106
+
107
+ if (!val) continue;
108
+
109
+ if (
110
+ Node.isStringLiteral(val) ||
111
+ Node.isJsxExpression(val)
112
+ ) {
113
+ result.set(name, { name, prop });
114
+ }
115
+ }
116
+ }
117
+
118
+ return [...result.values()];
119
+ }