@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
|
@@ -14,8 +14,8 @@ const resolvedCallCache_1 = require("../analysis/cache/resolvedCallCache");
|
|
|
14
14
|
const resolvedExportCache_1 = require("../analysis/cache/resolvedExportCache");
|
|
15
15
|
const returnPropagationCache_1 = require("../analysis/cache/returnPropagationCache");
|
|
16
16
|
const analyzeFile_1 = require("../analysis/engine/analyzeFile");
|
|
17
|
+
const wrapperChains_1 = require("../analysis/scanner/wrapperChains");
|
|
17
18
|
const wrapperRegistry_1 = require("../analysis/symbols/wrapperRegistry");
|
|
18
|
-
const processFile_1 = require("../filesystem/processFile");
|
|
19
19
|
const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
20
20
|
const EMPTY_RESULT = () => ({
|
|
21
21
|
events: new Set(),
|
|
@@ -34,6 +34,10 @@ class EventraEngine {
|
|
|
34
34
|
fileIndices = new Map();
|
|
35
35
|
fileResults = new Map();
|
|
36
36
|
normalizedPathCache = new Map();
|
|
37
|
+
// files whose events count (the scanned sources); files that are in the
|
|
38
|
+
// program only because something imports them are never extracted, as in a
|
|
39
|
+
// fresh scan
|
|
40
|
+
scannedFiles = new Set();
|
|
37
41
|
plugins;
|
|
38
42
|
isPreloading = false;
|
|
39
43
|
lastConfig = {
|
|
@@ -47,14 +51,32 @@ class EventraEngine {
|
|
|
47
51
|
constructor(rootDir, plugins = (0, loadPlugins_1.createBuiltinPluginRegistry)()) {
|
|
48
52
|
this.plugins = plugins;
|
|
49
53
|
this.compiler = new compilerContext_1.CompilerContext(rootDir);
|
|
50
|
-
|
|
54
|
+
// resolved lazily: asking for the checker now would build the program
|
|
55
|
+
this.wrapperRegistry = new wrapperRegistry_1.WrapperRegistry(() => this.compiler.getChecker(), this.resolvedExportCache);
|
|
51
56
|
this.scheduler = new scheduler_1.Scheduler(async (updates) => {
|
|
52
57
|
const changedFiles = new Set();
|
|
58
|
+
// a file the graph has not seen yet changes what other files' imports resolve to
|
|
59
|
+
let fileSetChanged = false;
|
|
60
|
+
// Stage every file, rebuild the program ONCE, then refresh the import
|
|
61
|
+
// graph against that final program (one rebuild per flush, not per file).
|
|
53
62
|
for (const [file, content] of updates) {
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
if (!this.importGraph.hasFile(file))
|
|
64
|
+
fileSetChanged = true;
|
|
65
|
+
this.scannedFiles.add(file);
|
|
66
|
+
this.compiler.stageFile(file, content);
|
|
56
67
|
changedFiles.add(file);
|
|
57
68
|
}
|
|
69
|
+
this.compiler.flushUpdates();
|
|
70
|
+
const edgeChanged = new Set();
|
|
71
|
+
this.compiler.withResolutionCache(() => {
|
|
72
|
+
for (const file of changedFiles) {
|
|
73
|
+
this.updateImportGraph(file);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if (fileSetChanged) {
|
|
77
|
+
for (const file of this.refreshImportEdges(changedFiles))
|
|
78
|
+
edgeChanged.add(file);
|
|
79
|
+
}
|
|
58
80
|
// Program was rebuilt → every ts.Symbol from the previous program is now
|
|
59
81
|
// dead. WeakMap-keyed caches (incl. WrapperRegistry) must be dropped before
|
|
60
82
|
// we touch any NEW symbols, otherwise cross-file wrapper lookups miss.
|
|
@@ -64,18 +86,20 @@ class EventraEngine {
|
|
|
64
86
|
// symbols for every file, otherwise affected files cannot resolve
|
|
65
87
|
// wrappers defined in unaffected files (this is the watch-corruption bug).
|
|
66
88
|
this.reindexAll(this.lastConfig);
|
|
67
|
-
// Incremental extract: only re-extract changed files + their dependents
|
|
89
|
+
// Incremental extract: only re-extract changed files + their dependents
|
|
90
|
+
// (+ files whose imports now resolve differently, and their dependents).
|
|
68
91
|
// Unaffected files keep their previously-computed Set<string> events,
|
|
69
92
|
// which is safe (just strings, no AST refs).
|
|
70
93
|
const affected = new Set();
|
|
71
|
-
for (const file of changedFiles) {
|
|
94
|
+
for (const file of [...changedFiles, ...edgeChanged]) {
|
|
72
95
|
affected.add(file);
|
|
73
96
|
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
74
97
|
affected.add(dependent);
|
|
75
98
|
}
|
|
76
99
|
}
|
|
77
100
|
for (const file of affected) {
|
|
78
|
-
this.
|
|
101
|
+
if (this.scannedFiles.has(file))
|
|
102
|
+
this.extractFile(file, this.lastConfig);
|
|
79
103
|
}
|
|
80
104
|
});
|
|
81
105
|
}
|
|
@@ -94,6 +118,17 @@ class EventraEngine {
|
|
|
94
118
|
for (const sf of sources) {
|
|
95
119
|
this.indexFile(sf.fileName, config);
|
|
96
120
|
}
|
|
121
|
+
this.resolveWrapperChains();
|
|
122
|
+
}
|
|
123
|
+
// Wrappers that reach a sink only through another wrapper (`trackEvent` -> `capture` -> the SDK
|
|
124
|
+
// call) need every file indexed first, so this runs after the files are indexed, never per file.
|
|
125
|
+
resolveWrapperChains() {
|
|
126
|
+
const found = (0, wrapperChains_1.resolveWrapperChains)(this.fileIndices.values(), this.compiler.getChecker(), this.wrapperRegistry, this.resolvedCallCache, this.resolvedExportCache);
|
|
127
|
+
for (const [file, wrappers] of found) {
|
|
128
|
+
const index = this.fileIndices.get(file);
|
|
129
|
+
const kept = index.wrappers.filter((w) => !wrappers.some((n) => n.declaration === w.declaration));
|
|
130
|
+
this.fileIndices.set(file, { ...index, wrappers: [...kept, ...wrappers] });
|
|
131
|
+
}
|
|
97
132
|
}
|
|
98
133
|
beginPreload() {
|
|
99
134
|
this.isPreloading = true;
|
|
@@ -107,12 +142,14 @@ class EventraEngine {
|
|
|
107
142
|
this.normalizedPathCache.set(fileName, normalized);
|
|
108
143
|
return normalized;
|
|
109
144
|
}
|
|
145
|
+
// true when the file's resolved imports changed
|
|
110
146
|
updateImportGraph(fileName) {
|
|
111
147
|
const normalized = this.normalize(fileName);
|
|
112
148
|
const source = this.compiler.getSourceFile(normalized);
|
|
113
149
|
if (!source) {
|
|
150
|
+
const known = this.importGraph.hasFile(normalized);
|
|
114
151
|
this.importGraph.removeFile(normalized);
|
|
115
|
-
return;
|
|
152
|
+
return known;
|
|
116
153
|
}
|
|
117
154
|
const imports = [];
|
|
118
155
|
for (const statement of source.statements) {
|
|
@@ -132,22 +169,45 @@ class EventraEngine {
|
|
|
132
169
|
}
|
|
133
170
|
imports.push(this.normalize(resolved));
|
|
134
171
|
}
|
|
135
|
-
this.importGraph.updateFile(normalized, imports);
|
|
172
|
+
return this.importGraph.updateFile(normalized, imports);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Creating or deleting a file can change what OTHER files' imports resolve to,
|
|
176
|
+
* without those files changing: an import written before its target existed
|
|
177
|
+
* starts to resolve, a deleted target stops. Their edges are re-resolved
|
|
178
|
+
* against the current program; the files whose imports changed are returned
|
|
179
|
+
* so they get analysed again (the graph alone cannot know about them: the
|
|
180
|
+
* reverse edge of a file that did not exist was never recorded).
|
|
181
|
+
*/
|
|
182
|
+
refreshImportEdges(skip) {
|
|
183
|
+
const changed = new Set();
|
|
184
|
+
this.compiler.withResolutionCache(() => {
|
|
185
|
+
for (const file of this.importGraph.getFiles()) {
|
|
186
|
+
if (skip.has(file))
|
|
187
|
+
continue;
|
|
188
|
+
if (this.updateImportGraph(file))
|
|
189
|
+
changed.add(file);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return changed;
|
|
136
193
|
}
|
|
137
194
|
async preloadFile(fileName, content) {
|
|
138
195
|
if (!this.isPreloading) {
|
|
139
196
|
throw new Error("preload phase not active");
|
|
140
197
|
}
|
|
141
198
|
const normalized = this.normalize(fileName);
|
|
142
|
-
|
|
143
|
-
|
|
199
|
+
// processFile only echoed fileName/content back (its dependency list was
|
|
200
|
+
// unused here), so stage directly instead of parsing the file a second time.
|
|
201
|
+
this.compiler.stageFile(normalized, content);
|
|
144
202
|
}
|
|
145
203
|
endPreload() {
|
|
146
204
|
this.isPreloading = false;
|
|
147
205
|
this.compiler.flushUpdates();
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
206
|
+
this.compiler.withResolutionCache(() => {
|
|
207
|
+
for (const sourceFile of this.compiler.getAllSourceFiles()) {
|
|
208
|
+
this.updateImportGraph(sourceFile.fileName);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
151
211
|
}
|
|
152
212
|
async updateFile(fileName, content, config) {
|
|
153
213
|
this.lastConfig = config;
|
|
@@ -162,8 +222,10 @@ class EventraEngine {
|
|
|
162
222
|
this.refreshChecker();
|
|
163
223
|
const normalized = fileNames.map((f) => this.normalize(f));
|
|
164
224
|
for (const file of normalized) {
|
|
225
|
+
this.scannedFiles.add(file);
|
|
165
226
|
this.indexFile(file, config);
|
|
166
227
|
}
|
|
228
|
+
this.resolveWrapperChains();
|
|
167
229
|
for (const file of normalized) {
|
|
168
230
|
this.extractFile(file, config);
|
|
169
231
|
}
|
|
@@ -178,8 +240,62 @@ class EventraEngine {
|
|
|
178
240
|
this.reindexAll(config);
|
|
179
241
|
const normalized = fileNames.map((f) => this.normalize(f));
|
|
180
242
|
for (const file of normalized) {
|
|
181
|
-
this.
|
|
243
|
+
if (this.scannedFiles.has(file))
|
|
244
|
+
this.extractFile(file, config);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Files that are only imported (not scanned, e.g. `shared/events.ts` outside
|
|
249
|
+
* `include`) changed on disk, appeared or vanished. Their content is re-read
|
|
250
|
+
* (one program rebuild for the whole batch), the import graph is refreshed and
|
|
251
|
+
* the scanned files that depend on them are analysed again, so a long-running
|
|
252
|
+
* engine agrees with a fresh scan. Returns false when no content changed.
|
|
253
|
+
*/
|
|
254
|
+
async refreshExternalFiles(fileNames, config) {
|
|
255
|
+
this.lastConfig = config;
|
|
256
|
+
const normalized = fileNames.map((f) => this.normalize(f));
|
|
257
|
+
// dependents are read before the edges change
|
|
258
|
+
const affected = new Set();
|
|
259
|
+
for (const file of normalized) {
|
|
260
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
261
|
+
affected.add(dependent);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const changed = this.compiler.refreshFromDisk(normalized);
|
|
265
|
+
if (changed.length === 0) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
this.compiler.withResolutionCache(() => {
|
|
269
|
+
for (const file of changed) {
|
|
270
|
+
this.updateImportGraph(file);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// importers that had no edge before (a missing target appeared) or lost it
|
|
274
|
+
// (the target vanished), and everything depending on them
|
|
275
|
+
for (const file of this.refreshImportEdges(new Set(changed))) {
|
|
276
|
+
affected.add(file);
|
|
277
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
278
|
+
affected.add(dependent);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const file of changed) {
|
|
282
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
283
|
+
affected.add(dependent);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const file of changed) {
|
|
287
|
+
affected.delete(file);
|
|
182
288
|
}
|
|
289
|
+
await this.reanalyzeFiles([...affected], config);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
/** True when the file is in the program (scanned or only imported). */
|
|
293
|
+
hasProgramFile(fileName) {
|
|
294
|
+
return this.compiler.hasFile(this.normalize(fileName));
|
|
295
|
+
}
|
|
296
|
+
/** Project files in the program, for watching those that live outside the project root. */
|
|
297
|
+
getProjectFileNames() {
|
|
298
|
+
return this.compiler.getProjectFileNames();
|
|
183
299
|
}
|
|
184
300
|
indexFile(fileName, _config) {
|
|
185
301
|
const normalized = this.normalize(fileName);
|
|
@@ -204,8 +320,10 @@ class EventraEngine {
|
|
|
204
320
|
}
|
|
205
321
|
async scanFile(fileName, config) {
|
|
206
322
|
this.lastConfig = config;
|
|
323
|
+
this.scannedFiles.add(this.normalize(fileName));
|
|
207
324
|
this.refreshChecker();
|
|
208
325
|
this.indexFile(fileName, config);
|
|
326
|
+
this.resolveWrapperChains();
|
|
209
327
|
return this.extractFile(fileName, config);
|
|
210
328
|
}
|
|
211
329
|
async removeFile(fileName, config) {
|
|
@@ -215,8 +333,16 @@ class EventraEngine {
|
|
|
215
333
|
this.compiler.removeFile(normalized);
|
|
216
334
|
this.fileIndices.delete(normalized);
|
|
217
335
|
this.fileResults.delete(normalized);
|
|
336
|
+
this.scannedFiles.delete(normalized);
|
|
218
337
|
this.normalizedPathCache.delete(fileName);
|
|
219
338
|
this.normalizedPathCache.delete(normalized);
|
|
339
|
+
// importers of the removed file now resolve differently (and another file
|
|
340
|
+
// may now win a resolution the removed one was shadowing)
|
|
341
|
+
for (const file of this.refreshImportEdges(new Set([normalized]))) {
|
|
342
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
343
|
+
affected.add(dependent);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
220
346
|
const nextConfig = config ?? this.lastConfig;
|
|
221
347
|
const toReanalyze = [...affected].filter((f) => f !== normalized);
|
|
222
348
|
// reanalyzeFiles already resets caches + reindexes all sources,
|
|
@@ -259,7 +385,8 @@ class EventraEngine {
|
|
|
259
385
|
for (const result of this.fileResults.values()) {
|
|
260
386
|
occurrences.push(...result.dynamicOccurrences);
|
|
261
387
|
}
|
|
262
|
-
|
|
388
|
+
const byText = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
389
|
+
return occurrences.sort((a, b) => byText(a.fileName, b.fileName) || a.line - b.line || a.character - b.character);
|
|
263
390
|
}
|
|
264
391
|
}
|
|
265
392
|
exports.EventraEngine = EventraEngine;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPullRequestBuild = isPullRequestBuild;
|
|
4
|
+
exports.detectBranch = detectBranch;
|
|
5
|
+
exports.countUnresolved = countUnresolved;
|
|
6
|
+
exports.collectCatalogMeta = collectCatalogMeta;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const projectScanner_1 = require("./projectScanner");
|
|
9
|
+
const MAX_BRANCH_LENGTH = 200;
|
|
10
|
+
// CI systems usually check out a detached HEAD, so git cannot name the branch
|
|
11
|
+
// there; these variables can. First one set wins.
|
|
12
|
+
const BRANCH_ENV_VARS = [
|
|
13
|
+
"GITHUB_REF_NAME",
|
|
14
|
+
"CI_COMMIT_REF_NAME",
|
|
15
|
+
"BRANCH_NAME",
|
|
16
|
+
"BITBUCKET_BRANCH",
|
|
17
|
+
"CIRCLE_BRANCH",
|
|
18
|
+
"BUILDKITE_BRANCH",
|
|
19
|
+
"VERCEL_GIT_COMMIT_REF",
|
|
20
|
+
];
|
|
21
|
+
// A pull / merge request build checks out the PROPOSED code: a PR that deletes
|
|
22
|
+
// an event must not mark it removed on the project catalog.
|
|
23
|
+
const PULL_REQUEST_MARKERS = [
|
|
24
|
+
(env) => env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target",
|
|
25
|
+
(env) => (env.GITHUB_REF ?? "").startsWith("refs/pull/"),
|
|
26
|
+
(env) => Boolean(env.CI_MERGE_REQUEST_IID) || env.CI_PIPELINE_SOURCE === "merge_request_event",
|
|
27
|
+
(env) => Boolean(env.BITBUCKET_PR_ID),
|
|
28
|
+
(env) => Boolean(env.CIRCLE_PULL_REQUEST || env.CIRCLE_PR_NUMBER),
|
|
29
|
+
(env) => Boolean(env.BUILDKITE_PULL_REQUEST) && env.BUILDKITE_PULL_REQUEST !== "false",
|
|
30
|
+
(env) => Boolean(env.VERCEL_GIT_PULL_REQUEST_ID),
|
|
31
|
+
(env) => Boolean(env.TRAVIS_PULL_REQUEST) && env.TRAVIS_PULL_REQUEST !== "false",
|
|
32
|
+
(env) => Boolean(env.SYSTEM_PULLREQUEST_PULLREQUESTID) || env.BUILD_REASON === "PullRequest",
|
|
33
|
+
(env) => Boolean(env.CHANGE_ID),
|
|
34
|
+
];
|
|
35
|
+
/** True when the environment says this run is a pull / merge request build. */
|
|
36
|
+
function isPullRequestBuild(env = process.env) {
|
|
37
|
+
return PULL_REQUEST_MARKERS.some((marker) => marker(env));
|
|
38
|
+
}
|
|
39
|
+
/** Best-effort current branch. Never throws; returns undefined when unknown. */
|
|
40
|
+
function detectBranch(env = process.env, cwd = process.cwd()) {
|
|
41
|
+
for (const name of BRANCH_ENV_VARS) {
|
|
42
|
+
const value = env[name]?.trim();
|
|
43
|
+
if (value)
|
|
44
|
+
return value.slice(0, MAX_BRANCH_LENGTH);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const out = (0, child_process_1.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
48
|
+
cwd,
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
timeout: 3000,
|
|
51
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
52
|
+
}).trim();
|
|
53
|
+
// "HEAD" is what git prints for a detached checkout: not a branch name
|
|
54
|
+
if (out && out !== "HEAD")
|
|
55
|
+
return out.slice(0, MAX_BRANCH_LENGTH);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// not a git checkout, git missing or too slow
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function sameSet(a, b) {
|
|
63
|
+
if (a.length !== b.length)
|
|
64
|
+
return false;
|
|
65
|
+
const set = new Set(a);
|
|
66
|
+
return b.every((value) => set.has(value));
|
|
67
|
+
}
|
|
68
|
+
// An Angular template module embeds a copy of its component class, so the same
|
|
69
|
+
// call is reported twice: once from the class file and once from that copy
|
|
70
|
+
// (inGeneratedCode). Only the real one is counted.
|
|
71
|
+
function countUnresolved(occurrences) {
|
|
72
|
+
return occurrences.filter((o) => o.resolvedValues.length === 0 && !o.inGeneratedCode).length;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Scans the project again to find out whether the event list about to be sent
|
|
76
|
+
* is complete. Never throws: any failure means complete = false.
|
|
77
|
+
*/
|
|
78
|
+
async function collectCatalogMeta(config) {
|
|
79
|
+
const branch = detectBranch();
|
|
80
|
+
const withBranch = (meta) => (branch ? { ...meta, branch } : meta);
|
|
81
|
+
try {
|
|
82
|
+
const { engine, skipped } = await (0, projectScanner_1.scanProject)(config);
|
|
83
|
+
const scanned = engine.getAllEvents();
|
|
84
|
+
const dynamicCount = countUnresolved(engine.getAllDynamicOccurrences());
|
|
85
|
+
const complete = skipped.length === 0 && !isPullRequestBuild() && sameSet(config.events ?? [], scanned);
|
|
86
|
+
return withBranch({ complete, dynamicCount });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return withBranch({ complete: false });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OVERSIZED_REASON = exports.MAX_FILE_CHARS = void 0;
|
|
4
|
+
exports.isOversized = isOversized;
|
|
5
|
+
// Files above this size are generated or vendored output (bundles, minified
|
|
6
|
+
// code, big fixtures), not something to analyse. One limit for every command,
|
|
7
|
+
// so `sync` and `watch` can never disagree about a file.
|
|
8
|
+
exports.MAX_FILE_CHARS = 2_000_000;
|
|
9
|
+
exports.OVERSIZED_REASON = "file is larger than 2 MB";
|
|
10
|
+
function isOversized(content) {
|
|
11
|
+
return content.length > exports.MAX_FILE_CHARS;
|
|
12
|
+
}
|
|
@@ -12,59 +12,47 @@ const EventraEngine_1 = require("./EventraEngine");
|
|
|
12
12
|
const processFile_1 = require("../filesystem/processFile");
|
|
13
13
|
const getVirtualFile_1 = require("../filesystem/getVirtualFile");
|
|
14
14
|
const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
15
|
+
const scanFilters_1 = require("./scanFilters");
|
|
16
|
+
const skipped_1 = require("./skipped");
|
|
17
|
+
const fileLimits_1 = require("./fileLimits");
|
|
15
18
|
const SDK_TYPES_FILE = "__eventra_sdk_types__.d.ts";
|
|
16
|
-
async function getParsedFile(file, cache) {
|
|
17
|
-
const existing = cache.get(file);
|
|
18
|
-
if (existing)
|
|
19
|
-
return existing;
|
|
20
|
-
const raw = await promises_1.default.readFile(file, "utf8");
|
|
21
|
-
const parsed = await (0, processFile_1.processFile)(file, raw);
|
|
22
|
-
cache.set(file, parsed);
|
|
23
|
-
return parsed;
|
|
24
|
-
}
|
|
25
19
|
async function scanProject(config) {
|
|
26
20
|
const plugins = await (0, loadPlugins_1.createPluginRegistry)(config);
|
|
27
21
|
const include = [...config.sync.include, ...plugins.getIncludePatterns()];
|
|
28
22
|
const files = await (0, fast_glob_1.default)(include, {
|
|
29
|
-
ignore: config.sync.exclude,
|
|
23
|
+
ignore: (0, scanFilters_1.expandExcludePatterns)(config.sync.exclude),
|
|
30
24
|
absolute: true,
|
|
31
25
|
});
|
|
32
26
|
const engine = new EventraEngine_1.EventraEngine(process.cwd(), plugins);
|
|
33
27
|
const cache = new Map();
|
|
34
28
|
const toScan = new Set();
|
|
29
|
+
const skipped = [];
|
|
30
|
+
const ignored = [];
|
|
35
31
|
for (const file of files) {
|
|
36
32
|
try {
|
|
37
33
|
const raw = await promises_1.default.readFile(file, "utf8");
|
|
38
|
-
if (raw
|
|
34
|
+
if ((0, fileLimits_1.isOversized)(raw)) {
|
|
35
|
+
ignored.push({ file, reason: fileLimits_1.OVERSIZED_REASON });
|
|
39
36
|
continue;
|
|
37
|
+
}
|
|
40
38
|
const virtualFiles = await plugins.preprocessFile({ fileName: file, content: raw });
|
|
41
39
|
for (const virtualFile of virtualFiles) {
|
|
42
40
|
const parsed = await (0, processFile_1.processFile)(virtualFile.fileName, virtualFile.content);
|
|
43
41
|
const scanPath = path_1.default.resolve(virtualFile.fileName);
|
|
44
42
|
cache.set(scanPath, parsed);
|
|
45
43
|
toScan.add(scanPath);
|
|
46
|
-
for (const dep of parsed.dependencies) {
|
|
47
|
-
if (dep.startsWith(".") || dep.startsWith("/")) {
|
|
48
|
-
toScan.add(path_1.default.resolve(path_1.default.dirname(scanPath), dep));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
44
|
}
|
|
52
45
|
}
|
|
53
46
|
catch (err) {
|
|
54
|
-
|
|
47
|
+
skipped.push((0, skipped_1.skippedFrom)(file, err));
|
|
55
48
|
}
|
|
56
49
|
}
|
|
57
50
|
const virtualFiles = [SDK_TYPES_FILE, ...toScan].map((f) => (0, getVirtualFile_1.getVirtualFile)(f));
|
|
58
51
|
engine.beginPreload();
|
|
59
52
|
await engine.preloadFile(SDK_TYPES_FILE, eventraSdk_1.EVENTRA_SDK_SHIM);
|
|
60
53
|
for (const file of toScan) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await engine.preloadFile((0, getVirtualFile_1.getVirtualFile)(file), parsed.content);
|
|
64
|
-
}
|
|
65
|
-
catch (err) {
|
|
66
|
-
console.error(`preload skip: ${file}`, err instanceof Error ? err.message : err);
|
|
67
|
-
}
|
|
54
|
+
// every path in toScan was parsed and cached in the loop above
|
|
55
|
+
await engine.preloadFile((0, getVirtualFile_1.getVirtualFile)(file), cache.get(file).content);
|
|
68
56
|
}
|
|
69
57
|
engine.endPreload();
|
|
70
58
|
await engine.runFullAnalysis(virtualFiles, config);
|
|
@@ -77,5 +65,7 @@ async function scanProject(config) {
|
|
|
77
65
|
results,
|
|
78
66
|
files,
|
|
79
67
|
plugins,
|
|
68
|
+
skipped,
|
|
69
|
+
ignored,
|
|
80
70
|
};
|
|
81
71
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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.expandExcludePatterns = expandExcludePatterns;
|
|
7
|
+
exports.globToRegExp = globToRegExp;
|
|
8
|
+
exports.toPosix = toPosix;
|
|
9
|
+
exports.createFileFilter = createFileFilter;
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const GLOB_CHARS = /[*?[\]{}()!]/;
|
|
12
|
+
/**
|
|
13
|
+
* `sync.exclude` entries are written as bare directory names (`node_modules`,
|
|
14
|
+
* `dist`). fast-glob only matches a bare name against the repo root, so a
|
|
15
|
+
* nested `packages/x/node_modules` would still be scanned. A plain name is
|
|
16
|
+
* therefore expanded to match at any depth; entries that already contain a
|
|
17
|
+
* `/` or glob characters are left exactly as written.
|
|
18
|
+
*/
|
|
19
|
+
function expandExcludePatterns(exclude) {
|
|
20
|
+
const out = new Set();
|
|
21
|
+
for (const entry of exclude) {
|
|
22
|
+
const pattern = entry.trim();
|
|
23
|
+
if (!pattern)
|
|
24
|
+
continue;
|
|
25
|
+
if (pattern.includes("/") || GLOB_CHARS.test(pattern)) {
|
|
26
|
+
out.add(pattern);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
out.add(`**/${pattern}`);
|
|
30
|
+
out.add(`**/${pattern}/**`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [...out];
|
|
34
|
+
}
|
|
35
|
+
// fast-glob runs with dot:false: `*`, `**` and `?` never match a path segment
|
|
36
|
+
// that starts with a dot (.svelte-kit, .nuxt, .turbo, ...) unless the glob
|
|
37
|
+
// spells the dot itself (`.github/**`). The watch filter must agree, or
|
|
38
|
+
// generated output in those directories leaks in while `sync` ignores it.
|
|
39
|
+
const NO_DOT = "(?!\\.)";
|
|
40
|
+
/** Converts the glob subset used by eventra configs (`**`, `*`, `?`, `{a,b}`, `[..]`) to a RegExp. */
|
|
41
|
+
function globToRegExp(glob) {
|
|
42
|
+
const source = glob.replace(/^\.\//, "");
|
|
43
|
+
let out = "";
|
|
44
|
+
let braceDepth = 0;
|
|
45
|
+
// a segment starts at the beginning, after a slash, or at a brace alternative
|
|
46
|
+
const atSegmentStart = (i) => i === 0 || "/{,".includes(source[i - 1]);
|
|
47
|
+
for (let i = 0; i < source.length; i++) {
|
|
48
|
+
const ch = source[i];
|
|
49
|
+
if (ch === "*") {
|
|
50
|
+
if (source[i + 1] === "*") {
|
|
51
|
+
if (source[i + 2] === "/" && atSegmentStart(i)) {
|
|
52
|
+
// zero or more whole segments, none of them hidden
|
|
53
|
+
out += `(?:${NO_DOT}[^/]+/)*`;
|
|
54
|
+
i += 2;
|
|
55
|
+
}
|
|
56
|
+
else if (i + 2 >= source.length && atSegmentStart(i)) {
|
|
57
|
+
// trailing `**`: everything below, again without hidden segments
|
|
58
|
+
out += `(?:${NO_DOT}[^/]+/)*(?:${NO_DOT}[^/]*)?`;
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
out += ".*";
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out += `${atSegmentStart(i) ? NO_DOT : ""}[^/]*`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (ch === "?") {
|
|
71
|
+
out += `${atSegmentStart(i) ? NO_DOT : ""}[^/]`;
|
|
72
|
+
}
|
|
73
|
+
else if (ch === "{") {
|
|
74
|
+
braceDepth++;
|
|
75
|
+
out += "(?:";
|
|
76
|
+
}
|
|
77
|
+
else if (ch === "}" && braceDepth > 0) {
|
|
78
|
+
braceDepth--;
|
|
79
|
+
out += ")";
|
|
80
|
+
}
|
|
81
|
+
else if (ch === "," && braceDepth > 0) {
|
|
82
|
+
out += "|";
|
|
83
|
+
}
|
|
84
|
+
else if (ch === "[") {
|
|
85
|
+
const end = source.indexOf("]", i + 1);
|
|
86
|
+
if (end === -1) {
|
|
87
|
+
out += "\\[";
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
out += source.slice(i, end + 1);
|
|
91
|
+
i = end;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
out += ch.replace(/[.+^$()|\\\]]/g, "\\$&");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new RegExp(`^${out}$`);
|
|
99
|
+
}
|
|
100
|
+
function toPosix(file) {
|
|
101
|
+
return file.replace(/\\/g, "/");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Mirrors what the initial fast-glob scan accepts, so watch mode reacts to the
|
|
105
|
+
* same files as `sync`.
|
|
106
|
+
*/
|
|
107
|
+
function createFileFilter(include, exclude, cwd, alwaysExcluded = []) {
|
|
108
|
+
const includeRes = include.filter((p) => !p.startsWith("!")).map(globToRegExp);
|
|
109
|
+
const negatedRes = include
|
|
110
|
+
.filter((p) => p.startsWith("!"))
|
|
111
|
+
.map((p) => globToRegExp(p.slice(1)));
|
|
112
|
+
const excludeRes = exclude
|
|
113
|
+
.map((p) => p.trim())
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.map((p) => {
|
|
116
|
+
const globbed = p.includes("/") || GLOB_CHARS.test(p) ? p : `**/${p}`;
|
|
117
|
+
return globToRegExp(globbed.replace(/\/+$/, ""));
|
|
118
|
+
});
|
|
119
|
+
const relative = (absPath) => toPosix(path_1.default.relative(cwd, absPath));
|
|
120
|
+
const isExcluded = (absPath) => {
|
|
121
|
+
const rel = relative(absPath);
|
|
122
|
+
if (alwaysExcluded.includes(rel))
|
|
123
|
+
return true;
|
|
124
|
+
const segments = rel.split("/");
|
|
125
|
+
// test every leading prefix so a match on a directory excludes what is inside it
|
|
126
|
+
for (let n = 1; n <= segments.length; n++) {
|
|
127
|
+
const prefix = segments.slice(0, n).join("/");
|
|
128
|
+
if (excludeRes.some((re) => re.test(prefix)))
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
isExcluded,
|
|
135
|
+
isIncluded(absPath) {
|
|
136
|
+
const rel = relative(absPath);
|
|
137
|
+
if (rel === ".." || rel.startsWith("../") || path_1.default.isAbsolute(rel))
|
|
138
|
+
return false;
|
|
139
|
+
if (isExcluded(absPath))
|
|
140
|
+
return false;
|
|
141
|
+
if (negatedRes.some((re) => re.test(rel)))
|
|
142
|
+
return false;
|
|
143
|
+
return includeRes.some((re) => re.test(rel));
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
package/dist/core/scanResults.js
CHANGED
|
@@ -3,16 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildConfigFromScan = buildConfigFromScan;
|
|
4
4
|
exports.persistScanResults = persistScanResults;
|
|
5
5
|
const config_1 = require("../config/config");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const uniqueSorted = (values) => [...new Set(values)].sort();
|
|
7
|
+
function buildConfigFromScan(config, engine, options = {}) {
|
|
8
|
+
const foundWrappers = engine.getAllFunctionWrappers();
|
|
9
|
+
const foundEvents = engine.getAllEvents();
|
|
10
|
+
if (options.preserveKnown) {
|
|
11
|
+
return {
|
|
12
|
+
...config,
|
|
13
|
+
events: uniqueSorted([...(config.events ?? []), ...foundEvents]),
|
|
14
|
+
functionWrappers: uniqueSorted([
|
|
15
|
+
...(config.functionWrappers ?? []).map((w) => w.name),
|
|
16
|
+
...foundWrappers,
|
|
17
|
+
]).map((name) => ({ name })),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const functionWrappers = foundWrappers.map((name) => ({ name }));
|
|
10
21
|
return {
|
|
11
22
|
...config,
|
|
12
|
-
events:
|
|
23
|
+
events: foundEvents,
|
|
13
24
|
functionWrappers,
|
|
14
25
|
};
|
|
15
26
|
}
|
|
16
|
-
async function persistScanResults(config, engine) {
|
|
17
|
-
|
|
27
|
+
async function persistScanResults(config, engine, options = {}) {
|
|
28
|
+
const next = buildConfigFromScan(config, engine, options);
|
|
29
|
+
await (0, config_1.saveConfig)(next);
|
|
30
|
+
// what is now in eventra.json: callers report these counts
|
|
31
|
+
return (0, config_1.normalizeConfig)(next);
|
|
18
32
|
}
|