@eventra_dev/eventra-cli 0.0.19 → 0.1.2

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 (41) 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 +34 -0
  5. package/dist/engine/engine.js +47 -0
  6. package/dist/engine/graph.js +25 -0
  7. package/dist/engine/hash.js +10 -0
  8. package/dist/engine/languageService.js +67 -0
  9. package/dist/engine/resolver.js +69 -0
  10. package/dist/engine/scanner.js +50 -0
  11. package/dist/utils/parsers/vue.js +16 -12
  12. package/dist/utils/processFile.js +20 -0
  13. package/package.json +1 -1
  14. package/src/commands/check.ts +19 -151
  15. package/src/commands/sync.ts +23 -194
  16. package/src/commands/watch.ts +119 -66
  17. package/src/engine/callResolver.ts +43 -0
  18. package/src/engine/engine.ts +60 -0
  19. package/src/engine/graph.ts +23 -0
  20. package/src/engine/hash.ts +5 -0
  21. package/src/engine/languageService.ts +73 -0
  22. package/src/engine/resolver.ts +86 -0
  23. package/src/engine/scanner.ts +61 -0
  24. package/src/types.ts +5 -6
  25. package/src/utils/parsers/vue.ts +19 -20
  26. package/src/utils/processFile.ts +18 -0
  27. package/tests/fixtures/backend/express/app.ts +167 -12
  28. package/tests/fixtures/wrappers/component/test.tsx +1 -1
  29. package/tests/run.ts +4 -17
  30. package/dist/utils/detect-aliases.js +0 -39
  31. package/dist/utils/detect-wrappers.js +0 -84
  32. package/dist/utils/extract.js +0 -56
  33. package/dist/utils/scanners/component-wrappers.js +0 -73
  34. package/dist/utils/scanners/function-wrappers.js +0 -83
  35. package/dist/utils/scanners/track.js +0 -64
  36. package/src/utils/detect-aliases.ts +0 -46
  37. package/src/utils/detect-wrappers.ts +0 -119
  38. package/src/utils/extract.ts +0 -72
  39. package/src/utils/scanners/component-wrappers.ts +0 -109
  40. package/src/utils/scanners/function-wrappers.ts +0 -137
  41. package/src/utils/scanners/track.ts +0 -99
