@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
|
@@ -15,7 +15,6 @@ 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
17
|
const wrapperRegistry_1 = require("../analysis/symbols/wrapperRegistry");
|
|
18
|
-
const processFile_1 = require("../filesystem/processFile");
|
|
19
18
|
const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
20
19
|
const EMPTY_RESULT = () => ({
|
|
21
20
|
events: new Set(),
|
|
@@ -34,6 +33,10 @@ class EventraEngine {
|
|
|
34
33
|
fileIndices = new Map();
|
|
35
34
|
fileResults = new Map();
|
|
36
35
|
normalizedPathCache = new Map();
|
|
36
|
+
// files whose events count (the scanned sources); files that are in the
|
|
37
|
+
// program only because something imports them are never extracted, as in a
|
|
38
|
+
// fresh scan
|
|
39
|
+
scannedFiles = new Set();
|
|
37
40
|
plugins;
|
|
38
41
|
isPreloading = false;
|
|
39
42
|
lastConfig = {
|
|
@@ -47,14 +50,32 @@ class EventraEngine {
|
|
|
47
50
|
constructor(rootDir, plugins = (0, loadPlugins_1.createBuiltinPluginRegistry)()) {
|
|
48
51
|
this.plugins = plugins;
|
|
49
52
|
this.compiler = new compilerContext_1.CompilerContext(rootDir);
|
|
50
|
-
|
|
53
|
+
// resolved lazily: asking for the checker now would build the program
|
|
54
|
+
this.wrapperRegistry = new wrapperRegistry_1.WrapperRegistry(() => this.compiler.getChecker(), this.resolvedExportCache);
|
|
51
55
|
this.scheduler = new scheduler_1.Scheduler(async (updates) => {
|
|
52
56
|
const changedFiles = new Set();
|
|
57
|
+
// a file the graph has not seen yet changes what other files' imports resolve to
|
|
58
|
+
let fileSetChanged = false;
|
|
59
|
+
// Stage every file, rebuild the program ONCE, then refresh the import
|
|
60
|
+
// graph against that final program (one rebuild per flush, not per file).
|
|
53
61
|
for (const [file, content] of updates) {
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
if (!this.importGraph.hasFile(file))
|
|
63
|
+
fileSetChanged = true;
|
|
64
|
+
this.scannedFiles.add(file);
|
|
65
|
+
this.compiler.stageFile(file, content);
|
|
56
66
|
changedFiles.add(file);
|
|
57
67
|
}
|
|
68
|
+
this.compiler.flushUpdates();
|
|
69
|
+
const edgeChanged = new Set();
|
|
70
|
+
this.compiler.withResolutionCache(() => {
|
|
71
|
+
for (const file of changedFiles) {
|
|
72
|
+
this.updateImportGraph(file);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (fileSetChanged) {
|
|
76
|
+
for (const file of this.refreshImportEdges(changedFiles))
|
|
77
|
+
edgeChanged.add(file);
|
|
78
|
+
}
|
|
58
79
|
// Program was rebuilt → every ts.Symbol from the previous program is now
|
|
59
80
|
// dead. WeakMap-keyed caches (incl. WrapperRegistry) must be dropped before
|
|
60
81
|
// we touch any NEW symbols, otherwise cross-file wrapper lookups miss.
|
|
@@ -64,18 +85,20 @@ class EventraEngine {
|
|
|
64
85
|
// symbols for every file, otherwise affected files cannot resolve
|
|
65
86
|
// wrappers defined in unaffected files (this is the watch-corruption bug).
|
|
66
87
|
this.reindexAll(this.lastConfig);
|
|
67
|
-
// Incremental extract: only re-extract changed files + their dependents
|
|
88
|
+
// Incremental extract: only re-extract changed files + their dependents
|
|
89
|
+
// (+ files whose imports now resolve differently, and their dependents).
|
|
68
90
|
// Unaffected files keep their previously-computed Set<string> events,
|
|
69
91
|
// which is safe (just strings, no AST refs).
|
|
70
92
|
const affected = new Set();
|
|
71
|
-
for (const file of changedFiles) {
|
|
93
|
+
for (const file of [...changedFiles, ...edgeChanged]) {
|
|
72
94
|
affected.add(file);
|
|
73
95
|
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
74
96
|
affected.add(dependent);
|
|
75
97
|
}
|
|
76
98
|
}
|
|
77
99
|
for (const file of affected) {
|
|
78
|
-
this.
|
|
100
|
+
if (this.scannedFiles.has(file))
|
|
101
|
+
this.extractFile(file, this.lastConfig);
|
|
79
102
|
}
|
|
80
103
|
});
|
|
81
104
|
}
|
|
@@ -107,12 +130,14 @@ class EventraEngine {
|
|
|
107
130
|
this.normalizedPathCache.set(fileName, normalized);
|
|
108
131
|
return normalized;
|
|
109
132
|
}
|
|
133
|
+
// true when the file's resolved imports changed
|
|
110
134
|
updateImportGraph(fileName) {
|
|
111
135
|
const normalized = this.normalize(fileName);
|
|
112
136
|
const source = this.compiler.getSourceFile(normalized);
|
|
113
137
|
if (!source) {
|
|
138
|
+
const known = this.importGraph.hasFile(normalized);
|
|
114
139
|
this.importGraph.removeFile(normalized);
|
|
115
|
-
return;
|
|
140
|
+
return known;
|
|
116
141
|
}
|
|
117
142
|
const imports = [];
|
|
118
143
|
for (const statement of source.statements) {
|
|
@@ -132,22 +157,45 @@ class EventraEngine {
|
|
|
132
157
|
}
|
|
133
158
|
imports.push(this.normalize(resolved));
|
|
134
159
|
}
|
|
135
|
-
this.importGraph.updateFile(normalized, imports);
|
|
160
|
+
return this.importGraph.updateFile(normalized, imports);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Creating or deleting a file can change what OTHER files' imports resolve to,
|
|
164
|
+
* without those files changing: an import written before its target existed
|
|
165
|
+
* starts to resolve, a deleted target stops. Their edges are re-resolved
|
|
166
|
+
* against the current program; the files whose imports changed are returned
|
|
167
|
+
* so they get analysed again (the graph alone cannot know about them: the
|
|
168
|
+
* reverse edge of a file that did not exist was never recorded).
|
|
169
|
+
*/
|
|
170
|
+
refreshImportEdges(skip) {
|
|
171
|
+
const changed = new Set();
|
|
172
|
+
this.compiler.withResolutionCache(() => {
|
|
173
|
+
for (const file of this.importGraph.getFiles()) {
|
|
174
|
+
if (skip.has(file))
|
|
175
|
+
continue;
|
|
176
|
+
if (this.updateImportGraph(file))
|
|
177
|
+
changed.add(file);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return changed;
|
|
136
181
|
}
|
|
137
182
|
async preloadFile(fileName, content) {
|
|
138
183
|
if (!this.isPreloading) {
|
|
139
184
|
throw new Error("preload phase not active");
|
|
140
185
|
}
|
|
141
186
|
const normalized = this.normalize(fileName);
|
|
142
|
-
|
|
143
|
-
|
|
187
|
+
// processFile only echoed fileName/content back (its dependency list was
|
|
188
|
+
// unused here), so stage directly instead of parsing the file a second time.
|
|
189
|
+
this.compiler.stageFile(normalized, content);
|
|
144
190
|
}
|
|
145
191
|
endPreload() {
|
|
146
192
|
this.isPreloading = false;
|
|
147
193
|
this.compiler.flushUpdates();
|
|
148
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
194
|
+
this.compiler.withResolutionCache(() => {
|
|
195
|
+
for (const sourceFile of this.compiler.getAllSourceFiles()) {
|
|
196
|
+
this.updateImportGraph(sourceFile.fileName);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
151
199
|
}
|
|
152
200
|
async updateFile(fileName, content, config) {
|
|
153
201
|
this.lastConfig = config;
|
|
@@ -162,6 +210,7 @@ class EventraEngine {
|
|
|
162
210
|
this.refreshChecker();
|
|
163
211
|
const normalized = fileNames.map((f) => this.normalize(f));
|
|
164
212
|
for (const file of normalized) {
|
|
213
|
+
this.scannedFiles.add(file);
|
|
165
214
|
this.indexFile(file, config);
|
|
166
215
|
}
|
|
167
216
|
for (const file of normalized) {
|
|
@@ -178,8 +227,62 @@ class EventraEngine {
|
|
|
178
227
|
this.reindexAll(config);
|
|
179
228
|
const normalized = fileNames.map((f) => this.normalize(f));
|
|
180
229
|
for (const file of normalized) {
|
|
181
|
-
this.
|
|
230
|
+
if (this.scannedFiles.has(file))
|
|
231
|
+
this.extractFile(file, config);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Files that are only imported (not scanned, e.g. `shared/events.ts` outside
|
|
236
|
+
* `include`) changed on disk, appeared or vanished. Their content is re-read
|
|
237
|
+
* (one program rebuild for the whole batch), the import graph is refreshed and
|
|
238
|
+
* the scanned files that depend on them are analysed again, so a long-running
|
|
239
|
+
* engine agrees with a fresh scan. Returns false when no content changed.
|
|
240
|
+
*/
|
|
241
|
+
async refreshExternalFiles(fileNames, config) {
|
|
242
|
+
this.lastConfig = config;
|
|
243
|
+
const normalized = fileNames.map((f) => this.normalize(f));
|
|
244
|
+
// dependents are read before the edges change
|
|
245
|
+
const affected = new Set();
|
|
246
|
+
for (const file of normalized) {
|
|
247
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
248
|
+
affected.add(dependent);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const changed = this.compiler.refreshFromDisk(normalized);
|
|
252
|
+
if (changed.length === 0) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
this.compiler.withResolutionCache(() => {
|
|
256
|
+
for (const file of changed) {
|
|
257
|
+
this.updateImportGraph(file);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// importers that had no edge before (a missing target appeared) or lost it
|
|
261
|
+
// (the target vanished), and everything depending on them
|
|
262
|
+
for (const file of this.refreshImportEdges(new Set(changed))) {
|
|
263
|
+
affected.add(file);
|
|
264
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
265
|
+
affected.add(dependent);
|
|
266
|
+
}
|
|
182
267
|
}
|
|
268
|
+
for (const file of changed) {
|
|
269
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
270
|
+
affected.add(dependent);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const file of changed) {
|
|
274
|
+
affected.delete(file);
|
|
275
|
+
}
|
|
276
|
+
await this.reanalyzeFiles([...affected], config);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
/** True when the file is in the program (scanned or only imported). */
|
|
280
|
+
hasProgramFile(fileName) {
|
|
281
|
+
return this.compiler.hasFile(this.normalize(fileName));
|
|
282
|
+
}
|
|
283
|
+
/** Project files in the program, for watching those that live outside the project root. */
|
|
284
|
+
getProjectFileNames() {
|
|
285
|
+
return this.compiler.getProjectFileNames();
|
|
183
286
|
}
|
|
184
287
|
indexFile(fileName, _config) {
|
|
185
288
|
const normalized = this.normalize(fileName);
|
|
@@ -204,6 +307,7 @@ class EventraEngine {
|
|
|
204
307
|
}
|
|
205
308
|
async scanFile(fileName, config) {
|
|
206
309
|
this.lastConfig = config;
|
|
310
|
+
this.scannedFiles.add(this.normalize(fileName));
|
|
207
311
|
this.refreshChecker();
|
|
208
312
|
this.indexFile(fileName, config);
|
|
209
313
|
return this.extractFile(fileName, config);
|
|
@@ -215,8 +319,16 @@ class EventraEngine {
|
|
|
215
319
|
this.compiler.removeFile(normalized);
|
|
216
320
|
this.fileIndices.delete(normalized);
|
|
217
321
|
this.fileResults.delete(normalized);
|
|
322
|
+
this.scannedFiles.delete(normalized);
|
|
218
323
|
this.normalizedPathCache.delete(fileName);
|
|
219
324
|
this.normalizedPathCache.delete(normalized);
|
|
325
|
+
// importers of the removed file now resolve differently (and another file
|
|
326
|
+
// may now win a resolution the removed one was shadowing)
|
|
327
|
+
for (const file of this.refreshImportEdges(new Set([normalized]))) {
|
|
328
|
+
for (const dependent of this.importGraph.collectDependents(file)) {
|
|
329
|
+
affected.add(dependent);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
220
332
|
const nextConfig = config ?? this.lastConfig;
|
|
221
333
|
const toReanalyze = [...affected].filter((f) => f !== normalized);
|
|
222
334
|
// reanalyzeFiles already resets caches + reindexes all sources,
|
|
@@ -259,7 +371,8 @@ class EventraEngine {
|
|
|
259
371
|
for (const result of this.fileResults.values()) {
|
|
260
372
|
occurrences.push(...result.dynamicOccurrences);
|
|
261
373
|
}
|
|
262
|
-
|
|
374
|
+
const byText = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
375
|
+
return occurrences.sort((a, b) => byText(a.fileName, b.fileName) || a.line - b.line || a.character - b.character);
|
|
263
376
|
}
|
|
264
377
|
}
|
|
265
378
|
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
|
}
|