@eventra_dev/eventra-cli 1.0.4 → 1.0.6
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/README.md +5 -4
- package/dist/analysis/extractor/extractor.js +7 -1
- package/dist/analysis/extractor/propagationExtractor.js +7 -3
- package/dist/analysis/resolver/exportResolver.js +43 -0
- package/dist/analysis/scanner/scanner.js +2 -1
- package/dist/analysis/shared/dynamicOccurrence.js +22 -0
- package/dist/cli/check.js +2 -1
- package/dist/cli/sync.js +2 -1
- package/dist/core/EventraEngine.js +8 -0
- package/dist/core/projectScanner.js +1 -0
- package/dist/plugin/adapters/external.js +0 -4
- package/dist/plugin/builtins/consoleDynamicEventReporter.js +24 -0
- package/dist/plugin/loadPlugins.js +2 -0
- package/dist/plugin/registry.js +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,11 +206,12 @@ pnpm add -D @eventra_dev/cli-plugin-vue
|
|
|
206
206
|
|
|
207
207
|
The plugin:
|
|
208
208
|
|
|
209
|
-
-
|
|
210
|
-
- extracts `track()` calls from
|
|
211
|
-
- detects
|
|
209
|
+
- parses each `.vue` file with the real Vue compiler (`@vue/compiler-sfc`) into a single virtual TypeScript module combining `<script>`/`<script setup>`
|
|
210
|
+
- extracts `track()` calls from the script exactly like a regular `.ts` file (direct calls, wrappers, propagation)
|
|
211
|
+
- detects `event="feature_name"` attributes in `<template>` — both literal and dynamic (`:event="expr"`); dynamic bindings resolve through the same module scope as the script, or surface as a dynamic occurrence if unresolvable
|
|
212
|
+
- Nuxt `.vue` files (pages/layouts/components) are handled the same way; auto-imported composables resolve once the project has run `nuxt prepare`/`nuxt dev`
|
|
212
213
|
|
|
213
|
-
`sync.include` does **not** need `**/*.vue` manually — the plugin registers `**/*.vue` via `includeGlobs`.
|
|
214
|
+
`sync.include` does **not** need `**/*.vue` manually — the plugin registers `**/*.vue` via `includeGlobs`. See [`@eventra_dev/cli-plugin-vue`](https://www.npmjs.com/package/@eventra_dev/cli-plugin-vue) for details.
|
|
214
215
|
|
|
215
216
|
### Before publishing / local development
|
|
216
217
|
|
|
@@ -9,8 +9,10 @@ const resolver_1 = require("../resolver/resolver");
|
|
|
9
9
|
const evaluationContext_1 = require("../shared/evaluationContext");
|
|
10
10
|
const propagationExtractor_1 = require("./propagationExtractor");
|
|
11
11
|
const eventValidation_1 = require("../shared/eventValidation");
|
|
12
|
+
const dynamicOccurrence_1 = require("../shared/dynamicOccurrence");
|
|
12
13
|
function extractEvents(index, checker, _config, evaluationCache, exportCache, resolvedCallCache, returnPropagationCache, wrapperRegistry) {
|
|
13
14
|
const events = new Set();
|
|
15
|
+
const dynamicOccurrences = [];
|
|
14
16
|
// direct track sinks
|
|
15
17
|
for (const call of index.trackCalls) {
|
|
16
18
|
for (const argument of call.trackedArguments) {
|
|
@@ -21,12 +23,15 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
|
|
|
21
23
|
events.add(normalized);
|
|
22
24
|
}
|
|
23
25
|
}
|
|
26
|
+
if (resolved.dynamic) {
|
|
27
|
+
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call.node, resolved.values, dynamicOccurrences);
|
|
28
|
+
}
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
const propagationVisited = new Set();
|
|
27
32
|
function visit(node) {
|
|
28
33
|
if (typescript_1.default.isCallExpression(node)) {
|
|
29
|
-
(0, propagationExtractor_1.extractPropagationEvents)(node, checker, wrapperRegistry, resolvedCallCache, returnPropagationCache, evaluationCache, exportCache, events, propagationVisited);
|
|
34
|
+
(0, propagationExtractor_1.extractPropagationEvents)(node, checker, wrapperRegistry, resolvedCallCache, returnPropagationCache, evaluationCache, exportCache, events, propagationVisited, dynamicOccurrences);
|
|
30
35
|
}
|
|
31
36
|
typescript_1.default.forEachChild(node, visit);
|
|
32
37
|
}
|
|
@@ -34,5 +39,6 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
|
|
|
34
39
|
return {
|
|
35
40
|
events,
|
|
36
41
|
detectedFunctionWrappers: new Set(index.wrappers.map(wrapper => wrapper.symbol.getName())),
|
|
42
|
+
dynamicOccurrences,
|
|
37
43
|
};
|
|
38
44
|
}
|
|
@@ -9,6 +9,7 @@ const callResolver_1 = require("../resolver/callResolver");
|
|
|
9
9
|
const resolver_1 = require("../resolver/resolver");
|
|
10
10
|
const evaluationContext_1 = require("../shared/evaluationContext");
|
|
11
11
|
const eventValidation_1 = require("../shared/eventValidation");
|
|
12
|
+
const dynamicOccurrence_1 = require("../shared/dynamicOccurrence");
|
|
12
13
|
function resolveObjectPath(expression, path) {
|
|
13
14
|
let current = expression;
|
|
14
15
|
for (const segment of path) {
|
|
@@ -34,7 +35,7 @@ function resolveObjectPath(expression, path) {
|
|
|
34
35
|
}
|
|
35
36
|
return current;
|
|
36
37
|
}
|
|
37
|
-
function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCache, returnPropagationCache, evaluationCache, exportCache, events, visited) {
|
|
38
|
+
function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCache, returnPropagationCache, evaluationCache, exportCache, events, visited, dynamicOccurrences) {
|
|
38
39
|
const resolved = (0, callResolver_1.resolveFunctionFromCall)(call.expression, checker, resolvedCallCache, exportCache);
|
|
39
40
|
if (!resolved) {
|
|
40
41
|
return;
|
|
@@ -53,14 +54,14 @@ function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCa
|
|
|
53
54
|
visited.add(signature);
|
|
54
55
|
try {
|
|
55
56
|
for (const propagation of semantic.propagations) {
|
|
56
|
-
extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events);
|
|
57
|
+
extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events, dynamicOccurrences);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
finally {
|
|
60
61
|
visited.delete(signature);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
function extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events) {
|
|
64
|
+
function extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events, dynamicOccurrences) {
|
|
64
65
|
let argument = call.arguments[propagation.sourceParameterIndex];
|
|
65
66
|
if (!argument) {
|
|
66
67
|
return;
|
|
@@ -89,4 +90,7 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
|
|
|
89
90
|
events.add(normalized);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
93
|
+
if (resolved.dynamic) {
|
|
94
|
+
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call, resolved.values, dynamicOccurrences);
|
|
95
|
+
}
|
|
92
96
|
}
|
|
@@ -15,6 +15,42 @@ function isConcreteDeclaration(node) {
|
|
|
15
15
|
|| typescript_1.default.isMethodDeclaration(node)
|
|
16
16
|
|| typescript_1.default.isPropertyDeclaration(node));
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* `declare const x: typeof import("./module")["name"]` (or the dotted form,
|
|
20
|
+
* `typeof import("./module").name`) is how Nuxt/unplugin-auto-import
|
|
21
|
+
* generate global ambient bindings for auto-imported exports. A `const`
|
|
22
|
+
* with no initializer is only legal in an ambient context, so this shape is
|
|
23
|
+
* unambiguous. Without unwrapping it, this ambient variable is its own
|
|
24
|
+
* distinct symbol — the wrapper/export registered under the *real*
|
|
25
|
+
* declaration's symbol would never match a call site that resolves to the
|
|
26
|
+
* ambient one instead.
|
|
27
|
+
*/
|
|
28
|
+
function resolveAmbientAutoImportSymbol(symbol, checker) {
|
|
29
|
+
const declarations = symbol.getDeclarations();
|
|
30
|
+
if (!declarations || declarations.length !== 1) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const [declaration] = declarations;
|
|
34
|
+
if (!typescript_1.default.isVariableDeclaration(declaration) || declaration.initializer || !declaration.type) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
let nameNode;
|
|
38
|
+
const type = declaration.type;
|
|
39
|
+
if (typescript_1.default.isImportTypeNode(type) && type.qualifier && typescript_1.default.isIdentifier(type.qualifier)) {
|
|
40
|
+
nameNode = type.qualifier;
|
|
41
|
+
}
|
|
42
|
+
else if (typescript_1.default.isIndexedAccessTypeNode(type)
|
|
43
|
+
&& typescript_1.default.isImportTypeNode(type.objectType)
|
|
44
|
+
&& typescript_1.default.isLiteralTypeNode(type.indexType)
|
|
45
|
+
&& typescript_1.default.isStringLiteral(type.indexType.literal)) {
|
|
46
|
+
nameNode = type.indexType.literal;
|
|
47
|
+
}
|
|
48
|
+
if (!nameNode) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const resolved = checker.getSymbolAtLocation(nameNode);
|
|
52
|
+
return resolved && resolved !== symbol ? resolved : null;
|
|
53
|
+
}
|
|
18
54
|
function resolveExportedSymbol(symbol, checker, cache, visited) {
|
|
19
55
|
if (typeof symbol.getDeclarations !== "function") {
|
|
20
56
|
return null;
|
|
@@ -44,6 +80,13 @@ function resolveExportedSymbol(symbol, checker, cache, visited) {
|
|
|
44
80
|
cache.set(symbol, resolved);
|
|
45
81
|
return resolved;
|
|
46
82
|
}
|
|
83
|
+
// ambient auto-import unwrap (Nuxt/unplugin-auto-import style globals)
|
|
84
|
+
const ambientTarget = resolveAmbientAutoImportSymbol(symbol, checker);
|
|
85
|
+
if (ambientTarget) {
|
|
86
|
+
const resolved = resolveExportedSymbol(ambientTarget, checker, cache, seen);
|
|
87
|
+
cache.set(symbol, resolved);
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
47
90
|
// declaration existence
|
|
48
91
|
const declarations = symbol.getDeclarations() ?? [];
|
|
49
92
|
for (const declaration of declarations) {
|
|
@@ -72,7 +72,8 @@ function scanSource(source, checker, wrapperRegistry, plugins) {
|
|
|
72
72
|
}
|
|
73
73
|
return false;
|
|
74
74
|
});
|
|
75
|
-
const wrapper = (
|
|
75
|
+
const wrapper = plugins.detectWrapper({ fn, checker, sinks: localSinks }) ??
|
|
76
|
+
(0, propagationAnalyzer_1.analyzeWrapperPropagation)(fn, checker, localSinks);
|
|
76
77
|
if (!wrapper) {
|
|
77
78
|
continue;
|
|
78
79
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/analysis/shared/dynamicOccurrence.ts
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.recordDynamicOccurrence = recordDynamicOccurrence;
|
|
8
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
9
|
+
const CALL_TEXT_MAX_LENGTH = 120;
|
|
10
|
+
function recordDynamicOccurrence(call, resolvedValues, out) {
|
|
11
|
+
const sourceFile = call.getSourceFile();
|
|
12
|
+
const { line, character } = typescript_1.default.getLineAndCharacterOfPosition(sourceFile, call.getStart(sourceFile));
|
|
13
|
+
const callText = call.getText(sourceFile);
|
|
14
|
+
out.push({
|
|
15
|
+
fileName: sourceFile.fileName,
|
|
16
|
+
line: line + 1,
|
|
17
|
+
character,
|
|
18
|
+
calleeText: call.expression.getText(sourceFile),
|
|
19
|
+
callText: callText.length > CALL_TEXT_MAX_LENGTH ? `${callText.slice(0, CALL_TEXT_MAX_LENGTH)}…` : callText,
|
|
20
|
+
resolvedValues,
|
|
21
|
+
});
|
|
22
|
+
}
|
package/dist/cli/check.js
CHANGED
|
@@ -21,7 +21,8 @@ async function check({ fix = false }) {
|
|
|
21
21
|
process.exit(1);
|
|
22
22
|
}
|
|
23
23
|
console.log(chalk_1.default.blue("Checking events and function wrappers..."));
|
|
24
|
-
const { engine } = await (0, projectScanner_1.scanProject)(config);
|
|
24
|
+
const { engine, plugins } = await (0, projectScanner_1.scanProject)(config);
|
|
25
|
+
await plugins.runDynamicEventReporters(engine.getAllDynamicOccurrences());
|
|
25
26
|
const foundEvents = new Set(engine.getAllEvents());
|
|
26
27
|
const knownEvents = new Set(config.events ?? []);
|
|
27
28
|
const eventDiff = diffSets(foundEvents, knownEvents);
|
package/dist/cli/sync.js
CHANGED
|
@@ -15,9 +15,10 @@ async function sync() {
|
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
console.log(chalk_1.default.blue("Scanning..."));
|
|
18
|
-
const { engine } = await (0, projectScanner_1.scanProject)(config);
|
|
18
|
+
const { engine, plugins } = await (0, projectScanner_1.scanProject)(config);
|
|
19
19
|
await (0, scanResults_1.persistScanResults)(config, engine);
|
|
20
20
|
const events = engine.getAllEvents();
|
|
21
21
|
const wrappers = engine.getAllFunctionWrappers();
|
|
22
22
|
console.log(chalk_1.default.green(`Found ${events.length} events, ${wrappers.length} function wrappers`));
|
|
23
|
+
await plugins.runDynamicEventReporters(engine.getAllDynamicOccurrences());
|
|
23
24
|
}
|
|
@@ -20,6 +20,7 @@ const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
|
20
20
|
const EMPTY_RESULT = () => ({
|
|
21
21
|
events: new Set(),
|
|
22
22
|
detectedFunctionWrappers: new Set(),
|
|
23
|
+
dynamicOccurrences: [],
|
|
23
24
|
});
|
|
24
25
|
class EventraEngine {
|
|
25
26
|
compiler;
|
|
@@ -253,5 +254,12 @@ class EventraEngine {
|
|
|
253
254
|
getScanResult(fileName) {
|
|
254
255
|
return this.fileResults.get(this.normalize(fileName)) ?? EMPTY_RESULT();
|
|
255
256
|
}
|
|
257
|
+
getAllDynamicOccurrences() {
|
|
258
|
+
const occurrences = [];
|
|
259
|
+
for (const result of this.fileResults.values()) {
|
|
260
|
+
occurrences.push(...result.dynamicOccurrences);
|
|
261
|
+
}
|
|
262
|
+
return occurrences.sort((a, b) => a.fileName.localeCompare(b.fileName) || a.line - b.line);
|
|
263
|
+
}
|
|
256
264
|
}
|
|
257
265
|
exports.EventraEngine = EventraEngine;
|
|
@@ -48,10 +48,6 @@ function createStaticCalleeSinkDetector(sink) {
|
|
|
48
48
|
if (call.arguments.length <= argIndex) {
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
|
-
const arg = call.arguments[argIndex];
|
|
52
|
-
if (!typescript_1.default.isStringLiteral(arg)) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
51
|
return {
|
|
56
52
|
call,
|
|
57
53
|
trackedArguments: [{ index: argIndex, propertyPath: [] }],
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerConsoleDynamicEventReporterPlugin = registerConsoleDynamicEventReporterPlugin;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const consoleDynamicEventReporter = {
|
|
9
|
+
name: "console",
|
|
10
|
+
report({ occurrences }) {
|
|
11
|
+
if (occurrences.length === 0) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(chalk_1.default.blue("\nDynamic event names:"));
|
|
15
|
+
for (const occ of occurrences) {
|
|
16
|
+
const values = occ.resolvedValues.length > 0 ? ` (resolved: ${occ.resolvedValues.join(", ")})` : "";
|
|
17
|
+
console.log(chalk_1.default.yellow(`~ ${occ.fileName}:${occ.line} ${occ.calleeText}(...)${values}`));
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
/** Default reporter: prints unresolved/dynamic event-name call sites to the console (always enabled). */
|
|
22
|
+
function registerConsoleDynamicEventReporterPlugin(registry) {
|
|
23
|
+
registry.registerDynamicEventReporter(consoleDynamicEventReporter);
|
|
24
|
+
}
|
|
@@ -38,6 +38,7 @@ exports.createBuiltinPluginRegistry = createBuiltinPluginRegistry;
|
|
|
38
38
|
exports.createPluginRegistry = createPluginRegistry;
|
|
39
39
|
const external_1 = require("./adapters/external");
|
|
40
40
|
const eventra_sdk_1 = require("./builtins/eventra-sdk");
|
|
41
|
+
const consoleDynamicEventReporter_1 = require("./builtins/consoleDynamicEventReporter");
|
|
41
42
|
const registry_1 = require("./registry");
|
|
42
43
|
/**
|
|
43
44
|
* Only packages published under the `@eventra_dev` npm scope can ever be
|
|
@@ -89,6 +90,7 @@ async function loadExternalPlugin(registry, spec) {
|
|
|
89
90
|
function createBuiltinPluginRegistry() {
|
|
90
91
|
const registry = new registry_1.PluginRegistry();
|
|
91
92
|
(0, eventra_sdk_1.registerEventraSdkPlugin)(registry);
|
|
93
|
+
(0, consoleDynamicEventReporter_1.registerConsoleDynamicEventReporterPlugin)(registry);
|
|
92
94
|
return registry;
|
|
93
95
|
}
|
|
94
96
|
/** Built-ins + plugins listed in eventra.json `plugins` array. */
|
package/dist/plugin/registry.js
CHANGED
|
@@ -8,6 +8,8 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
class PluginRegistry {
|
|
9
9
|
preprocessors = [];
|
|
10
10
|
sinkDetectors = [];
|
|
11
|
+
wrapperDetectors = [];
|
|
12
|
+
dynamicEventReporters = [];
|
|
11
13
|
includePatterns = [];
|
|
12
14
|
sourceToVirtuals = new Map();
|
|
13
15
|
registerFilePreprocessor(preprocessor) {
|
|
@@ -22,6 +24,18 @@ class PluginRegistry {
|
|
|
22
24
|
}
|
|
23
25
|
this.sinkDetectors.push(detector);
|
|
24
26
|
}
|
|
27
|
+
registerWrapperDetector(detector) {
|
|
28
|
+
if (this.wrapperDetectors.some((d) => d.name === detector.name)) {
|
|
29
|
+
throw new Error(`Wrapper detector already registered: ${detector.name}`);
|
|
30
|
+
}
|
|
31
|
+
this.wrapperDetectors.push(detector);
|
|
32
|
+
}
|
|
33
|
+
registerDynamicEventReporter(reporter) {
|
|
34
|
+
if (this.dynamicEventReporters.some((r) => r.name === reporter.name)) {
|
|
35
|
+
throw new Error(`Dynamic event reporter already registered: ${reporter.name}`);
|
|
36
|
+
}
|
|
37
|
+
this.dynamicEventReporters.push(reporter);
|
|
38
|
+
}
|
|
25
39
|
registerIncludePattern(pattern) {
|
|
26
40
|
const trimmed = pattern.trim();
|
|
27
41
|
if (!trimmed)
|
|
@@ -36,6 +50,12 @@ class PluginRegistry {
|
|
|
36
50
|
getSinkDetectors() {
|
|
37
51
|
return this.sinkDetectors;
|
|
38
52
|
}
|
|
53
|
+
getWrapperDetectors() {
|
|
54
|
+
return this.wrapperDetectors;
|
|
55
|
+
}
|
|
56
|
+
getDynamicEventReporters() {
|
|
57
|
+
return this.dynamicEventReporters;
|
|
58
|
+
}
|
|
39
59
|
getFilePreprocessors() {
|
|
40
60
|
return this.preprocessors;
|
|
41
61
|
}
|
|
@@ -71,6 +91,20 @@ class PluginRegistry {
|
|
|
71
91
|
}
|
|
72
92
|
return null;
|
|
73
93
|
}
|
|
94
|
+
detectWrapper(context) {
|
|
95
|
+
for (const detector of this.wrapperDetectors) {
|
|
96
|
+
const wrapper = detector.detect(context);
|
|
97
|
+
if (wrapper) {
|
|
98
|
+
return wrapper;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
async runDynamicEventReporters(occurrences) {
|
|
104
|
+
for (const reporter of this.dynamicEventReporters) {
|
|
105
|
+
await reporter.report({ occurrences });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
74
108
|
normalizePath(fileName) {
|
|
75
109
|
return path_1.default.resolve(fileName).replace(/\\/g, "/");
|
|
76
110
|
}
|