@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
@@ -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/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;
@@ -6,14 +8,13 @@ export type ComponentWrapper = {
6
8
  export type FunctionWrapper = {
7
9
  name: string;
8
10
  event?: string;
9
- file?: string;
10
- line?: number;
11
11
  };
12
12
 
13
13
  export type EventraConfig = {
14
14
  apiKey?: string;
15
15
  endpoint?: string;
16
16
  events: string[];
17
+ mode?: ScanMode;
17
18
  wrappers: ComponentWrapper[];
18
19
  functionWrappers: FunctionWrapper[];
19
20
  aliases?: Record<string, string>;
@@ -22,10 +23,3 @@ export type EventraConfig = {
22
23
  exclude: string[];
23
24
  };
24
25
  };
25
-
26
- export type ExtractedEvent = {
27
- value: string;
28
- dynamic: boolean;
29
- file?: string;
30
- line?: number;
31
- };
@@ -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
+ // =========================
@@ -43,7 +43,7 @@ export default function WrapperTest() {
43
43
  ]}
44
44
 
45
45
  {/* wrapper components */}
46
- <TrackedButton event="tracked_button" />
46
+ <TrackedButton event={eventName} />
47
47
  <MyButton event="my_button" />
48
48
 
49
49
  {/* multiline */}
package/tests/run.ts CHANGED
@@ -18,7 +18,7 @@ const fixtures = [
18
18
  "wrappers/function"
19
19
  ];
20
20
 
21
- function runCLI(args: string[], cwd: string) {
21
+ function runCLI(args: string[], cwd: string): string {
22
22
  const result = spawnSync(
23
23
  process.execPath,
24
24
  [CLI, ...args],
@@ -35,23 +35,12 @@ function runCLI(args: string[], cwd: string) {
35
35
  function ensureTestConfig(dir: string) {
36
36
  const configPath = path.join(dir, "eventra.json");
37
37
 
38
- if (fs.existsSync(configPath)) return;
39
-
40
38
  const config = {
41
39
  apiKey: "",
42
40
  events: [],
43
- wrappers: [
44
- {
45
- name: "Button",
46
- prop: "event"
47
- }
48
- ],
49
- functionWrappers: [
50
- {
51
- name: "trackFeature",
52
- path: "0"
53
- }
54
- ],
41
+ wrappers: [],
42
+ functionWrappers: [],
43
+ aliases: {},
55
44
  sync: {
56
45
  include: [
57
46
  "**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
@@ -90,8 +79,6 @@ function runFixture(name: string) {
90
79
 
91
80
  ensureTestConfig(dir);
92
81
 
93
- runCLI(["init"], dir);
94
-
95
82
  const output = runCLI(["sync"], dir);
96
83
 
97
84
  const match =