@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
@@ -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
+ }
@@ -0,0 +1,23 @@
1
+ import { SourceFile } from "ts-morph";
2
+
3
+ export class DependencyGraph {
4
+ private reverse = new Map<string, Set<string>>();
5
+
6
+ update(file: string, source: SourceFile) {
7
+ const imports = source
8
+ .getImportDeclarations()
9
+ .map(i => i.getModuleSpecifierSourceFile()?.getFilePath())
10
+ .filter(Boolean) as string[];
11
+
12
+ for (const dep of imports) {
13
+ if (!this.reverse.has(dep)) {
14
+ this.reverse.set(dep, new Set());
15
+ }
16
+ this.reverse.get(dep)!.add(file);
17
+ }
18
+ }
19
+
20
+ getDependents(file: string) {
21
+ return [...(this.reverse.get(file) ?? [])];
22
+ }
23
+ }
@@ -0,0 +1,75 @@
1
+ import { Project, SourceFile, ts } from "ts-morph";
2
+ import path from "path";
3
+ import fs from "fs";
4
+
5
+ function normalize(file: string) {
6
+ return path.resolve(file);
7
+ }
8
+
9
+ function findTsConfig(start: string): string | null {
10
+ let dir = path.dirname(start);
11
+
12
+ while (true) {
13
+ const file = path.join(dir, "tsconfig.json");
14
+ if (fs.existsSync(file)) return file;
15
+
16
+ const parent = path.dirname(dir);
17
+ if (parent === dir) break;
18
+
19
+ dir = parent;
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ function getScriptKind(file: string): ts.ScriptKind {
26
+ if (file.endsWith(".tsx")) return ts.ScriptKind.TSX;
27
+ if (file.endsWith(".jsx")) return ts.ScriptKind.JSX;
28
+ if (file.endsWith(".js")) return ts.ScriptKind.JS;
29
+ if (file.endsWith(".vue")) return ts.ScriptKind.TSX;
30
+ return ts.ScriptKind.TS;
31
+ }
32
+
33
+ export class ProjectGraph {
34
+ private project: Project;
35
+
36
+ constructor(entry: string) {
37
+ const tsconfig = findTsConfig(entry);
38
+
39
+ this.project = tsconfig
40
+ ? new Project({
41
+ tsConfigFilePath: tsconfig,
42
+ skipAddingFilesFromTsConfig: false,
43
+ })
44
+ : new Project({
45
+ useInMemoryFileSystem: true,
46
+ compilerOptions: {
47
+ allowJs: true,
48
+ jsx: ts.JsxEmit.Preserve,
49
+ moduleResolution: ts.ModuleResolutionKind.NodeJs,
50
+ target: ts.ScriptTarget.ESNext,
51
+ },
52
+ });
53
+ }
54
+
55
+ getSource(file: string, content: string): SourceFile {
56
+ const filePath = normalize(file);
57
+
58
+ let source = this.project.getSourceFile(filePath);
59
+
60
+ if (!source) {
61
+ source = this.project.createSourceFile(filePath, content, {
62
+ overwrite: true,
63
+ scriptKind: getScriptKind(filePath),
64
+ });
65
+ } else {
66
+ source.replaceWithText(content);
67
+ }
68
+
69
+ return source;
70
+ }
71
+
72
+ getChecker() {
73
+ return this.project.getTypeChecker().compilerObject;
74
+ }
75
+ }
@@ -0,0 +1,53 @@
1
+ import { Node, ts } from "ts-morph";
2
+
3
+ export type ResolveResult = {
4
+ values: string[];
5
+ dynamic: boolean;
6
+ };
7
+
8
+ export function resolveNodeValue(
9
+ node: Node,
10
+ checker: ts.TypeChecker
11
+ ): ResolveResult | null {
12
+ if (!Node.isIdentifier(node)) return null;
13
+
14
+ const symbol = checker.getSymbolAtLocation(node.compilerNode);
15
+ if (!symbol) return null;
16
+
17
+ const decls = symbol.getDeclarations() ?? [];
18
+
19
+ for (const d of decls) {
20
+ if (ts.isVariableDeclaration(d) && d.initializer) {
21
+ const init = d.initializer;
22
+
23
+ if (ts.isStringLiteral(init)) {
24
+ return { values: [init.text], dynamic: false };
25
+ }
26
+
27
+ if (ts.isNoSubstitutionTemplateLiteral(init)) {
28
+ return { values: [init.text], dynamic: false };
29
+ }
30
+
31
+ if (ts.isTemplateExpression(init)) {
32
+ let result = init.head.text;
33
+
34
+ for (const span of init.templateSpans) {
35
+ result += "*";
36
+ result += span.literal.text;
37
+ }
38
+
39
+ return { values: [result], dynamic: true };
40
+ }
41
+ }
42
+
43
+ if (ts.isParameter(d) && d.initializer) {
44
+ const init = d.initializer;
45
+
46
+ if (ts.isStringLiteral(init)) {
47
+ return { values: [init.text], dynamic: false };
48
+ }
49
+ }
50
+ }
51
+
52
+ return null;
53
+ }
@@ -0,0 +1,90 @@
1
+ import { SourceFile, SyntaxKind, Node, ts } from "ts-morph";
2
+ import { extractEventsFromArgs } from "./extract";
3
+ import { resolveFunctionFromCall } from "./callResolver";
4
+ import { getForwardedParamIndex } from "./flow";
5
+
6
+ function getCallName(expr: Node): string {
7
+ if (Node.isIdentifier(expr)) return expr.getText();
8
+ if (Node.isPropertyAccessExpression(expr)) return expr.getName();
9
+ return expr.getText();
10
+ }
11
+
12
+ function isTrackLike(name: string) {
13
+ return /(track|trackFeature)/i.test(name);
14
+ }
15
+
16
+ function pickArg(args: Node[]) {
17
+ return (
18
+ args.find(Node.isStringLiteral) ??
19
+ args.find(Node.isTemplateExpression) ??
20
+ args.find(Node.isObjectLiteralExpression) ??
21
+ args[0]
22
+ );
23
+ }
24
+
25
+ export function scanSource(
26
+ source: SourceFile,
27
+ checker: ts.TypeChecker
28
+ ) {
29
+ const events = new Set<string>();
30
+
31
+ // CALLS
32
+ for (const call of source.getDescendantsOfKind(SyntaxKind.CallExpression)) {
33
+ const expr = call.getExpression();
34
+ const name = getCallName(expr);
35
+
36
+ if (!isTrackLike(name)) continue;
37
+
38
+ const args = call.getArguments();
39
+ if (!args.length) continue;
40
+
41
+ let target = pickArg(args);
42
+
43
+ const fn = resolveFunctionFromCall(expr);
44
+
45
+ if (fn) {
46
+ const index = getForwardedParamIndex(fn);
47
+ if (index !== null && args[index]) {
48
+ target = args[index];
49
+ }
50
+ }
51
+
52
+ extractEventsFromArgs([target], checker).forEach(({ value }) => {
53
+ if (isValid(value)) events.add(value);
54
+ });
55
+ }
56
+
57
+ // JSX
58
+ const jsx = [
59
+ ...source.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
60
+ ...source.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
61
+ ];
62
+
63
+ for (const el of jsx) {
64
+ for (const attr of el.getAttributes()) {
65
+ if (!Node.isJsxAttribute(attr)) continue;
66
+
67
+ const name = attr.getNameNode().getText();
68
+ if (!/(event|name|action)/i.test(name)) continue;
69
+
70
+ const init = attr.getInitializer();
71
+ if (!init) continue;
72
+
73
+ const expr = Node.isJsxExpression(init)
74
+ ? init.getExpression()
75
+ : init;
76
+
77
+ if (!expr) continue;
78
+
79
+ extractEventsFromArgs([expr], checker).forEach(({ value }) => {
80
+ if (isValid(value)) events.add(value);
81
+ });
82
+ }
83
+ }
84
+
85
+ return events;
86
+ }
87
+
88
+ function isValid(v: string) {
89
+ return !!v && v.length > 1 && !v.includes(" ");
90
+ }
@@ -0,0 +1,42 @@
1
+ import { Node, ts } from "ts-morph";
2
+
3
+ export type ResolveResult = {
4
+ values: string[];
5
+ dynamic: boolean;
6
+ };
7
+
8
+ export function resolveNodeValue(
9
+ node: Node,
10
+ checker: ts.TypeChecker
11
+ ): ResolveResult | null {
12
+
13
+ if (Node.isIdentifier(node)) {
14
+ const symbol = checker.getSymbolAtLocation(node.compilerNode);
15
+ if (!symbol) return null;
16
+
17
+ const decls = symbol.getDeclarations() ?? [];
18
+
19
+ for (const d of decls) {
20
+
21
+ if (ts.isVariableDeclaration(d) && d.initializer) {
22
+ if (Node.isStringLiteral(d.initializer as any)) {
23
+ return {
24
+ values: [(d.initializer as any).text],
25
+ dynamic: false
26
+ };
27
+ }
28
+ }
29
+
30
+ if (ts.isParameter(d) && d.initializer) {
31
+ if (Node.isStringLiteral(d.initializer as any)) {
32
+ return {
33
+ values: [(d.initializer as any).text],
34
+ dynamic: false
35
+ };
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ return null;
42
+ }
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
@@ -1,3 +1,5 @@
1
+ export type ScanMode = "strict" | "hybrid" | "discovery";
2
+
1
3
  export type ComponentWrapper = {
2
4
  name: string;
3
5
  prop: string;
@@ -12,6 +14,7 @@ export type EventraConfig = {
12
14
  apiKey?: string;
13
15
  endpoint?: string;
14
16
  events: string[];
17
+ mode?: ScanMode;
15
18
  wrappers: ComponentWrapper[];
16
19
  functionWrappers: FunctionWrapper[];
17
20
  aliases?: Record<string, string>;
@@ -20,8 +23,3 @@ export type EventraConfig = {
20
23
  exclude: string[];
21
24
  };
22
25
  };
23
-
24
- export type ExtractedEvent = {
25
- value: string;
26
- dynamic: boolean;
27
- };
@@ -1,25 +1,24 @@
1
- export function parseVue(
2
- content: string
3
- ) {
4
- const template =
5
- content.match(
6
- /<template[\s\S]*?>([\s\S]*?)<\/template>/
7
- );
1
+ export function parseVue(content: string) {
2
+ const scriptMatch = content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/);
3
+ const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
8
4
 
9
- const script =
10
- content.match(
11
- /<script[\s\S]*?>([\s\S]*?)<\/script>/
12
- );
5
+ const script = scriptMatch?.[1] ?? "";
6
+ let template = templateMatch?.[1] ?? "";
13
7
 
14
- return `
15
- ${script?.[1] ?? ""}
8
+ template = template.replace(/<!--[\s\S]*?-->/g, "");
16
9
 
17
- function __vue_template__() {
18
- return (
19
- <>
20
- ${template?.[1] ?? ""}
21
- </>
22
- );
23
- }
10
+ template = template
11
+ .replace(/v-if="[^"]+"/g, "")
12
+ .replace(/v-for="[^"]+"/g, "")
13
+ .replace(/@click=/g, "onClick=");
14
+
15
+ const jsx = `
16
+ const __VUE_JSX__ = (
17
+ <>
18
+ ${template}
19
+ </>
20
+ );
24
21
  `;
22
+
23
+ return script + "\n" + jsx;
25
24
  }
@@ -0,0 +1,18 @@
1
+ import { detectParser } from "./parsers/router";
2
+ import { parseVue } from "./parsers/vue";
3
+ import { parseSvelte } from "./parsers/svelte";
4
+ import { parseAstro } from "./parsers/astro";
5
+
6
+ export function processFile(file: string, content: string) {
7
+ const parser = detectParser(file);
8
+
9
+ if (parser === "vue") content = parseVue(content);
10
+ if (parser === "svelte") content = parseSvelte(content);
11
+ if (parser === "astro") content = parseAstro(content);
12
+
13
+ const virtualFile = file.endsWith(".vue")
14
+ ? file + ".tsx"
15
+ : file;
16
+
17
+ return { content, virtualFile };
18
+ }
@@ -13,66 +13,221 @@ const express = (...args: any[]) => ({
13
13
 
14
14
  const app = express();
15
15
 
16
- // ===== direct =====
16
+ // =========================
17
+ // BASIC
18
+ // =========================
19
+
17
20
  track("express_event");
18
21
 
19
- // ===== middleware =====
22
+ // =========================
23
+ // VARIABLES
24
+ // =========================
25
+
26
+ const eventName = "variable_event";
27
+ track(eventName);
28
+
29
+ let anotherEvent;
30
+ anotherEvent = "assigned_event";
31
+ track(anotherEvent);
32
+
33
+ // =========================
34
+ // TEMPLATE
35
+ // =========================
36
+
37
+ track(`template_event`);
38
+
39
+ const id = "123";
40
+ track(`template_${id}`); // dynamic
41
+
42
+ // =========================
43
+ // MIDDLEWARE
44
+ // =========================
45
+
20
46
  app.use(() => {
21
47
  track("middleware_event");
22
48
  });
23
49
 
24
- // ===== get =====
50
+ // =========================
51
+ // ROUTES
52
+ // =========================
53
+
25
54
  app.get("/", () => {
26
55
  track("get_event");
27
56
  });
28
57
 
29
- // ===== post =====
30
58
  app.post("/users", () => {
31
59
  track("post_event");
32
60
  });
33
61
 
34
- // ===== put =====
35
62
  app.put("/users", () => {
36
63
  track("put_event");
37
64
  });
38
65
 
39
- // ===== delete =====
40
66
  app.delete("/users", () => {
41
67
  track("delete_event");
42
68
  });
43
69
 
44
- // ===== nested =====
70
+ // =========================
71
+ // NESTED
72
+ // =========================
73
+
45
74
  app.get("/nested", () => {
46
75
  if (true) {
47
76
  track("nested_event");
48
77
  }
49
78
  });
50
79
 
51
- // ===== async =====
80
+ // =========================
81
+ // ASYNC
82
+ // =========================
83
+
52
84
  app.get("/async", async () => {
53
85
  track("async_event");
54
86
  });
55
87
 
56
- // ===== arrow =====
88
+ // =========================
89
+ // ARROW HANDLER
90
+ // =========================
91
+
57
92
  const handler = () => {
58
93
  track("arrow_event");
59
94
  };
60
95
 
61
96
  app.get("/arrow", handler);
62
97
 
63
- // ===== function =====
98
+ // =========================
99
+ // FUNCTION DECLARATION
100
+ // =========================
101
+
64
102
  function service() {
65
103
  track("function_event");
66
104
  }
67
105
 
68
- // ===== class =====
106
+ // =========================
107
+ // CLASS
108
+ // =========================
109
+
69
110
  class Service {
70
111
  run() {
71
112
  track("class_event");
72
113
  }
73
114
  }
74
115
 
75
- // ===== listen =====
116
+ // =========================
117
+ // LISTEN
118
+ // =========================
119
+
76
120
  app.listen(3000, () => {
77
121
  track("listen_event");
78
122
  });
123
+
124
+ // =========================
125
+ // OBJECT WRAPPER
126
+ // =========================
127
+
128
+ const analytics = {
129
+ track
130
+ };
131
+
132
+ analytics.track("object_event");
133
+
134
+ // nested object
135
+ const analyticsDeep = {
136
+ events: {
137
+ track
138
+ }
139
+ };
140
+
141
+ analyticsDeep.events.track("nested_object_event");
142
+
143
+ // =========================
144
+ // CONDITIONALS
145
+ // =========================
146
+
147
+ if (true) {
148
+ track("if_event");
149
+ } else {
150
+ track("else_event");
151
+ }
152
+
153
+ // ternary
154
+ true
155
+ ? track("ternary_true")
156
+ : track("ternary_false");
157
+
158
+ // =========================
159
+ // ARRAYS
160
+ // =========================
161
+
162
+ [
163
+ () => track("array_1"),
164
+ () => track("array_2")
165
+ ];
166
+
167
+ // =========================
168
+ // LOOP
169
+ // =========================
170
+
171
+ for (let i = 0; i < 1; i++) {
172
+ track("loop_event");
173
+ }
174
+
175
+ // =========================
176
+ // TRY / CATCH
177
+ // =========================
178
+
179
+ try {
180
+ track("try_event");
181
+ } catch {
182
+ track("catch_event");
183
+ }
184
+
185
+ // =========================
186
+ // RETURN
187
+ // =========================
188
+
189
+ function returnTest() {
190
+ return track("return_event");
191
+ }
192
+
193
+ // =========================
194
+ // PARAM DEFAULT
195
+ // =========================
196
+
197
+ function withDefault(e = "default_event") {
198
+ track(e);
199
+ }
200
+
201
+ // =========================
202
+ // INLINE OBJECT
203
+ // =========================
204
+
205
+ track({
206
+ name: "object_payload_event"
207
+ } as any);
208
+
209
+ // =========================
210
+ // OPTIONAL CHAINING
211
+ // =========================
212
+
213
+ analytics?.track?.("optional_chain_event");
214
+
215
+ // =========================
216
+ // EDGE CASES
217
+ // =========================
218
+
219
+ // not first argument (should be ignored usually)
220
+ // @ts-ignore
221
+ track("real_event", "extra");
222
+
223
+ // no args
224
+ // @ts-ignore
225
+ track();
226
+
227
+ // dynamic identifier (should be dynamic)
228
+ const dynamicEvent = Math.random() > 0.5 ? "a" : "b";
229
+ track(dynamicEvent);
230
+
231
+ // =========================
232
+ // END
233
+ // =========================