@@ -0,0 +1,86 @@
1
+ import ts from "typescript";
2
+
3
+ export type ResolveResult = {
4
+ values: string[];
5
+ dynamic: boolean;
6
+ };
7
+
8
+ export function resolveNodeValue(
9
+ node: ts.Node,
10
+ checker: ts.TypeChecker
11
+ ): ResolveResult | null {
12
+
13
+ if (ts.isStringLiteral(node)) {
14
+ return { values: [node.text], dynamic: false };
15
+ }
16
+
17
+ if (ts.isTemplateExpression(node)) {
18
+ let result = node.head.text;
19
+
20
+ for (const span of node.templateSpans) {
21
+ result += "*";
22
+ result += span.literal.text;
23
+ }
24
+
25
+ return { values: [result], dynamic: true };
26
+ }
27
+
28
+ if (ts.isConditionalExpression(node)) {
29
+ const a = resolveNodeValue(node.whenTrue, checker);
30
+ const b = resolveNodeValue(node.whenFalse, checker);
31
+
32
+ return {
33
+ values: [...new Set([...(a?.values ?? []), ...(b?.values ?? [])])],
34
+ dynamic: true,
35
+ };
36
+ }
37
+
38
+ if (ts.isIdentifier(node)) {
39
+ let symbol = checker.getSymbolAtLocation(node);
40
+ if (!symbol) return null;
41
+
42
+ if (symbol.flags & ts.SymbolFlags.Alias) {
43
+ symbol = checker.getAliasedSymbol(symbol);
44
+ }
45
+
46
+ for (const decl of symbol.getDeclarations() ?? []) {
47
+ if (ts.isVariableDeclaration(decl) && decl.initializer) {
48
+ return resolveNodeValue(decl.initializer, checker);
49
+ }
50
+
51
+ if (ts.isEnumMember(decl) && decl.initializer) {
52
+ if (ts.isStringLiteral(decl.initializer)) {
53
+ return {
54
+ values: [decl.initializer.text],
55
+ dynamic: false,
56
+ };
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ if (ts.isPropertyAccessExpression(node)) {
63
+ const symbol = checker.getSymbolAtLocation(node);
64
+
65
+ if (symbol) {
66
+ let s = symbol;
67
+
68
+ if (s.flags & ts.SymbolFlags.Alias) {
69
+ s = checker.getAliasedSymbol(s);
70
+ }
71
+
72
+ for (const decl of s.getDeclarations() ?? []) {
73
+ if (ts.isEnumMember(decl) && decl.initializer) {
74
+ if (ts.isStringLiteral(decl.initializer)) {
75
+ return {
76
+ values: [decl.initializer.text],
77
+ dynamic: false,
78
+ };
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ return null;
86
+ }
@@ -0,0 +1,61 @@
1
+ import ts from "typescript";
2
+ import { resolveNodeValue } from "./resolver";
3
+ import { resolveFunctionFromCall } from "./callResolver";
4
+
5
+ export function scanSource(
6
+ source: ts.SourceFile,
7
+ checker: ts.TypeChecker,
8
+ visited = new Set<string>()
9
+ ) {
10
+ const events = new Set<string>();
11
+
12
+ if (visited.has(source.fileName)) return events;
13
+ visited.add(source.fileName);
14
+
15
+ function visit(node: ts.Node) {
16
+ if (ts.isCallExpression(node)) {
17
+ const name = getCallName(node.expression);
18
+
19
+ if (/(track|event|capture|send)/i.test(name)) {
20
+ let target = node.arguments[0];
21
+
22
+ const fn = resolveFunctionFromCall(node.expression, checker);
23
+
24
+ // 🔥 cross-file
25
+ if (fn) {
26
+ const sf = fn.getSourceFile();
27
+
28
+ if (!visited.has(sf.fileName)) {
29
+ scanSource(sf, checker, visited).forEach(e => events.add(e));
30
+ }
31
+ }
32
+
33
+ if (target) {
34
+ const res = resolveNodeValue(target, checker);
35
+
36
+ res?.values.forEach(v => {
37
+ if (v && v.length > 1 && !v.includes(" ")) {
38
+ events.add(v);
39
+ }
40
+ });
41
+ }
42
+ }
43
+ }
44
+
45
+ ts.forEachChild(node, visit);
46
+ }
47
+
48
+ visit(source);
49
+
50
+ return events;
51
+ }
52
+
53
+ function getCallName(expr: ts.Expression): string {
54
+ if (ts.isIdentifier(expr)) return expr.text;
55
+
56
+ if (ts.isPropertyAccessExpression(expr)) {
57
+ return expr.name.text;
58
+ }
59
+
60
+ return "";
61
+ }
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>;
@@ -23,9 +24,7 @@ export type EventraConfig = {
23
24
  };
24
25
  };
25
26
 
26
- export type ExtractedEvent = {
27
- value: string;
27
+ export type ResolveResult = {
28
+ values: string[];
28
29
  dynamic: boolean;
29
- file?: string;
30
- line?: number;
31
30
  };
@@ -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 =
@@ -1,39 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectAliases = detectAliases;
4
- const ts_morph_1 = require("ts-morph");
5
- function detectAliases(source) {
6
- const aliases = {};
7
- const vars = source.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration);
8
- for (const v of vars) {
9
- const name = v.getName();
10
- const init = v.getInitializer();
11
- if (!init)
12
- continue;
13
- // const A = "event"
14
- if (ts_morph_1.Node.isStringLiteral(init)) {
15
- aliases[name] = init.getLiteralText();
16
- continue;
17
- }
18
- // const A = `event`
19
- if (init.getKind() === ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
20
- aliases[name] = init.getText().replace(/`/g, "");
21
- continue;
22
- }
23
- // const EVENTS = { CLICK: "click" }
24
- if (ts_morph_1.Node.isObjectLiteralExpression(init)) {
25
- for (const p of init.getProperties()) {
26
- if (!ts_morph_1.Node.isPropertyAssignment(p))
27
- continue;
28
- const key = p.getName();
29
- const val = p.getInitializer();
30
- if (!val)
31
- continue;
32
- if (ts_morph_1.Node.isStringLiteral(val)) {
33
- aliases[`${name}.${key}`] = val.getLiteralText();
34
- }
35
- }
36
- }
37
- }
38
- return aliases;
39
- }
@@ -1,84 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.detectWrappers = detectWrappers;
4
- const ts_morph_1 = require("ts-morph");
5
- function detectWrappers(source) {
6
- return {
7
- functions: detectFunctionWrappers(source),
8
- components: detectComponentWrappers(source)
9
- };
10
- }
11
- // FUNCTIONS
12
- function detectFunctionWrappers(source) {
13
- const result = new Map();
14
- const functions = [
15
- ...source.getDescendantsOfKind(ts_morph_1.SyntaxKind.FunctionDeclaration),
16
- ...source.getDescendantsOfKind(ts_morph_1.SyntaxKind.ArrowFunction),
17
- ...source.getDescendantsOfKind(ts_morph_1.SyntaxKind.FunctionExpression)
18
- ];
19
- for (const fn of functions) {
20
- const body = fn.getBody();
21
- if (!body)
22
- continue;
23
- const calls = body.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
24
- for (const call of calls) {
25
- if (!isTrackCall(call))
26
- continue;
27
- const name = getFunctionName(fn);
28
- if (!name)
29
- continue;
30
- const arg = call.getArguments()[0];
31
- let eventField;
32
- if (arg && ts_morph_1.Node.isObjectLiteralExpression(arg)) {
33
- const prop = arg.getProperties()[0];
34
- if (prop && ts_morph_1.Node.isPropertyAssignment(prop)) {
35
- eventField = prop.getName();
36
- }
37
- }
38
- result.set(name, { name, event: eventField });
39
- }
40
- }
41
- return [...result.values()];
42
- }
43
- function isTrackCall(call) {
44
- const expr = call.getExpression();
45
- if (ts_morph_1.Node.isIdentifier(expr)) {
46
- return expr.getText() === "track";
47
- }
48
- if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
49
- return expr.getName() === "track";
50
- }
51
- return false;
52
- }
53
- function getFunctionName(fn) {
54
- if (ts_morph_1.Node.isFunctionDeclaration(fn)) {
55
- return fn.getName() ?? null;
56
- }
57
- const varDecl = fn.getFirstAncestorByKind(ts_morph_1.SyntaxKind.VariableDeclaration);
58
- return varDecl?.getName() ?? null;
59
- }
60
- // COMPONENTS
61
- function detectComponentWrappers(source) {
62
- const result = new Map();
63
- const elements = [
64
- ...source.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxOpeningElement),
65
- ...source.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxSelfClosingElement)
66
- ];
67
- for (const el of elements) {
68
- const name = el.getTagNameNode().getText();
69
- for (const attr of el.getAttributes()) {
70
- const a = attr.asKind(ts_morph_1.SyntaxKind.JsxAttribute);
71
- if (!a)
72
- continue;
73
- const prop = a.getNameNode().getText();
74
- const val = a.getInitializer();
75
- if (!val)
76
- continue;
77
- if (ts_morph_1.Node.isStringLiteral(val) ||
78
- ts_morph_1.Node.isJsxExpression(val)) {
79
- result.set(name, { name, prop });
80
- }
81
- }
82
- }
83
- return [...result.values()];
84
- }