@eventra_dev/eventra-cli 0.0.5 → 0.0.7

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.
@@ -33,6 +33,9 @@ jobs:
33
33
  - name: Build CLI
34
34
  run: pnpm build
35
35
 
36
+ - name: Run tests
37
+ run: pnpm test
38
+
36
39
  - name: Publish to npm
37
40
  run: npm publish --access public
38
41
  env:
package/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
  [![npm version](https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
9
9
  [![TypeScript](https://img.shields.io/badge/typescript-ready-blue.svg)](https://www.typescriptlang.org/)
10
+ [![Tests](https://github.com/and-1991/eventra-cli/actions/workflows/release.yml/badge.svg)]()
11
+ [![License](https://img.shields.io/npm/l/@eventra_dev/eventra-cli)]()
10
12
 
11
13
  Eventra CLI automatically discovers feature usage events in your codebase and syncs them with Eventra.
12
14
 
@@ -5,31 +5,97 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.init = init;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const fs_extra_1 = __importDefault(require("fs-extra"));
9
- const path_1 = __importDefault(require("path"));
10
8
  const inquirer_1 = __importDefault(require("inquirer"));
11
9
  const config_1 = require("../utils/config");
12
10
  async function init() {
13
11
  console.log(chalk_1.default.blue("Initializing Eventra..."));
14
- const configPath = path_1.default.join(process.cwd(), config_1.CONFIG_NAME);
15
- if (await fs_extra_1.default.pathExists(configPath)) {
16
- console.log(chalk_1.default.yellow("eventra.json already exists"));
17
- return;
18
- }
19
- const answers = await inquirer_1.default.prompt([
12
+ // API KEY
13
+ const { apiKey } = await inquirer_1.default.prompt([
20
14
  {
21
15
  type: "input",
22
16
  name: "apiKey",
23
17
  message: "API Key (optional):"
24
18
  }
25
19
  ]);
20
+ console.log(chalk_1.default.gray("\nEventra automatically detects:"));
21
+ console.log(chalk_1.default.gray("• track('event')"));
22
+ console.log(chalk_1.default.gray("• tracker.track('event')"));
23
+ const wrappers = [];
24
+ const functionWrappers = [];
25
+ // COMPONENT WRAPPERS
26
+ console.log(chalk_1.default.blue("\nComponent wrappers"));
27
+ let addComponent = true;
28
+ while (addComponent) {
29
+ const { useWrapper } = await inquirer_1.default.prompt([
30
+ {
31
+ type: "confirm",
32
+ name: "useWrapper",
33
+ message: "Add component wrapper?",
34
+ default: false
35
+ }
36
+ ]);
37
+ if (!useWrapper)
38
+ break;
39
+ const { name, prop } = await inquirer_1.default.prompt([
40
+ {
41
+ type: "input",
42
+ name: "name",
43
+ message: "Component name:",
44
+ validate: (v) => v ? true : "Required"
45
+ },
46
+ {
47
+ type: "input",
48
+ name: "prop",
49
+ message: "Event prop:",
50
+ default: "event"
51
+ }
52
+ ]);
53
+ wrappers.push({
54
+ name,
55
+ prop
56
+ });
57
+ }
58
+ // FUNCTION WRAPPERS
59
+ console.log(chalk_1.default.blue("\nFunction wrappers"));
60
+ let addFunction = true;
61
+ while (addFunction) {
62
+ const { useWrapper } = await inquirer_1.default.prompt([
63
+ {
64
+ type: "confirm",
65
+ name: "useWrapper",
66
+ message: "Add function wrapper?",
67
+ default: false
68
+ }
69
+ ]);
70
+ if (!useWrapper)
71
+ break;
72
+ const { name, event } = await inquirer_1.default.prompt([
73
+ {
74
+ type: "input",
75
+ name: "name",
76
+ message: "Function name:"
77
+ },
78
+ {
79
+ type: "input",
80
+ name: "event",
81
+ message: "Event field (leave empty if string argument):",
82
+ default: ""
83
+ }
84
+ ]);
85
+ functionWrappers.push({
86
+ name,
87
+ event: event || undefined
88
+ });
89
+ }
26
90
  const config = {
27
- apiKey: answers.apiKey || "",
91
+ apiKey,
28
92
  events: [],
29
- wrappers: [],
30
- functionWrappers: [],
93
+ wrappers,
94
+ functionWrappers,
31
95
  sync: {
32
- include: ["**/*.{ts,tsx,js,jsx}"],
96
+ include: [
97
+ "**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
98
+ ],
33
99
  exclude: [
34
100
  "node_modules",
35
101
  "dist",
@@ -39,5 +105,6 @@ async function init() {
39
105
  }
40
106
  };
41
107
  await (0, config_1.saveConfig)(config);
42
- console.log(chalk_1.default.green("eventra.json created"));
108
+ console.log(chalk_1.default.green("\neventra.json created"));
109
+ console.log(chalk_1.default.gray("\nRun `eventra sync`"));
43
110
  }
@@ -28,6 +28,13 @@ async function sync() {
28
28
  const files = await (0, fast_glob_1.default)(config.sync.include, {
29
29
  ignore: config.sync.exclude
30
30
  });
31
+ const functionWrappers = (config.functionWrappers ?? []).map((w) => ({
32
+ name: w.name,
33
+ path: w.event
34
+ ? `0.${w.event}`
35
+ : "0"
36
+ }));
37
+ const componentWrappers = config.wrappers ?? [];
31
38
  for (const file of files) {
32
39
  const parser = (0, router_1.detectParser)(file);
33
40
  let content = await promises_1.default.readFile(file, "utf-8");
@@ -37,10 +44,13 @@ async function sync() {
37
44
  content = (0, svelte_1.parseSvelte)(content);
38
45
  if (parser === "astro")
39
46
  content = (0, astro_1.parseAstro)(content);
40
- const source = project.createSourceFile(file, content, { overwrite: true });
47
+ const virtualFile = parser === "ts"
48
+ ? file
49
+ : file + ".tsx";
50
+ const source = project.createSourceFile(virtualFile, content, { overwrite: true });
41
51
  (0, track_1.scanTrack)(source).forEach((e) => events.add(e));
42
- (0, function_wrappers_1.scanFunctionWrappers)(source, config.functionWrappers ?? []).forEach((e) => events.add(e));
43
- (0, component_wrappers_1.scanComponentWrappers)(source, config.wrappers ?? []).forEach((e) => events.add(e));
52
+ (0, function_wrappers_1.scanFunctionWrappers)(source, functionWrappers).forEach((e) => events.add(e));
53
+ (0, component_wrappers_1.scanComponentWrappers)(source, componentWrappers).forEach((e) => events.add(e));
44
54
  }
45
55
  const list = [...events].sort();
46
56
  config.events = list;
@@ -18,7 +18,7 @@ function normalizeConfig(config) {
18
18
  functionWrappers: config.functionWrappers ?? [],
19
19
  sync: config.sync ?? {
20
20
  include: [
21
- "**/*.{ts,tsx,js,jsx}"
21
+ "**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
22
22
  ],
23
23
  exclude: [
24
24
  "node_modules",
@@ -7,23 +7,38 @@ function extractEvent(call, path) {
7
7
  let node = call.getArguments()[Number(parts[0])];
8
8
  if (!node)
9
9
  return null;
10
+ node = unwrap(node);
10
11
  for (let i = 1; i < parts.length; i++) {
11
- if (ts_morph_1.Node.isObjectLiteralExpression(node)) {
12
- const obj = node;
13
- const prop = obj.getProperty(parts[i]);
14
- if (!prop)
15
- return null;
16
- if (ts_morph_1.Node.isPropertyAssignment(prop)) {
17
- const initializer = prop.getInitializer();
18
- if (!initializer)
19
- return null;
20
- node = initializer;
21
- }
12
+ if (!ts_morph_1.Node.isObjectLiteralExpression(node)) {
13
+ return null;
22
14
  }
15
+ const obj = node;
16
+ const prop = obj.getProperty(parts[i]);
17
+ if (!prop)
18
+ return null;
19
+ if (!ts_morph_1.Node.isPropertyAssignment(prop)) {
20
+ return null;
21
+ }
22
+ const initializer = prop.getInitializer();
23
+ if (!initializer)
24
+ return null;
25
+ node = unwrap(initializer);
23
26
  }
24
- if (node &&
25
- ts_morph_1.Node.isStringLiteral(node)) {
27
+ if (ts_morph_1.Node.isStringLiteral(node)) {
26
28
  return node.getLiteralText();
27
29
  }
30
+ if (node.getKind() ===
31
+ ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
32
+ return node
33
+ .getText()
34
+ .replace(/`/g, "");
35
+ }
28
36
  return null;
29
37
  }
38
+ function unwrap(node) {
39
+ let current = node;
40
+ while (ts_morph_1.Node.isParenthesizedExpression(current)) {
41
+ current = current.getExpression();
42
+ }
43
+ return current;
44
+ }
@@ -4,7 +4,15 @@ exports.parseVue = parseVue;
4
4
  function parseVue(content) {
5
5
  const template = content.match(/<template[\s\S]*?>([\s\S]*?)<\/template>/);
6
6
  const script = content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/);
7
- return ((template?.[1] ?? "") +
8
- "\n" +
9
- (script?.[1] ?? ""));
7
+ return `
8
+ ${script?.[1] ?? ""}
9
+
10
+ function __vue_template__() {
11
+ return (
12
+ <>
13
+ ${template?.[1] ?? ""}
14
+ </>
15
+ );
16
+ }
17
+ `;
10
18
  }
@@ -32,13 +32,11 @@ function scanComponentWrappers(source, wrappers) {
32
32
  const init = attrNode.getInitializer();
33
33
  if (!init)
34
34
  continue;
35
- // event="signup"
36
35
  if (init.getKind() ===
37
36
  ts_morph_1.SyntaxKind.StringLiteral) {
38
37
  const value = init.asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral);
39
38
  events.add(value.getLiteralText());
40
39
  }
41
- // event={"signup"}
42
40
  if (init.getKind() ===
43
41
  ts_morph_1.SyntaxKind.JsxExpression) {
44
42
  const expr = init
@@ -7,29 +7,65 @@ function scanFunctionWrappers(source, wrappers) {
7
7
  const events = new Set();
8
8
  const calls = source.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
9
9
  for (const call of calls) {
10
- const expression = call.getExpression();
11
- let name = null;
12
- // trackFeature()
13
- if (expression.getKind() ===
14
- ts_morph_1.SyntaxKind.Identifier) {
15
- name =
16
- expression.getText();
17
- }
18
- // analytics.trackFeature()
19
- if (expression.getKind() ===
20
- ts_morph_1.SyntaxKind.PropertyAccessExpression) {
21
- const prop = expression.asKindOrThrow(ts_morph_1.SyntaxKind.PropertyAccessExpression);
22
- name = prop.getName();
23
- }
10
+ const name = getFunctionName(call);
24
11
  if (!name)
25
12
  continue;
26
13
  for (const wrapper of wrappers) {
27
- if (name !== wrapper.name)
14
+ if (wrapper.name !== name)
28
15
  continue;
29
- const event = (0, extract_1.extractEvent)(call, wrapper.path);
16
+ const event = extractEventFromArgs(call, wrapper.event);
30
17
  if (event)
31
18
  events.add(event);
32
19
  }
33
20
  }
34
21
  return events;
35
22
  }
23
+ function getFunctionName(call) {
24
+ const expression = call.getExpression();
25
+ // trackFeature()
26
+ if (ts_morph_1.Node.isIdentifier(expression)) {
27
+ return expression.getText();
28
+ }
29
+ // analytics.trackFeature()
30
+ if (ts_morph_1.Node.isPropertyAccessExpression(expression)) {
31
+ return getDeepName(expression);
32
+ }
33
+ return null;
34
+ }
35
+ function getDeepName(node) {
36
+ let current = node;
37
+ let name = "";
38
+ while (ts_morph_1.Node.isPropertyAccessExpression(current)) {
39
+ name =
40
+ current.getName();
41
+ current =
42
+ current.getExpression();
43
+ }
44
+ return name;
45
+ }
46
+ function extractEventFromArgs(call, event) {
47
+ const args = call.getArguments();
48
+ for (let i = 0; i < args.length; i++) {
49
+ const arg = args[i];
50
+ // string case
51
+ if (!event) {
52
+ if (ts_morph_1.Node.isStringLiteral(arg)) {
53
+ return arg.getLiteralText();
54
+ }
55
+ // template literal
56
+ if (arg.getKind() ===
57
+ ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
58
+ return arg
59
+ .getText()
60
+ .replace(/`/g, "");
61
+ }
62
+ }
63
+ // object case
64
+ if (event) {
65
+ const result = (0, extract_1.extractEvent)(call, `${i}.${event}`);
66
+ if (result)
67
+ return result;
68
+ }
69
+ }
70
+ return null;
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Eventra CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "license": "MIT",
16
16
  "scripts": {
17
17
  "build": "tsc",
18
- "dev": "tsx src/index.ts"
18
+ "dev": "tsx src/index.ts",
19
+ "test": "tsx tests/run.ts"
19
20
  },
20
21
  "dependencies": {
21
22
  "chalk": "^4.1.2",
@@ -29,6 +30,7 @@
29
30
  "@types/fs-extra": "^11.0.4",
30
31
  "@types/inquirer": "^9.0.9",
31
32
  "@types/node": "^20.0.0",
33
+ "cross-spawn": "^7.0.6",
32
34
  "tsx": "^4.7.0",
33
35
  "typescript": "^5.3.0"
34
36
  }
@@ -1,47 +1,147 @@
1
1
  import chalk from "chalk";
2
- import fs from "fs-extra";
3
- import path from "path";
4
2
  import inquirer from "inquirer";
5
- import {
6
- CONFIG_NAME,
7
- saveConfig
8
- } from "../utils/config";
3
+
4
+ import { saveConfig } from "../utils/config";
5
+ import {ComponentWrapper, FunctionWrapper} from "../types";
9
6
 
10
7
  export async function init() {
11
8
  console.log(
12
9
  chalk.blue("Initializing Eventra...")
13
10
  );
14
11
 
15
- const configPath = path.join(
16
- process.cwd(),
17
- CONFIG_NAME
12
+ // API KEY
13
+ const { apiKey } =
14
+ await inquirer.prompt([
15
+ {
16
+ type: "input",
17
+ name: "apiKey",
18
+ message:
19
+ "API Key (optional):"
20
+ }
21
+ ]);
22
+
23
+ console.log(
24
+ chalk.gray(
25
+ "\nEventra automatically detects:"
26
+ )
27
+ );
28
+
29
+ console.log(
30
+ chalk.gray("• track('event')")
31
+ );
32
+
33
+ console.log(
34
+ chalk.gray(
35
+ "• tracker.track('event')"
36
+ )
18
37
  );
19
38
 
20
- if (await fs.pathExists(configPath)) {
21
- console.log(
22
- chalk.yellow(
23
- "eventra.json already exists"
24
- )
25
- );
26
- return;
39
+ const wrappers: ComponentWrapper[] = [];
40
+ const functionWrappers: FunctionWrapper[] = [];
41
+
42
+
43
+ // COMPONENT WRAPPERS
44
+ console.log(
45
+ chalk.blue(
46
+ "\nComponent wrappers"
47
+ )
48
+ );
49
+
50
+ let addComponent = true;
51
+
52
+ while (addComponent) {
53
+ const { useWrapper } =
54
+ await inquirer.prompt([
55
+ {
56
+ type: "confirm",
57
+ name: "useWrapper",
58
+ message:
59
+ "Add component wrapper?",
60
+ default: false
61
+ }
62
+ ]);
63
+
64
+ if (!useWrapper) break;
65
+
66
+ const { name, prop } =
67
+ await inquirer.prompt([
68
+ {
69
+ type: "input",
70
+ name: "name",
71
+ message: "Component name:",
72
+ validate: (v) =>
73
+ v ? true : "Required"
74
+ },
75
+ {
76
+ type: "input",
77
+ name: "prop",
78
+ message:
79
+ "Event prop:",
80
+ default: "event"
81
+ }
82
+ ]);
83
+
84
+ wrappers.push({
85
+ name,
86
+ prop
87
+ });
27
88
  }
28
89
 
29
- const answers = await inquirer.prompt([
30
- {
31
- type: "input",
32
- name: "apiKey",
33
- message:
34
- "API Key (optional):"
35
- }
36
- ]);
90
+ // FUNCTION WRAPPERS
91
+ console.log(
92
+ chalk.blue(
93
+ "\nFunction wrappers"
94
+ )
95
+ );
96
+
97
+ let addFunction = true;
98
+
99
+ while (addFunction) {
100
+ const { useWrapper } =
101
+ await inquirer.prompt([
102
+ {
103
+ type: "confirm",
104
+ name: "useWrapper",
105
+ message:
106
+ "Add function wrapper?",
107
+ default: false
108
+ }
109
+ ]);
110
+
111
+ if (!useWrapper) break;
112
+
113
+ const { name, event } =
114
+ await inquirer.prompt([
115
+ {
116
+ type: "input",
117
+ name: "name",
118
+ message:
119
+ "Function name:"
120
+ },
121
+ {
122
+ type: "input",
123
+ name: "event",
124
+ message:
125
+ "Event field (leave empty if string argument):",
126
+ default: ""
127
+ }
128
+ ]);
129
+
130
+ functionWrappers.push({
131
+ name,
132
+ event: event || undefined
133
+ });
134
+ }
37
135
 
38
136
  const config = {
39
- apiKey: answers.apiKey || "",
137
+ apiKey,
40
138
  events: [],
41
- wrappers: [],
42
- functionWrappers: [],
139
+ wrappers,
140
+ functionWrappers,
43
141
  sync: {
44
- include: ["**/*.{ts,tsx,js,jsx}"],
142
+ include: [
143
+ "**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
144
+ ],
45
145
  exclude: [
46
146
  "node_modules",
47
147
  "dist",
@@ -55,7 +155,13 @@ export async function init() {
55
155
 
56
156
  console.log(
57
157
  chalk.green(
58
- "eventra.json created"
158
+ "\neventra.json created"
159
+ )
160
+ );
161
+
162
+ console.log(
163
+ chalk.gray(
164
+ "\nRun `eventra sync`"
59
165
  )
60
166
  );
61
167
  }
@@ -41,6 +41,18 @@ export async function sync() {
41
41
  }
42
42
  );
43
43
 
44
+ const functionWrappers =
45
+ (config.functionWrappers ?? []).map(
46
+ (w) => ({
47
+ name: w.name,
48
+ path: w.event
49
+ ? `0.${w.event}`
50
+ : "0"
51
+ })
52
+ );
53
+ const componentWrappers =
54
+ config.wrappers ?? [];
55
+
44
56
  for (const file of files) {
45
57
  const parser =
46
58
  detectParser(file);
@@ -60,9 +72,14 @@ export async function sync() {
60
72
  if (parser === "astro")
61
73
  content = parseAstro(content);
62
74
 
75
+ const virtualFile =
76
+ parser === "ts"
77
+ ? file
78
+ : file + ".tsx";
79
+
63
80
  const source =
64
81
  project.createSourceFile(
65
- file,
82
+ virtualFile,
66
83
  content,
67
84
  { overwrite: true }
68
85
  );
@@ -71,16 +88,17 @@ export async function sync() {
71
88
  (e) => events.add(e)
72
89
  );
73
90
 
91
+
74
92
  scanFunctionWrappers(
75
93
  source,
76
- config.functionWrappers ?? []
94
+ functionWrappers
77
95
  ).forEach((e) =>
78
96
  events.add(e)
79
97
  );
80
98
 
81
99
  scanComponentWrappers(
82
100
  source,
83
- config.wrappers ?? []
101
+ componentWrappers
84
102
  ).forEach((e) =>
85
103
  events.add(e)
86
104
  );
package/src/types.ts CHANGED
@@ -5,7 +5,7 @@ export type ComponentWrapper = {
5
5
 
6
6
  export type FunctionWrapper = {
7
7
  name: string;
8
- path: string;
8
+ event?: string;
9
9
  };
10
10
 
11
11
  export type EventraConfig = {
@@ -15,7 +15,7 @@ export function normalizeConfig(
15
15
  config.functionWrappers ?? [],
16
16
  sync: config.sync ?? {
17
17
  include: [
18
- "**/*.{ts,tsx,js,jsx}"
18
+ "**/*.{ts,tsx,js,jsx,vue,svelte,astro}"
19
19
  ],
20
20
  exclude: [
21
21
  "node_modules",