@eventra_dev/eventra-cli 2.2.0 → 2.3.1
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 +35 -7
- package/dist/analysis/extractor/extractor.js +2 -1
- package/dist/analysis/resolver/callResolver.js +81 -3
- 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/sdk/isEventraTrackCall.js +79 -3
- package/dist/analysis/symbols/symbolUtils.js +41 -0
- package/dist/cli/init.js +45 -10
- package/dist/cli/projectDependencies.js +8 -3
- package/dist/compiler/compilerContext.js +158 -16
- package/dist/compiler/workspacePackages.js +58 -0
- package/dist/config/config.js +16 -1
- package/dist/core/EventraEngine.js +14 -0
- package/dist/index.js +49 -41
- package/dist/plugin/builtins/configuredSinks.js +84 -17
- package/dist/plugin/builtins/consoleDynamicEventReporter.js +3 -0
- package/dist/plugin/registry.js +33 -0
- package/dist/respawn.js +89 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -28,6 +28,9 @@ const consoleDynamicEventReporter = {
|
|
|
28
28
|
if (occ.inTemplate) {
|
|
29
29
|
console.log(chalk_1.default.yellow(`~ template of ${file}: event="${occ.eventExpression ?? ""}"${values}`));
|
|
30
30
|
}
|
|
31
|
+
else if (occ.inTemplateCode) {
|
|
32
|
+
console.log(chalk_1.default.yellow(`~ template of ${file}: ${occ.calleeText}(...)${values}`));
|
|
33
|
+
}
|
|
31
34
|
else {
|
|
32
35
|
console.log(chalk_1.default.yellow(`~ ${file}:${occ.line} ${occ.calleeText}(...)${values}`));
|
|
33
36
|
}
|
package/dist/plugin/registry.js
CHANGED
|
@@ -7,6 +7,20 @@ exports.PluginRegistry = void 0;
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
// the synthetic callee each framework plugin uses for a template `event=...`
|
|
9
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
|
+
}
|
|
10
24
|
// `__eventra_x_template_event__(expr)` (callText may be cut with an ellipsis)
|
|
11
25
|
function templateExpression(occurrence) {
|
|
12
26
|
const open = occurrence.callText.indexOf("(");
|
|
@@ -22,6 +36,7 @@ class PluginRegistry {
|
|
|
22
36
|
dynamicEventReporters = [];
|
|
23
37
|
includePatterns = [];
|
|
24
38
|
sourceToVirtuals = new Map();
|
|
39
|
+
templateRegions = new Map();
|
|
25
40
|
registerFilePreprocessor(preprocessor) {
|
|
26
41
|
if (this.preprocessors.some((p) => p.name === preprocessor.name)) {
|
|
27
42
|
throw new Error(`File preprocessor already registered: ${preprocessor.name}`);
|
|
@@ -85,10 +100,23 @@ class PluginRegistry {
|
|
|
85
100
|
}
|
|
86
101
|
const files = await preprocessor.process(input);
|
|
87
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
|
+
}
|
|
88
114
|
this.sourceToVirtuals.set(sourceKey, files.map((file) => this.normalizePath(file.fileName)));
|
|
89
115
|
return files;
|
|
90
116
|
}
|
|
91
117
|
}
|
|
118
|
+
for (const stale of this.sourceToVirtuals.get(sourceKey) ?? [])
|
|
119
|
+
this.templateRegions.delete(stale);
|
|
92
120
|
this.sourceToVirtuals.delete(sourceKey);
|
|
93
121
|
return [{ fileName: input.fileName, content: input.content }];
|
|
94
122
|
}
|
|
@@ -147,6 +175,11 @@ class PluginRegistry {
|
|
|
147
175
|
eventExpression: templateExpression(occurrence),
|
|
148
176
|
};
|
|
149
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
|
+
}
|
|
150
183
|
// an Angular template module embeds the component class: lines there
|
|
151
184
|
// are generated, the real call is also reported from the class file
|
|
152
185
|
return source.toLowerCase().endsWith(".html")
|
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
|
+
}
|