@eventra_dev/eventra-cli 2.1.0 → 2.3.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 +49 -11
- package/dist/analysis/resolver/callResolver.js +16 -2
- package/dist/analysis/scanner/scanner.js +1 -0
- package/dist/analysis/scanner/wrapperChains.js +115 -0
- package/dist/analysis/sdk/eventraSdk.js +6 -4
- package/dist/analysis/symbols/wrapperRegistry.js +14 -5
- package/dist/cli/check.js +25 -9
- package/dist/cli/init.js +87 -23
- package/dist/cli/projectDependencies.js +8 -3
- 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 +246 -6
- package/dist/compiler/documentRegistry.js +15 -1
- package/dist/compiler/importGraph.js +8 -0
- package/dist/compiler/workspacePackages.js +58 -0
- package/dist/config/config.js +61 -3
- package/dist/core/EventraEngine.js +143 -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 +49 -38
- package/dist/plugin/builtins/configuredSinks.js +84 -17
- package/dist/plugin/builtins/consoleDynamicEventReporter.js +21 -2
- package/dist/plugin/loadPlugins.js +20 -2
- package/dist/plugin/registry.js +83 -1
- package/dist/respawn.js +89 -0
- 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
|
@@ -39,42 +39,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
const commander_1 = require("commander");
|
|
41
41
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
program
|
|
48
|
-
.command("init")
|
|
49
|
-
.action(async () => {
|
|
50
|
-
const { init } = await Promise.resolve().then(() => __importStar(require("./cli/init")));
|
|
51
|
-
await init();
|
|
42
|
+
const respawn_1 = require("./respawn");
|
|
43
|
+
// a large repository can need more memory than Node's default: start again with enough
|
|
44
|
+
(0, respawn_1.respawnWithMoreHeap)().then((respawned) => {
|
|
45
|
+
if (!respawned)
|
|
46
|
+
run();
|
|
52
47
|
});
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
program
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
66
|
-
program
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
program
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
80
|
-
|
|
48
|
+
function run() {
|
|
49
|
+
const program = new commander_1.Command();
|
|
50
|
+
program
|
|
51
|
+
.name("eventra")
|
|
52
|
+
.description("Eventra CLI")
|
|
53
|
+
.version(package_json_1.default.version);
|
|
54
|
+
program
|
|
55
|
+
.command("init")
|
|
56
|
+
.option("-y, --yes", "do not prompt: accept defaults and keep an existing eventra.json")
|
|
57
|
+
.action(async (opts) => {
|
|
58
|
+
const { init } = await Promise.resolve().then(() => __importStar(require("./cli/init")));
|
|
59
|
+
await init({ yes: opts.yes });
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.command("sync")
|
|
63
|
+
.option("--strict", "exit with a non-zero code if any file could not be analysed")
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
const { sync } = await Promise.resolve().then(() => __importStar(require("./cli/sync")));
|
|
66
|
+
await sync({ strict: opts.strict });
|
|
67
|
+
});
|
|
68
|
+
program
|
|
69
|
+
.command("check")
|
|
70
|
+
.option("--fix", "auto fix issues")
|
|
71
|
+
.option("--strict", "exit with a non-zero code if any file could not be analysed")
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
const { check } = await Promise.resolve().then(() => __importStar(require("./cli/check")));
|
|
74
|
+
await check({ fix: opts.fix, strict: opts.strict });
|
|
75
|
+
});
|
|
76
|
+
program
|
|
77
|
+
.command("watch")
|
|
78
|
+
.description("Watch files and detect events in real-time")
|
|
79
|
+
.action(async () => {
|
|
80
|
+
const { watch } = await Promise.resolve().then(() => __importStar(require("./cli/watch")));
|
|
81
|
+
await watch();
|
|
82
|
+
});
|
|
83
|
+
program
|
|
84
|
+
.command("send")
|
|
85
|
+
.option("--trust-endpoint", "approve the current eventra.json endpoint for local use")
|
|
86
|
+
.action(async (opts) => {
|
|
87
|
+
const { send } = await Promise.resolve().then(() => __importStar(require("./cli/send")));
|
|
88
|
+
await send({ trustEndpoint: opts.trustEndpoint });
|
|
89
|
+
});
|
|
90
|
+
program.parse();
|
|
91
|
+
}
|
|
@@ -82,20 +82,6 @@ function moduleMatches(specifier, modules) {
|
|
|
82
82
|
return modules.some((m) => specifier === m || specifier.startsWith(`${m}/`));
|
|
83
83
|
}
|
|
84
84
|
const MAX_ALIAS_HOPS = 6;
|
|
85
|
-
/** Identifiers naming the types in a type annotation: `PostHog`, `PostHog | null`, `ph.PostHog`. */
|
|
86
|
-
function typeReferenceRoots(type) {
|
|
87
|
-
if (typescript_1.default.isParenthesizedTypeNode(type))
|
|
88
|
-
return typeReferenceRoots(type.type);
|
|
89
|
-
if (typescript_1.default.isUnionTypeNode(type) || typescript_1.default.isIntersectionTypeNode(type))
|
|
90
|
-
return type.types.flatMap(typeReferenceRoots);
|
|
91
|
-
if (typescript_1.default.isTypeReferenceNode(type)) {
|
|
92
|
-
let name = type.typeName;
|
|
93
|
-
while (typescript_1.default.isQualifiedName(name))
|
|
94
|
-
name = name.left;
|
|
95
|
-
return [name];
|
|
96
|
-
}
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
85
|
/**
|
|
100
86
|
* The symbol an identifier refers to, keeping an import as the import binding itself. TS resolves an
|
|
101
87
|
* import all the way to its target in some positions once something makes the module resolvable
|
|
@@ -109,6 +95,55 @@ function symbolAt(id, checker) {
|
|
|
109
95
|
return symbol;
|
|
110
96
|
return checker.getSymbolsInScope(id, typescript_1.default.SymbolFlags.Alias).find((alias) => alias.name === id.text) ?? symbol;
|
|
111
97
|
}
|
|
98
|
+
/** `import("m")`: the specifier of the call decides, no installed package needed. */
|
|
99
|
+
function dynamicImportFrom(expression, modules) {
|
|
100
|
+
const specifier = typescript_1.default.isCallExpression(expression) && expression.expression.kind === typescript_1.default.SyntaxKind.ImportKeyword
|
|
101
|
+
? expression.arguments[0]
|
|
102
|
+
: undefined;
|
|
103
|
+
return specifier !== undefined && typescript_1.default.isStringLiteralLike(specifier) && moduleMatches(specifier.text, modules);
|
|
104
|
+
}
|
|
105
|
+
const PROMISE_TYPES = new Set(["Promise", "PromiseLike"]);
|
|
106
|
+
/**
|
|
107
|
+
* True when a type annotation names something from `modules`: `PostHog`, `PostHog | null`, `ph.PostHog`,
|
|
108
|
+
* `typeof import("posthog-js")`, `Promise<typeof import("posthog-js")>`, `typeof posthog`.
|
|
109
|
+
*/
|
|
110
|
+
function typeFromModules(type, checker, modules, hops) {
|
|
111
|
+
if (typescript_1.default.isParenthesizedTypeNode(type))
|
|
112
|
+
return typeFromModules(type.type, checker, modules, hops);
|
|
113
|
+
if (typescript_1.default.isUnionTypeNode(type) || typescript_1.default.isIntersectionTypeNode(type)) {
|
|
114
|
+
return type.types.some((member) => typeFromModules(member, checker, modules, hops));
|
|
115
|
+
}
|
|
116
|
+
if (typescript_1.default.isImportTypeNode(type)) {
|
|
117
|
+
return typescript_1.default.isLiteralTypeNode(type.argument) && typescript_1.default.isStringLiteral(type.argument.literal) && moduleMatches(type.argument.literal.text, modules);
|
|
118
|
+
}
|
|
119
|
+
const name = typescript_1.default.isTypeReferenceNode(type) ? type.typeName : typescript_1.default.isTypeQueryNode(type) ? type.exprName : undefined;
|
|
120
|
+
if (name === undefined)
|
|
121
|
+
return false;
|
|
122
|
+
let root = name;
|
|
123
|
+
while (typescript_1.default.isQualifiedName(root))
|
|
124
|
+
root = root.left;
|
|
125
|
+
return (symbolFromModules(symbolAt(root, checker), checker, modules, hops + 1) ||
|
|
126
|
+
// a promise of the module (`Promise<typeof import("posthog-js")>`), not any generic that mentions it
|
|
127
|
+
(typescript_1.default.isTypeReferenceNode(type) &&
|
|
128
|
+
typescript_1.default.isIdentifier(type.typeName) &&
|
|
129
|
+
PROMISE_TYPES.has(type.typeName.text) &&
|
|
130
|
+
(type.typeArguments ?? []).some((argument) => typeFromModules(argument, checker, modules, hops))));
|
|
131
|
+
}
|
|
132
|
+
/** `return expr` statements of a function body, not looking into functions nested in it. */
|
|
133
|
+
function returnedExpressions(body) {
|
|
134
|
+
if (!typescript_1.default.isBlock(body))
|
|
135
|
+
return [body];
|
|
136
|
+
const found = [];
|
|
137
|
+
const visit = (node) => {
|
|
138
|
+
if (typescript_1.default.isFunctionLike(node))
|
|
139
|
+
return;
|
|
140
|
+
if (typescript_1.default.isReturnStatement(node) && node.expression)
|
|
141
|
+
found.push(node.expression);
|
|
142
|
+
typescript_1.default.forEachChild(node, visit);
|
|
143
|
+
};
|
|
144
|
+
typescript_1.default.forEachChild(body, visit);
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
112
147
|
/** The symbol `name` exported by the module a specifier points at, when TS can resolve that module. */
|
|
113
148
|
function resolveExport(specifier, name, checker) {
|
|
114
149
|
const moduleSymbol = checker.getSymbolAtLocation(specifier);
|
|
@@ -123,6 +158,8 @@ function expressionFromModules(expression, checker, modules, hops) {
|
|
|
123
158
|
let expr = (0, utils_1.unwrapExpression)(expression);
|
|
124
159
|
if (typescript_1.default.isAwaitExpression(expr))
|
|
125
160
|
expr = (0, utils_1.unwrapExpression)(expr.expression);
|
|
161
|
+
if (dynamicImportFrom(expr, modules))
|
|
162
|
+
return true;
|
|
126
163
|
if (typescript_1.default.isCallExpression(expr) || typescript_1.default.isNewExpression(expr))
|
|
127
164
|
expr = (0, utils_1.unwrapExpression)(expr.expression);
|
|
128
165
|
const fromName = (name) => typescript_1.default.isIdentifier(name) && symbolFromModules(symbolAt(name, checker), checker, modules, hops);
|
|
@@ -133,7 +170,10 @@ function expressionFromModules(expression, checker, modules, hops) {
|
|
|
133
170
|
let root = expr;
|
|
134
171
|
while (typescript_1.default.isPropertyAccessExpression(root))
|
|
135
172
|
root = (0, utils_1.unwrapExpression)(root.expression);
|
|
136
|
-
|
|
173
|
+
// `(await import("posthog-js")).default`
|
|
174
|
+
if (typescript_1.default.isAwaitExpression(root))
|
|
175
|
+
root = (0, utils_1.unwrapExpression)(root.expression);
|
|
176
|
+
return fromName(expr.name) || fromName(root) || dynamicImportFrom(root, modules);
|
|
137
177
|
}
|
|
138
178
|
function declarationFromModules(decl, checker, modules, hops) {
|
|
139
179
|
if (typescript_1.default.isImportSpecifier(decl) || typescript_1.default.isImportClause(decl) || typescript_1.default.isNamespaceImport(decl)) {
|
|
@@ -162,15 +202,42 @@ function declarationFromModules(decl, checker, modules, hops) {
|
|
|
162
202
|
if (typescript_1.default.isExportAssignment(decl)) {
|
|
163
203
|
return expressionFromModules(decl.expression, checker, modules, hops + 1);
|
|
164
204
|
}
|
|
205
|
+
// function useAnalytics() { return usePostHog() } / const load = () => import("posthog-js"): a function
|
|
206
|
+
// gives back what it returns (or what its return annotation names)
|
|
207
|
+
const fn = typescript_1.default.isFunctionDeclaration(decl) || typescript_1.default.isMethodDeclaration(decl)
|
|
208
|
+
? decl
|
|
209
|
+
: (typescript_1.default.isVariableDeclaration(decl) || typescript_1.default.isPropertyDeclaration(decl)) &&
|
|
210
|
+
decl.initializer &&
|
|
211
|
+
(typescript_1.default.isArrowFunction(decl.initializer) || typescript_1.default.isFunctionExpression(decl.initializer))
|
|
212
|
+
? decl.initializer
|
|
213
|
+
: undefined;
|
|
214
|
+
if (fn) {
|
|
215
|
+
if (fn.type && typeFromModules(fn.type, checker, modules, hops))
|
|
216
|
+
return true;
|
|
217
|
+
if (fn.body && returnedExpressions(fn.body).some((returned) => expressionFromModules(returned, checker, modules, hops + 1))) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
165
221
|
// const { posthog } = usePostHog(): the binding's own declaration is the enclosing variable.
|
|
166
222
|
const owner = typescript_1.default.isBindingElement(decl) ? decl.parent.parent : decl;
|
|
223
|
+
// promise.then(({ default: posthog }) => ...) / promise.then((mod) => ...): the parameter holds what the promise resolves to
|
|
224
|
+
if (typescript_1.default.isParameter(owner)) {
|
|
225
|
+
const callback = owner.parent;
|
|
226
|
+
const call = callback.parent;
|
|
227
|
+
if ((typescript_1.default.isArrowFunction(callback) || typescript_1.default.isFunctionExpression(callback)) &&
|
|
228
|
+
typescript_1.default.isCallExpression(call) &&
|
|
229
|
+
call.arguments[0] === callback &&
|
|
230
|
+
typescript_1.default.isPropertyAccessExpression(call.expression) &&
|
|
231
|
+
call.expression.name.text === "then") {
|
|
232
|
+
return expressionFromModules(call.expression.expression, checker, modules, hops + 1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
167
235
|
// Variables, properties, parameters, functions (return type) and type aliases all keep their
|
|
168
236
|
// annotation in `type`; variables, properties and parameters keep a default/initial value in
|
|
169
237
|
// `initializer`. Anything else (classes, interfaces, ...) simply has neither.
|
|
170
238
|
const { type, initializer } = owner;
|
|
171
239
|
// let client: PostHog | null / private client: PostHog / (client: PostHog) => ... / type Client = PostHog
|
|
172
|
-
if (type &&
|
|
173
|
-
typeReferenceRoots(type).some((id) => symbolFromModules(symbolAt(id, checker), checker, modules, hops + 1))) {
|
|
240
|
+
if (type && typeFromModules(type, checker, modules, hops)) {
|
|
174
241
|
return true;
|
|
175
242
|
}
|
|
176
243
|
// const ph = usePostHog() / new PostHog(k) / client = new PostHog(k) / const useTrack = usePostHog
|
|
@@ -5,16 +5,35 @@ 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 if (occ.inTemplateCode) {
|
|
32
|
+
console.log(chalk_1.default.yellow(`~ template of ${file}: ${occ.calleeText}(...)${values}`));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log(chalk_1.default.yellow(`~ ${file}:${occ.line} ${occ.calleeText}(...)${values}`));
|
|
36
|
+
}
|
|
18
37
|
}
|
|
19
38
|
},
|
|
20
39
|
};
|
|
@@ -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,30 @@ 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
|
+
// A plugin starts the code it generates from a template with this comment line, so a call found
|
|
11
|
+
// below it is not on a line the user wrote.
|
|
12
|
+
const TEMPLATE_REGION_START = /^\s*\/\/ --- [a-z]+ template \(auto-generated\) ---$/;
|
|
13
|
+
// optional: a module that carries more after the generated code (an Angular component class) ends it
|
|
14
|
+
const TEMPLATE_REGION_END = /^\s*\/\/ --- end template ---$/;
|
|
15
|
+
// 1-based lines of the generated template region in a virtual module, or undefined when it has none
|
|
16
|
+
function templateRegionOf(content) {
|
|
17
|
+
const lines = content.split("\n");
|
|
18
|
+
const start = lines.findIndex((line) => TEMPLATE_REGION_START.test(line));
|
|
19
|
+
if (start === -1)
|
|
20
|
+
return undefined;
|
|
21
|
+
const end = lines.findIndex((line, index) => index > start && TEMPLATE_REGION_END.test(line));
|
|
22
|
+
return { from: start + 1, to: end === -1 ? Infinity : end + 1 };
|
|
23
|
+
}
|
|
24
|
+
// `__eventra_x_template_event__(expr)` (callText may be cut with an ellipsis)
|
|
25
|
+
function templateExpression(occurrence) {
|
|
26
|
+
const open = occurrence.callText.indexOf("(");
|
|
27
|
+
let expression = open === -1 ? occurrence.callText : occurrence.callText.slice(open + 1);
|
|
28
|
+
expression = expression.endsWith(")") ? expression.slice(0, -1) : expression;
|
|
29
|
+
// Angular qualifies a bare name with `this.`; the template never writes it
|
|
30
|
+
return occurrence.calleeText.includes("_angular_") ? expression.replace(/^this\./, "") : expression;
|
|
31
|
+
}
|
|
8
32
|
class PluginRegistry {
|
|
9
33
|
preprocessors = [];
|
|
10
34
|
sinkDetectors = [];
|
|
@@ -12,6 +36,7 @@ class PluginRegistry {
|
|
|
12
36
|
dynamicEventReporters = [];
|
|
13
37
|
includePatterns = [];
|
|
14
38
|
sourceToVirtuals = new Map();
|
|
39
|
+
templateRegions = new Map();
|
|
15
40
|
registerFilePreprocessor(preprocessor) {
|
|
16
41
|
if (this.preprocessors.some((p) => p.name === preprocessor.name)) {
|
|
17
42
|
throw new Error(`File preprocessor already registered: ${preprocessor.name}`);
|
|
@@ -75,10 +100,23 @@ class PluginRegistry {
|
|
|
75
100
|
}
|
|
76
101
|
const files = await preprocessor.process(input);
|
|
77
102
|
if (files.length > 0) {
|
|
103
|
+
// virtual files this source produced before and no longer does: their regions are not valid for anything else
|
|
104
|
+
for (const stale of this.sourceToVirtuals.get(sourceKey) ?? [])
|
|
105
|
+
this.templateRegions.delete(stale);
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const key = this.normalizePath(file.fileName);
|
|
108
|
+
const region = templateRegionOf(file.content);
|
|
109
|
+
if (region === undefined)
|
|
110
|
+
this.templateRegions.delete(key);
|
|
111
|
+
else
|
|
112
|
+
this.templateRegions.set(key, region);
|
|
113
|
+
}
|
|
78
114
|
this.sourceToVirtuals.set(sourceKey, files.map((file) => this.normalizePath(file.fileName)));
|
|
79
115
|
return files;
|
|
80
116
|
}
|
|
81
117
|
}
|
|
118
|
+
for (const stale of this.sourceToVirtuals.get(sourceKey) ?? [])
|
|
119
|
+
this.templateRegions.delete(stale);
|
|
82
120
|
this.sourceToVirtuals.delete(sourceKey);
|
|
83
121
|
return [{ fileName: input.fileName, content: input.content }];
|
|
84
122
|
}
|
|
@@ -101,10 +139,54 @@ class PluginRegistry {
|
|
|
101
139
|
return null;
|
|
102
140
|
}
|
|
103
141
|
async runDynamicEventReporters(occurrences) {
|
|
142
|
+
const attributed = this.attributeToSources(occurrences);
|
|
104
143
|
for (const reporter of this.dynamicEventReporters) {
|
|
105
|
-
await reporter.report({ occurrences });
|
|
144
|
+
await reporter.report({ occurrences: attributed });
|
|
106
145
|
}
|
|
107
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Occurrences are found in virtual modules (`App.vue.ts`). Point them back
|
|
149
|
+
* at the file the user wrote, and flag the ones that come from generated
|
|
150
|
+
* code so reporters do not print a line number that exists nowhere.
|
|
151
|
+
*/
|
|
152
|
+
attributeToSources(occurrences) {
|
|
153
|
+
if (this.sourceToVirtuals.size === 0) {
|
|
154
|
+
return occurrences;
|
|
155
|
+
}
|
|
156
|
+
const virtualToSource = new Map();
|
|
157
|
+
for (const [source, virtuals] of this.sourceToVirtuals) {
|
|
158
|
+
for (const virtual of virtuals) {
|
|
159
|
+
if (virtual !== source) {
|
|
160
|
+
virtualToSource.set(virtual, source);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return occurrences.map((occurrence) => {
|
|
165
|
+
const source = virtualToSource.get(this.normalizePath(occurrence.fileName));
|
|
166
|
+
if (!source) {
|
|
167
|
+
return occurrence;
|
|
168
|
+
}
|
|
169
|
+
const template = TEMPLATE_CALLEE.test(occurrence.calleeText);
|
|
170
|
+
if (template) {
|
|
171
|
+
return {
|
|
172
|
+
...occurrence,
|
|
173
|
+
fileName: source,
|
|
174
|
+
inTemplate: true,
|
|
175
|
+
eventExpression: templateExpression(occurrence),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// ordinary code the plugin copied out of the template: its line is in the generated text
|
|
179
|
+
const region = this.templateRegions.get(this.normalizePath(occurrence.fileName));
|
|
180
|
+
if (region !== undefined && occurrence.line >= region.from && occurrence.line <= region.to) {
|
|
181
|
+
return { ...occurrence, fileName: source, inTemplateCode: true };
|
|
182
|
+
}
|
|
183
|
+
// an Angular template module embeds the component class: lines there
|
|
184
|
+
// are generated, the real call is also reported from the class file
|
|
185
|
+
return source.toLowerCase().endsWith(".html")
|
|
186
|
+
? { ...occurrence, fileName: source, inGeneratedCode: true }
|
|
187
|
+
: { ...occurrence, fileName: source };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
108
190
|
normalizePath(fileName) {
|
|
109
191
|
return path_1.default.resolve(fileName).replace(/\\/g, "/");
|
|
110
192
|
}
|
package/dist/respawn.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
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.defaultRespawnDeps = exports.SCANNING_COMMANDS = exports.RESPAWN_ENV = void 0;
|
|
7
|
+
exports.wantedHeapMb = wantedHeapMb;
|
|
8
|
+
exports.usableMemory = usableMemory;
|
|
9
|
+
exports.respawnWithMoreHeap = respawnWithMoreHeap;
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const v8_1 = __importDefault(require("v8"));
|
|
13
|
+
/** Set on the child, so a process started with more memory never starts another one. */
|
|
14
|
+
exports.RESPAWN_ENV = "EVENTRA_HEAP_RESPAWNED";
|
|
15
|
+
/** The commands that read the whole project; the rest (`init`, `--version`, `--help`) stay in one process. */
|
|
16
|
+
exports.SCANNING_COMMANDS = new Set(["sync", "check", "watch", "send"]);
|
|
17
|
+
const MIB = 1024 * 1024;
|
|
18
|
+
// a project of tens of thousands of files needs a few GB while its program is built; more than
|
|
19
|
+
// this is never useful, and half the machine's memory is what can be taken without hurting it
|
|
20
|
+
const MAX_HEAP_MB = 8192;
|
|
21
|
+
const SHARE_OF_MEMORY = 0.5;
|
|
22
|
+
/** Signals a parent passes on, so stopping it stops the child too instead of leaving it running. */
|
|
23
|
+
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
24
|
+
/**
|
|
25
|
+
* The old-space limit (MB) worth starting with, or `undefined` to carry on as is: Node's default
|
|
26
|
+
* limit is a fraction of the memory and can be well below what a large monorepo needs.
|
|
27
|
+
*/
|
|
28
|
+
function wantedHeapMb(availableMemoryBytes, currentLimitBytes) {
|
|
29
|
+
const wanted = Math.min(MAX_HEAP_MB, Math.floor((availableMemoryBytes * SHARE_OF_MEMORY) / MIB));
|
|
30
|
+
// not worth a second process for a small difference
|
|
31
|
+
return wanted > (currentLimitBytes / MIB) * 1.25 ? wanted : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Memory this process can use: the smaller of the machine's and the container's limit (cgroups) when it has
|
|
35
|
+
* one. `constrained` is 0 when nothing limits the process, and undefined on a Node that does not report it.
|
|
36
|
+
*/
|
|
37
|
+
function usableMemory(total, constrained) {
|
|
38
|
+
return constrained !== undefined && constrained > 0 ? Math.min(total, constrained) : total;
|
|
39
|
+
}
|
|
40
|
+
function runChild(command, args, env) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const child = (0, child_process_1.spawn)(command, [...args], { stdio: "inherit", env });
|
|
43
|
+
const forwarders = FORWARDED_SIGNALS.map((signal) => {
|
|
44
|
+
const forward = () => child.kill(signal);
|
|
45
|
+
process.on(signal, forward);
|
|
46
|
+
return [signal, forward];
|
|
47
|
+
});
|
|
48
|
+
const done = (code) => {
|
|
49
|
+
for (const [signal, forward] of forwarders)
|
|
50
|
+
process.off(signal, forward);
|
|
51
|
+
resolve(code);
|
|
52
|
+
};
|
|
53
|
+
// a child that could not start at all (`error`) counts as a failed run, like one killed by a signal
|
|
54
|
+
child.on("error", () => done(1));
|
|
55
|
+
child.on("exit", (code) => done(code ?? 1));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const defaultRespawnDeps = () => ({
|
|
59
|
+
env: process.env,
|
|
60
|
+
execArgv: process.execArgv,
|
|
61
|
+
argv: process.argv,
|
|
62
|
+
execPath: process.execPath,
|
|
63
|
+
availableMemoryBytes: usableMemory(os_1.default.totalmem(), os_1.default.constrainedMemory?.()),
|
|
64
|
+
heapLimitBytes: v8_1.default.getHeapStatistics().heap_size_limit,
|
|
65
|
+
run: runChild,
|
|
66
|
+
exit: (code) => process.exit(code),
|
|
67
|
+
});
|
|
68
|
+
exports.defaultRespawnDeps = defaultRespawnDeps;
|
|
69
|
+
/**
|
|
70
|
+
* Runs a scanning command again in a child with a larger heap when this process would start with too
|
|
71
|
+
* little, and exits with the child's status. Resolves `true` when it did, so the caller stops there. An
|
|
72
|
+
* explicit `--max-old-space-size` (in NODE_OPTIONS or the command line) is the user's choice and is left alone.
|
|
73
|
+
*/
|
|
74
|
+
async function respawnWithMoreHeap(deps = (0, exports.defaultRespawnDeps)()) {
|
|
75
|
+
if (deps.env[exports.RESPAWN_ENV])
|
|
76
|
+
return false;
|
|
77
|
+
// a second process costs startup time, so only a command that scans a project is worth it
|
|
78
|
+
if (!exports.SCANNING_COMMANDS.has(deps.argv[2] ?? ""))
|
|
79
|
+
return false;
|
|
80
|
+
const explicit = [deps.env.NODE_OPTIONS ?? "", ...deps.execArgv].some((part) => part.includes("max-old-space-size"));
|
|
81
|
+
if (explicit)
|
|
82
|
+
return false;
|
|
83
|
+
const heapMb = wantedHeapMb(deps.availableMemoryBytes, deps.heapLimitBytes);
|
|
84
|
+
if (heapMb === undefined)
|
|
85
|
+
return false;
|
|
86
|
+
const code = await deps.run(deps.execPath, [`--max-old-space-size=${heapMb}`, ...deps.execArgv, ...deps.argv.slice(1)], { ...deps.env, [exports.RESPAWN_ENV]: "1" });
|
|
87
|
+
deps.exit(code);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventra_dev/eventra-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|