@eventra_dev/eventra-cli 2.1.0 → 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/README.md +16 -6
- package/dist/analysis/symbols/wrapperRegistry.js +14 -5
- package/dist/cli/check.js +25 -9
- package/dist/cli/init.js +45 -16
- 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 +45 -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/consoleDynamicEventReporter.js +18 -2
- package/dist/plugin/loadPlugins.js +20 -2
- package/dist/plugin/registry.js +50 -1
- package/package.json +3 -2
|
@@ -0,0 +1,77 @@
|
|
|
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.cleanSkipReason = cleanSkipReason;
|
|
7
|
+
exports.errorMessage = errorMessage;
|
|
8
|
+
exports.skippedFrom = skippedFrom;
|
|
9
|
+
exports.formatSkipped = formatSkipped;
|
|
10
|
+
exports.reportSkipped = reportSkipped;
|
|
11
|
+
exports.formatIgnored = formatIgnored;
|
|
12
|
+
exports.reportIgnored = reportIgnored;
|
|
13
|
+
exports.plural = plural;
|
|
14
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
/**
|
|
17
|
+
* A skipped file's error is wrapped once by the CLI, once by the plugin
|
|
18
|
+
* adapter and once by the plugin itself, each repeating the path. Keep only the
|
|
19
|
+
* innermost message.
|
|
20
|
+
*/
|
|
21
|
+
function cleanSkipReason(file, message) {
|
|
22
|
+
let text = message.split(file).join("").trim();
|
|
23
|
+
text = text.replace(/^Plugin "[^"]*" failed to transform\s*:\s*/, "");
|
|
24
|
+
text = text.replace(/^Failed to parse [A-Za-z][A-Za-z ]*?\s*:\s*/, "");
|
|
25
|
+
return text.replace(/\s+/g, " ").trim() || "unknown error";
|
|
26
|
+
}
|
|
27
|
+
function errorMessage(err) {
|
|
28
|
+
return err instanceof Error ? err.message : String(err);
|
|
29
|
+
}
|
|
30
|
+
function skippedFrom(file, err) {
|
|
31
|
+
return { file, reason: cleanSkipReason(file, errorMessage(err)) };
|
|
32
|
+
}
|
|
33
|
+
function formatSkipped(skipped, cwd) {
|
|
34
|
+
if (skipped.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
const lines = [
|
|
37
|
+
`Skipped ${skipped.length} file${skipped.length === 1 ? "" : "s"} that could not be analysed:`,
|
|
38
|
+
];
|
|
39
|
+
for (const { file, reason } of skipped) {
|
|
40
|
+
lines.push(` ${path_1.default.relative(cwd, file).replace(/\\/g, "/")}: ${reason}`);
|
|
41
|
+
}
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
44
|
+
/** Prints one summary block (stderr) for everything skipped in a scan. */
|
|
45
|
+
function reportSkipped(skipped, cwd = process.cwd()) {
|
|
46
|
+
const lines = formatSkipped(skipped, cwd);
|
|
47
|
+
if (lines.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
console.error(chalk_1.default.yellow(lines.join("\n")));
|
|
50
|
+
console.error(chalk_1.default.gray("Events from these files are not in this scan; previously known events were kept. Use --strict to fail on skipped files."));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Files that were deliberately not analysed (for example generated bundles
|
|
54
|
+
* over the size limit). Unlike skipped files they are not failures: their
|
|
55
|
+
* absence neither keeps known events nor makes a catalog snapshot incomplete.
|
|
56
|
+
*/
|
|
57
|
+
function formatIgnored(ignored, cwd) {
|
|
58
|
+
if (ignored.length === 0)
|
|
59
|
+
return [];
|
|
60
|
+
const lines = [
|
|
61
|
+
`Ignored ${ignored.length} file${ignored.length === 1 ? "" : "s"} (not analysed):`,
|
|
62
|
+
];
|
|
63
|
+
for (const { file, reason } of ignored) {
|
|
64
|
+
lines.push(` ${path_1.default.relative(cwd, file).replace(/\\/g, "/")}: ${reason}`);
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
/** Prints one note (stderr) for everything ignored in a scan. */
|
|
69
|
+
function reportIgnored(ignored, cwd = process.cwd()) {
|
|
70
|
+
const lines = formatIgnored(ignored, cwd);
|
|
71
|
+
if (lines.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
console.error(chalk_1.default.gray(lines.join("\n")));
|
|
74
|
+
}
|
|
75
|
+
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
76
|
+
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
77
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -46,22 +46,25 @@ program
|
|
|
46
46
|
.version(package_json_1.default.version);
|
|
47
47
|
program
|
|
48
48
|
.command("init")
|
|
49
|
-
.
|
|
49
|
+
.option("-y, --yes", "do not prompt: accept defaults and keep an existing eventra.json")
|
|
50
|
+
.action(async (opts) => {
|
|
50
51
|
const { init } = await Promise.resolve().then(() => __importStar(require("./cli/init")));
|
|
51
|
-
await init();
|
|
52
|
+
await init({ yes: opts.yes });
|
|
52
53
|
});
|
|
53
54
|
program
|
|
54
55
|
.command("sync")
|
|
55
|
-
.
|
|
56
|
+
.option("--strict", "exit with a non-zero code if any file could not be analysed")
|
|
57
|
+
.action(async (opts) => {
|
|
56
58
|
const { sync } = await Promise.resolve().then(() => __importStar(require("./cli/sync")));
|
|
57
|
-
await sync();
|
|
59
|
+
await sync({ strict: opts.strict });
|
|
58
60
|
});
|
|
59
61
|
program
|
|
60
62
|
.command("check")
|
|
61
63
|
.option("--fix", "auto fix issues")
|
|
64
|
+
.option("--strict", "exit with a non-zero code if any file could not be analysed")
|
|
62
65
|
.action(async (opts) => {
|
|
63
66
|
const { check } = await Promise.resolve().then(() => __importStar(require("./cli/check")));
|
|
64
|
-
await check({ fix: opts.fix });
|
|
67
|
+
await check({ fix: opts.fix, strict: opts.strict });
|
|
65
68
|
});
|
|
66
69
|
program
|
|
67
70
|
.command("watch")
|
|
@@ -5,16 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.registerConsoleDynamicEventReporterPlugin = registerConsoleDynamicEventReporterPlugin;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
// paths are printed relative to where the command runs, with forward slashes
|
|
10
|
+
function displayPath(fileName) {
|
|
11
|
+
const relative = path_1.default.relative(process.cwd(), fileName);
|
|
12
|
+
const outside = relative === ".." || relative.startsWith(`..${path_1.default.sep}`) || relative.startsWith("../");
|
|
13
|
+
const shown = relative && !outside && !path_1.default.isAbsolute(relative) ? relative : fileName;
|
|
14
|
+
return shown.replace(/\\/g, "/");
|
|
15
|
+
}
|
|
8
16
|
const consoleDynamicEventReporter = {
|
|
9
17
|
name: "console",
|
|
10
|
-
report({ occurrences }) {
|
|
18
|
+
report({ occurrences: all }) {
|
|
19
|
+
// the copy of a component class inside an Angular template module repeats a call the class file already reports
|
|
20
|
+
const occurrences = all.filter((occ) => !occ.inGeneratedCode);
|
|
11
21
|
if (occurrences.length === 0) {
|
|
12
22
|
return;
|
|
13
23
|
}
|
|
14
24
|
console.log(chalk_1.default.blue("\nDynamic event names:"));
|
|
15
25
|
for (const occ of occurrences) {
|
|
16
26
|
const values = occ.resolvedValues.length > 0 ? ` (resolved: ${occ.resolvedValues.join(", ")})` : "";
|
|
17
|
-
|
|
27
|
+
const file = displayPath(occ.fileName);
|
|
28
|
+
if (occ.inTemplate) {
|
|
29
|
+
console.log(chalk_1.default.yellow(`~ template of ${file}: event="${occ.eventExpression ?? ""}"${values}`));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(chalk_1.default.yellow(`~ ${file}:${occ.line} ${occ.calleeText}(...)${values}`));
|
|
33
|
+
}
|
|
18
34
|
}
|
|
19
35
|
},
|
|
20
36
|
};
|
|
@@ -32,10 +32,15 @@ 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");
|
|
40
45
|
const configuredSinks_1 = require("./builtins/configuredSinks");
|
|
41
46
|
const eventra_sdk_1 = require("./builtins/eventra-sdk");
|
|
@@ -69,6 +74,19 @@ function pluginLoadHint(spec) {
|
|
|
69
74
|
}
|
|
70
75
|
return "";
|
|
71
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
|
+
}
|
|
72
90
|
async function loadExternalPlugin(registry, spec) {
|
|
73
91
|
const trimmed = spec.trim();
|
|
74
92
|
// Defensive-only: the sole caller (createPluginRegistry) already does
|
|
@@ -80,7 +98,7 @@ async function loadExternalPlugin(registry, spec) {
|
|
|
80
98
|
return;
|
|
81
99
|
let mod;
|
|
82
100
|
try {
|
|
83
|
-
mod = await Promise.resolve(`${trimmed}`).then(s => __importStar(require(s)));
|
|
101
|
+
mod = await Promise.resolve(`${resolveFromProject(trimmed)}`).then(s => __importStar(require(s)));
|
|
84
102
|
}
|
|
85
103
|
catch (err) {
|
|
86
104
|
// Node's dynamic import() only ever rejects with an Error (module
|
|
@@ -114,7 +132,7 @@ async function createPluginRegistry(config) {
|
|
|
114
132
|
if (!trimmed)
|
|
115
133
|
continue;
|
|
116
134
|
if (!isTrustedPluginSpecifier(trimmed)) {
|
|
117
|
-
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)}`);
|
|
118
136
|
continue;
|
|
119
137
|
}
|
|
120
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventra_dev/eventra-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"test:e2e": "pnpm run clean && pnpm run build && tsx tests/run.ts",
|
|
38
38
|
"clean": "rm -rf dist",
|
|
39
39
|
"test:mutation": "stryker run",
|
|
40
|
-
"check:presets": "tsx scripts/check-preset-packages.ts"
|
|
40
|
+
"check:presets": "tsx scripts/check-preset-packages.ts",
|
|
41
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"chalk": "^4.1.2",
|