@eventra_dev/eventra-cli 2.0.10 → 2.2.0

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.
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * Built-in `sinks` presets, plus the npm packages each one is recognised by.
4
+ *
5
+ * Package names and what they export were checked against the npm registry and the packages' own
6
+ * type declarations on 2026-10-08 (`pnpm check:presets` re-checks the names against the registry,
7
+ * so a rename or a deprecation shows up there instead of as a silent detection gap).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SINK_PRESETS = exports.PRESET_PACKAGES = void 0;
11
+ exports.detectSinkPresets = detectSinkPresets;
12
+ const pkg = (name, flags = {}) => ({ name, ...flags });
13
+ exports.PRESET_PACKAGES = {
14
+ posthog: [
15
+ pkg("posthog-js"),
16
+ pkg("@posthog/react"),
17
+ pkg("posthog-js-lite"),
18
+ pkg("posthog-node"),
19
+ pkg("posthog-react-native"),
20
+ ],
21
+ segment: [
22
+ pkg("@segment/analytics-next"),
23
+ pkg("@segment/analytics-node"),
24
+ pkg("@segment/analytics-react-native"),
25
+ pkg("@segment/analytics.js-core", { legacy: true }),
26
+ pkg("analytics-node", { legacy: true }),
27
+ pkg("@segment/snippet", { detectOnly: true }),
28
+ ],
29
+ amplitude: [
30
+ pkg("@amplitude/analytics-browser"),
31
+ pkg("@amplitude/analytics-node"),
32
+ pkg("@amplitude/analytics-react-native"),
33
+ pkg("@amplitude/marketing-analytics-browser"),
34
+ pkg("@amplitude/unified"),
35
+ pkg("amplitude-js", { legacy: true }),
36
+ ],
37
+ ga4: [
38
+ pkg("react-ga4"),
39
+ pkg("vue-gtag"),
40
+ pkg("vue-gtag-next", { legacy: true }),
41
+ pkg("@next/third-parties", { detectOnly: true }),
42
+ pkg("nuxt-gtag", { detectOnly: true }),
43
+ pkg("@types/gtag.js", { detectOnly: true }),
44
+ ],
45
+ };
46
+ /** Packages a rule can vet a receiver against: everything except the detect-only signals. */
47
+ const modulesOf = (preset, only) => exports.PRESET_PACKAGES[preset]
48
+ .filter((p) => !p.detectOnly && (only === undefined || only.includes(p.name)))
49
+ .map((p) => p.name);
50
+ exports.SINK_PRESETS = {
51
+ posthog: [
52
+ // posthog.capture("x") - the conventional name, posthog-js and posthog-node alike.
53
+ { callee: "posthog.capture", nameArg: 0, nameProperty: "event" },
54
+ // Any other receiver name, e.g. `const ph = usePostHog(); ph.capture("x")` or
55
+ // `const client = new PostHog(key); client.capture({ distinctId, event: "x" })`.
56
+ { callee: "capture", nameArg: 0, nameProperty: "event", modules: modulesOf("posthog") },
57
+ ],
58
+ segment: [
59
+ // analytics.track("x", props) from analytics.js / @segment/analytics-next, `window.analytics` included;
60
+ // analytics-node takes `analytics.track({ userId, event: "x" })`.
61
+ { callee: "analytics.track", nameArg: 0, nameProperty: "event" },
62
+ // `const [analytics] = await AnalyticsBrowser.load(...)`, `new Analytics({ writeKey })`, any variable name.
63
+ { callee: "track", nameArg: 0, nameProperty: "event", modules: modulesOf("segment") },
64
+ ],
65
+ amplitude: [
66
+ // amplitude.track("x") / amplitude.track({ event_type: "x" }) / the legacy amplitude.logEvent("x").
67
+ { callee: "amplitude.track", nameArg: 0, nameProperty: "event_type" },
68
+ { callee: "amplitude.logEvent", nameArg: 0, nameProperty: "event_type" },
69
+ // `amplitude.getInstance().logEvent("x")`, `const client = Amplitude.createInstance()`, any variable name.
70
+ { callee: "track", nameArg: 0, nameProperty: "event_type", modules: modulesOf("amplitude") },
71
+ { callee: "logEvent", nameArg: 0, nameProperty: "event_type", modules: modulesOf("amplitude") },
72
+ ],
73
+ ga4: [
74
+ // gtag("event", "x", params): the name is the second argument, and only for the "event" command
75
+ // (`gtag("config", id)` and `gtag("set", ...)` are not events). `window.gtag` and nuxt-gtag's
76
+ // `const { gtag } = useGtag()` match too.
77
+ { callee: "gtag", nameArg: 1, when: { arg: 0, equals: "event" } },
78
+ // @next/third-parties: sendGAEvent("event", "x", params).
79
+ { callee: "sendGAEvent", nameArg: 1, when: { arg: 0, equals: "event" } },
80
+ // nuxt-gtag: useTrackEvent("x", params), auto-imported.
81
+ { callee: "useTrackEvent", nameArg: 0 },
82
+ // vue-gtag: this.$gtag.event("x", params).
83
+ { callee: "$gtag.event", nameArg: 0 },
84
+ // vue-gtag: `import { event } from "vue-gtag"` or `const { event } = useGtag()`.
85
+ { callee: "event", nameArg: 0, modules: modulesOf("ga4", ["vue-gtag", "vue-gtag-next"]) },
86
+ // react-ga4: ReactGA.event("x", params) or ReactGA.event({ category, action: "x" }).
87
+ { callee: "event", nameArg: 0, nameProperty: "action", modules: modulesOf("ga4", ["react-ga4"]) },
88
+ ],
89
+ };
90
+ /** Which presets a project's dependency names point at, in preset order. */
91
+ function detectSinkPresets(dependencyNames) {
92
+ const detected = [];
93
+ for (const [preset, packages] of Object.entries(exports.PRESET_PACKAGES)) {
94
+ const present = packages.map((p) => p.name).filter((name) => dependencyNames.has(name));
95
+ if (present.length > 0)
96
+ detected.push({ preset, packages: present });
97
+ }
98
+ return detected;
99
+ }
@@ -32,11 +32,17 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.isTrustedPluginSpecifier = isTrustedPluginSpecifier;
37
40
  exports.createBuiltinPluginRegistry = createBuiltinPluginRegistry;
