@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.
- package/LICENSE +14 -2
- package/README.md +95 -12
- package/dist/analysis/extractor/extractor.js +1 -1
- package/dist/analysis/extractor/propagationExtractor.js +56 -8
- package/dist/analysis/resolver/resolver.js +7 -4
- package/dist/analysis/scanner/analyzer/propagationAnalyzer.js +5 -0
- package/dist/analysis/scanner/scanner.js +10 -3
- package/dist/analysis/sdk/eventraSdk.js +6 -1
- package/dist/analysis/symbols/symbolUtils.js +12 -0
- package/dist/analysis/symbols/wrapperRegistry.js +14 -5
- package/dist/cli/check.js +25 -9
- package/dist/cli/init.js +71 -12
- package/dist/cli/projectDependencies.js +119 -0
- package/dist/cli/send.js +31 -2
- package/dist/cli/sync.js +13 -10
- package/dist/cli/watch.js +163 -62
- package/dist/compiler/compilerContext.js +103 -5
- package/dist/compiler/documentRegistry.js +15 -1
- package/dist/compiler/importGraph.js +8 -0
- package/dist/config/config.js +52 -2
- package/dist/core/EventraEngine.js +129 -16
- package/dist/core/catalogMeta.js +91 -0
- package/dist/core/fileLimits.js +12 -0
- package/dist/core/projectScanner.js +14 -24
- package/dist/core/scanFilters.js +146 -0
- package/dist/core/scanResults.js +21 -7
- package/dist/core/skipped.js +77 -0
- package/dist/index.js +8 -5
- package/dist/plugin/builtins/configuredSinks.js +253 -0
- package/dist/plugin/builtins/consoleDynamicEventReporter.js +18 -2
- package/dist/plugin/builtins/sinkPresets.js +99 -0
- package/dist/plugin/loadPlugins.js +22 -2
- package/dist/plugin/registry.js +50 -1
- package/package.json +11 -2
|
@@ -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}"
|
|
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);
|
package/dist/plugin/registry.js
CHANGED
|
@@ -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
|
|
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",
|