38
41
  exports.createPluginRegistry = createPluginRegistry;
42
+ const module_1 = require("module");
43
+ const path_1 = __importDefault(require("path"));
39
44
  const external_1 = require("./adapters/external");
45
+ const configuredSinks_1 = require("./builtins/configuredSinks");
40
46
  const eventra_sdk_1 = require("./builtins/eventra-sdk");
41
47
  const consoleDynamicEventReporter_1 = require("./builtins/consoleDynamicEventReporter");
42
48
  const registry_1 = require("./registry");
@@ -68,6 +74,19 @@ function pluginLoadHint(spec) {
68
74
  }
69
75
  return "";
70
76
  }
77
+ /**
78
+ * Plugins are installed in the user's project, not next to the CLI (a global
79
+ * install, npx or a pnpm layout would not find them from here), so resolve from
80
+ * the project first and fall back to normal resolution relative to the CLI.
81
+ */
82
+ function resolveFromProject(spec) {
83
+ try {
84
+ return (0, module_1.createRequire)(path_1.default.join(process.cwd(), "package.json")).resolve(spec);
85
+ }
86
+ catch {
87
+ return spec;
88
+ }
89
+ }
71
90
  async function loadExternalPlugin(registry, spec) {
72
91
  const trimmed = spec.trim();
73
92
  // Defensive-only: the sole caller (createPluginRegistry) already does
@@ -79,7 +98,7 @@ async function loadExternalPlugin(registry, spec) {
79
98
  return;
80
99
  let mod;
81
100
  try {
82
- mod = await Promise.resolve(`${trimmed}`).then(s => __importStar(require(s)));
101
+ mod = await Promise.resolve(`${resolveFromProject(trimmed)}`).then(s => __importStar(require(s)));
83
102
  }
84
103
  catch (err) {
85
104
  // Node's dynamic import() only ever rejects with an Error (module
@@ -107,12 +126,13 @@ function createBuiltinPluginRegistry() {
107
126
  /** Built-ins + plugins listed in eventra.json `plugins` array. */
108
127
  async function createPluginRegistry(config) {
109
128
  const registry = createBuiltinPluginRegistry();
129
+ (0, configuredSinks_1.registerConfiguredSinks)(registry, config.sinks);
110
130
  for (const spec of config.plugins ?? []) {
111
131
  const trimmed = spec.trim();
112
132
  if (!trimmed)
113
133
  continue;
114
134
  if (!isTrustedPluginSpecifier(trimmed)) {
115
- console.warn(`Eventra: skipping plugin "${trimmed}" — only official @eventra_dev/cli-plugin-* packages can be loaded.${pluginLoadHint(trimmed)}`);
135
+ console.warn(`Eventra: skipping plugin "${trimmed}" - only official @eventra_dev/cli-plugin-* packages can be loaded.${pluginLoadHint(trimmed)}`);
116
136
  continue;
117
137
  }
118
138
  await loadExternalPlugin(registry, trimmed);
@@ -5,6 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PluginRegistry = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
+ // the synthetic callee each framework plugin uses for a template `event=...`
9
+ const TEMPLATE_CALLEE = /^__eventra_[a-z]+_template_event__$/;
10
+ // `__eventra_x_template_event__(expr)` (callText may be cut with an ellipsis)
11
+ function templateExpression(occurrence) {
12
+ const open = occurrence.callText.indexOf("(");
13
+ let expression = open === -1 ? occurrence.callText : occurrence.callText.slice(open + 1);
14
+ expression = expression.endsWith(")") ? expression.slice(0, -1) : expression;
15
+ // Angular qualifies a bare name with `this.`; the template never writes it
16
+ return occurrence.calleeText.includes("_angular_") ? expression.replace(/^this\./, "") : expression;
17
+ }
8
18
  class PluginRegistry {
9
19
  preprocessors = [];
10
20
  sinkDetectors = [];
@@ -101,9 +111,48 @@ class PluginRegistry {
101
111
  return null;
102
112
  }
103
113
  async runDynamicEventReporters(occurrences) {
114
+ const attributed = this.attributeToSources(occurrences);
104
115
  for (const reporter of this.dynamicEventReporters) {
105
- await reporter.report({ occurrences });
116
+ await reporter.report({ occurrences: attributed });
117
+ }
118
+ }
119
+ /**
120
+ * Occurrences are found in virtual modules (`App.vue.ts`). Point them back
121
+ * at the file the user wrote, and flag the ones that come from generated
122
+ * code so reporters do not print a line number that exists nowhere.
123
+ */
124
+ attributeToSources(occurrences) {
125
+ if (this.sourceToVirtuals.size === 0) {
126
+ return occurrences;
106
127
  }
128
+ const virtualToSource = new Map();
129
+ for (const [source, virtuals] of this.sourceToVirtuals) {
130
+ for (const virtual of virtuals) {
131
+ if (virtual !== source) {
132
+ virtualToSource.set(virtual, source);
133
+ }
134
+ }
135
+ }
136
+ return occurrences.map((occurrence) => {
137
+ const source = virtualToSource.get(this.normalizePath(occurrence.fileName));
138
+ if (!source) {
139
+ return occurrence;
140
+ }
141
+ const template = TEMPLATE_CALLEE.test(occurrence.calleeText);
142
+ if (template) {
143
+ return {
144
+ ...occurrence,
145
+ fileName: source,
146
+ inTemplate: true,
147
+ eventExpression: templateExpression(occurrence),
148
+ };
149
+ }
150
+ // an Angular template module embeds the component class: lines there
151
+ // are generated, the real call is also reported from the class file
152
+ return source.toLowerCase().endsWith(".html")
153
+ ? { ...occurrence, fileName: source, inGeneratedCode: true }
154
+ : { ...occurrence, fileName: source };
155
+ });
107
156
  }
108
157
  normalizePath(fileName) {
109
158
  return path_1.default.resolve(fileName).replace(/\\/g, "/");
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "2.0.10",
3
+ "version": "2.2.0",
4
4
  "description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "and-1991",
8
+ "url": "https://github.com/and-1991"
9
+ },
6
10
  "type": "commonjs",
7
11
  "bin": {
8
12
  "eventra": "./dist/index.js"
@@ -31,7 +35,10 @@
31
35
  "test:unit": "vitest run",
32
36
  "test:coverage": "vitest run --coverage",
33
37
  "test:e2e": "pnpm run clean && pnpm run build && tsx tests/run.ts",
34
- "clean": "rm -rf dist"
38
+ "clean": "rm -rf dist",
39
+ "test:mutation": "stryker run",
40
+ "check:presets": "tsx scripts/check-preset-packages.ts",
41
+ "typecheck": "tsc --noEmit -p tsconfig.json"
35
42
  },
36
43
  "dependencies": {
37
44
  "chalk": "^4.1.2",
@@ -43,6 +50,8 @@
43
50
  "typescript": "^5.3.0"
44
51
  },
45
52
  "devDependencies": {
53
+ "@stryker-mutator/core": "9.6.1",
54
+ "@stryker-mutator/vitest-runner": "9.6.1",
46
55
  "@types/fs-extra": "^11.0.4",
47
56
  "@types/inquirer": "^9.0.9",
48
57
  "@types/node": "^20.0.0",