@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
|
@@ -8,13 +8,30 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const typescript_1 = __importDefault(require("typescript"));
|
|
10
10
|
const documentRegistry_1 = require("./documentRegistry");
|
|
11
|
+
const workspacePackages_1 = require("./workspacePackages");
|
|
11
12
|
class CompilerContext {
|
|
12
13
|
rootDir;
|
|
13
14
|
registry = new documentRegistry_1.DocumentRegistry();
|
|
15
|
+
// Parsed files survive a program rebuild: a flush changes a handful of files,
|
|
16
|
+
// and parsing every other file (and the default lib .d.ts files) again was
|
|
17
|
+
// most of the rebuild cost. Reusing a SourceFile object across programs is
|
|
18
|
+
// what the TypeScript language service does too.
|
|
19
|
+
sourceFileCache = new Map();
|
|
14
20
|
compilerOptions;
|
|
15
21
|
host;
|
|
16
|
-
|
|
22
|
+
// Built on first use (or by flushUpdates): the engine stages every file of a
|
|
23
|
+
// scan and flushes once, so an eager build in the constructor was thrown away.
|
|
24
|
+
programInstance;
|
|
17
25
|
rootNames = new Set();
|
|
26
|
+
// compiler options of the nearest tsconfig.json above a folder (the root one for files outside any other)
|
|
27
|
+
optionsByDir = new Map();
|
|
28
|
+
// the options a project starts from before any tsconfig.json is applied
|
|
29
|
+
defaultOptions;
|
|
30
|
+
workspacePackages;
|
|
31
|
+
// one module resolution cache per set of options; only alive while a program is built or a batch runs
|
|
32
|
+
resolutionCaches = new Map();
|
|
33
|
+
// how many builds or batches are running that share the caches (a rebuild can start inside a batch)
|
|
34
|
+
cachingDepth = 0;
|
|
18
35
|
constructor(rootDir) {
|
|
19
36
|
this.rootDir = rootDir;
|
|
20
37
|
const localTsconfig = path_1.default.join(rootDir, "tsconfig.json");
|
|
@@ -35,6 +52,7 @@ class CompilerContext {
|
|
|
35
52
|
allowNonTsExtensions: true,
|
|
36
53
|
incremental: false,
|
|
37
54
|
};
|
|
55
|
+
this.defaultOptions = compilerOptions;
|
|
38
56
|
if (configPath) {
|
|
39
57
|
const config = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
|
|
40
58
|
if (config.error) {
|
|
@@ -45,18 +63,138 @@ class CompilerContext {
|
|
|
45
63
|
...compilerOptions,
|
|
46
64
|
...parsed.options,
|
|
47
65
|
};
|
|
66
|
+
// A solution-style tsconfig (`"files": []` plus `references`, the Vite and Vue template) keeps its
|
|
67
|
+
// `@/*` aliases in a referenced config, so without this every aliased import stays unresolved.
|
|
68
|
+
if (!parsed.options.paths) {
|
|
69
|
+
Object.assign(compilerOptions, this.pathOptionsFromReferences(parsed.projectReferences));
|
|
70
|
+
}
|
|
48
71
|
for (const file of parsed.fileNames) {
|
|
49
72
|
this.rootNames.add(this.normalize(file));
|
|
50
73
|
}
|
|
51
74
|
}
|
|
52
75
|
this.compilerOptions = compilerOptions;
|
|
76
|
+
this.workspacePackages = (0, workspacePackages_1.findWorkspacePackages)(rootDir);
|
|
53
77
|
this.host = typescript_1.default.createCompilerHost(this.compilerOptions, true);
|
|
54
78
|
this.patchCompilerHost();
|
|
55
|
-
this.rebuildProgram();
|
|
56
79
|
}
|
|
57
80
|
normalize(fileName) {
|
|
58
81
|
return this.registry.normalize(fileName);
|
|
59
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Compiler options for resolving the imports of `fromFile`: those of the nearest tsconfig.json above
|
|
85
|
+
* it. A monorepo keeps one per app (`@/*` means a different folder in each) and often none at the root;
|
|
86
|
+
* a single root-level tsconfig.json answers for every file below it, as it always did.
|
|
87
|
+
*/
|
|
88
|
+
optionsFor(fromFile) {
|
|
89
|
+
// an installed package may ship a tsconfig.json of its own: its settings are for building it, not for this project
|
|
90
|
+
if (fromFile.split(/[\\/]/).includes("node_modules"))
|
|
91
|
+
return this.compilerOptions;
|
|
92
|
+
const visited = [];
|
|
93
|
+
let options = this.compilerOptions;
|
|
94
|
+
for (let dir = path_1.default.dirname(fromFile);; dir = path_1.default.dirname(dir)) {
|
|
95
|
+
const cached = this.optionsByDir.get(dir);
|
|
96
|
+
const own = cached ?? this.readDirectoryOptions(dir);
|
|
97
|
+
if (own) {
|
|
98
|
+
options = own;
|
|
99
|
+
visited.push(dir);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
visited.push(dir);
|
|
103
|
+
// a file outside the project, or the top of the filesystem, keeps the root options
|
|
104
|
+
if (path_1.default.relative(this.rootDir, dir).startsWith("..") || path_1.default.dirname(dir) === dir || dir === this.rootDir)
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
for (const dir of visited)
|
|
108
|
+
this.optionsByDir.set(dir, options);
|
|
109
|
+
return options;
|
|
110
|
+
}
|
|
111
|
+
/** The options a folder's own tsconfig.json sets, `undefined` when it has none (the root's are already loaded). */
|
|
112
|
+
readDirectoryOptions(dir) {
|
|
113
|
+
if (dir === this.rootDir)
|
|
114
|
+
return this.compilerOptions;
|
|
115
|
+
const configPath = path_1.default.join(dir, "tsconfig.json");
|
|
116
|
+
if (!fs_1.default.existsSync(configPath))
|
|
117
|
+
return undefined;
|
|
118
|
+
const config = typescript_1.default.readConfigFile(configPath, typescript_1.default.sys.readFile);
|
|
119
|
+
if (config.error)
|
|
120
|
+
return undefined;
|
|
121
|
+
const parsed = typescript_1.default.parseJsonConfigFileContent(config.config, typescript_1.default.sys, dir);
|
|
122
|
+
return {
|
|
123
|
+
...this.defaultOptions,
|
|
124
|
+
...parsed.options,
|
|
125
|
+
...(parsed.options.paths ? {} : this.aliasesWithout(parsed.options, parsed.projectReferences)),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Aliases for a config that sets no `paths` of its own: those of its project references, or else the root's.
|
|
130
|
+
* A tsconfig.json that only adjusts a few options (a `tests/` one, say) does not mean the root's `@/*` stopped
|
|
131
|
+
* being valid there, and the root's aliases applied to every file before configs were read per folder.
|
|
132
|
+
*/
|
|
133
|
+
aliasesWithout(own, references) {
|
|
134
|
+
const fromReferences = this.pathOptionsFromReferences(references);
|
|
135
|
+
if (fromReferences.paths)
|
|
136
|
+
return fromReferences;
|
|
137
|
+
const { paths, baseUrl, pathsBasePath } = this.compilerOptions;
|
|
138
|
+
return { paths, baseUrl: own.baseUrl ?? baseUrl, pathsBasePath };
|
|
139
|
+
}
|
|
140
|
+
cacheFor(options) {
|
|
141
|
+
if (this.cachingDepth === 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
let cache = this.resolutionCaches.get(options);
|
|
144
|
+
if (!cache) {
|
|
145
|
+
cache = typescript_1.default.createModuleResolutionCache(this.rootDir, (fileName) => this.normalize(fileName), options);
|
|
146
|
+
this.resolutionCaches.set(options, cache);
|
|
147
|
+
}
|
|
148
|
+
return cache;
|
|
149
|
+
}
|
|
150
|
+
/** Where a monorepo package's own folder is for an import of it (`@acme/lib`, `@acme/lib/hooks/x`), if it is one. */
|
|
151
|
+
workspaceTargets(moduleName) {
|
|
152
|
+
let best;
|
|
153
|
+
for (const [name, dir] of this.workspacePackages) {
|
|
154
|
+
if ((moduleName === name || moduleName.startsWith(`${name}/`)) && (!best || name.length > best.name.length)) {
|
|
155
|
+
best = { name, dir };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!best)
|
|
159
|
+
return [];
|
|
160
|
+
const sub = moduleName.slice(best.name.length + 1);
|
|
161
|
+
// the package's sources may sit in src/ while its package.json points at a build that is not there
|
|
162
|
+
return [path_1.default.join(best.dir, sub), path_1.default.join(best.dir, "src", sub)];
|
|
163
|
+
}
|
|
164
|
+
resolveImport(moduleName, containingFile, redirectedReference, mode) {
|
|
165
|
+
const options = this.optionsFor(containingFile);
|
|
166
|
+
const cache = this.cacheFor(options);
|
|
167
|
+
const resolved = typescript_1.default.resolveModuleName(moduleName, containingFile, options, this.host, cache, redirectedReference, mode);
|
|
168
|
+
if (resolved.resolvedModule)
|
|
169
|
+
return resolved;
|
|
170
|
+
// not installed, so no node_modules link: a package of this very monorepo is found by its folder
|
|
171
|
+
for (const target of this.workspaceTargets(moduleName)) {
|
|
172
|
+
const viaWorkspace = typescript_1.default.resolveModuleName(target, containingFile, options, this.host, cache, redirectedReference, mode);
|
|
173
|
+
if (viaWorkspace.resolvedModule)
|
|
174
|
+
return viaWorkspace;
|
|
175
|
+
}
|
|
176
|
+
return resolved;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Alias options (`paths`, `baseUrl`) of the first referenced tsconfig that defines `paths`. A
|
|
180
|
+
* reference that is unreadable or has no aliases is skipped; only aliases are taken, never the files.
|
|
181
|
+
*/
|
|
182
|
+
pathOptionsFromReferences(references) {
|
|
183
|
+
for (const reference of references ?? []) {
|
|
184
|
+
const referenced = typescript_1.default.sys.directoryExists(reference.path)
|
|
185
|
+
? path_1.default.join(reference.path, "tsconfig.json")
|
|
186
|
+
: reference.path;
|
|
187
|
+
const config = typescript_1.default.readConfigFile(referenced, typescript_1.default.sys.readFile);
|
|
188
|
+
if (config.error)
|
|
189
|
+
continue;
|
|
190
|
+
const { options } = typescript_1.default.parseJsonConfigFileContent(config.config, typescript_1.default.sys, path_1.default.dirname(referenced));
|
|
191
|
+
if (options.paths) {
|
|
192
|
+
const { paths, baseUrl, pathsBasePath } = options;
|
|
193
|
+
return { paths, baseUrl, pathsBasePath };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {};
|
|
197
|
+
}
|
|
60
198
|
resolveLanguageVersion(languageVersionOrOptions) {
|
|
61
199
|
if (typeof languageVersionOrOptions === "object") {
|
|
62
200
|
return languageVersionOrOptions.languageVersion;
|
|
@@ -75,7 +213,17 @@ class CompilerContext {
|
|
|
75
213
|
if (!snapshot) {
|
|
76
214
|
return undefined;
|
|
77
215
|
}
|
|
78
|
-
const
|
|
216
|
+
const content = snapshot.getText(0, snapshot.getLength());
|
|
217
|
+
const scriptKind = this.getScriptKind(normalized);
|
|
218
|
+
const cached = this.sourceFileCache.get(normalized);
|
|
219
|
+
if (cached &&
|
|
220
|
+
cached.content === content &&
|
|
221
|
+
cached.languageVersion === languageVersion &&
|
|
222
|
+
cached.scriptKind === scriptKind) {
|
|
223
|
+
return this.attachVersion(cached.sourceFile, normalized);
|
|
224
|
+
}
|
|
225
|
+
const sourceFile = typescript_1.default.createSourceFile(normalized, content, languageVersion, true, scriptKind);
|
|
226
|
+
this.sourceFileCache.set(normalized, { content, languageVersion, scriptKind, sourceFile });
|
|
79
227
|
return this.attachVersion(sourceFile, normalized);
|
|
80
228
|
}
|
|
81
229
|
patchCompilerHost() {
|
|
@@ -108,6 +256,7 @@ class CompilerContext {
|
|
|
108
256
|
const normalized = this.normalize(fileName);
|
|
109
257
|
return this.registry.has(normalized) || fs_1.default.existsSync(normalized);
|
|
110
258
|
};
|
|
259
|
+
this.host.resolveModuleNameLiterals = (literals, containingFile, redirectedReference, _options, containingSourceFile) => literals.map((literal) => this.resolveImport(literal.text, containingFile, redirectedReference, typescript_1.default.getModeForUsageLocation(containingSourceFile, literal, this.compilerOptions)));
|
|
111
260
|
this.host.getCanonicalFileName = (f) => this.normalize(f);
|
|
112
261
|
this.host.useCaseSensitiveFileNames = () => true;
|
|
113
262
|
this.host.getCurrentDirectory = () => this.rootDir;
|
|
@@ -127,7 +276,43 @@ class CompilerContext {
|
|
|
127
276
|
return typescript_1.default.ScriptKind.Unknown;
|
|
128
277
|
}
|
|
129
278
|
rebuildProgram() {
|
|
130
|
-
|
|
279
|
+
// options are re-read from disk with every build, so an edited nested tsconfig.json is seen
|
|
280
|
+
this.optionsByDir.clear();
|
|
281
|
+
this.withSharedCaches(() => {
|
|
282
|
+
this.programInstance = typescript_1.default.createProgram([...this.rootNames], this.compilerOptions, this.host);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Imports resolved while `fn` runs share caches, as they do in TypeScript's own host. Files may appear or
|
|
287
|
+
* go between runs, so nothing is kept after the outermost one; a run started inside another (a lazy
|
|
288
|
+
* program build inside a batch) joins it instead of dropping its caches.
|
|
289
|
+
*/
|
|
290
|
+
withSharedCaches(fn) {
|
|
291
|
+
if (this.cachingDepth === 0)
|
|
292
|
+
this.resolutionCaches.clear();
|
|
293
|
+
this.cachingDepth++;
|
|
294
|
+
try {
|
|
295
|
+
return fn();
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
this.cachingDepth--;
|
|
299
|
+
if (this.cachingDepth === 0)
|
|
300
|
+
this.resolutionCaches.clear();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
get program() {
|
|
304
|
+
if (!this.programInstance) {
|
|
305
|
+
this.rebuildProgram();
|
|
306
|
+
}
|
|
307
|
+
return this.programInstance;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Run `fn` with one shared module resolution cache, so resolving the imports
|
|
311
|
+
* of many files in a row (import graph after a flush) does not repeat the
|
|
312
|
+
* same directory lookups. The cache is dropped when `fn` returns.
|
|
313
|
+
*/
|
|
314
|
+
withResolutionCache(fn) {
|
|
315
|
+
return this.withSharedCaches(fn);
|
|
131
316
|
}
|
|
132
317
|
/** Register file content without rebuilding the TS program (batch via {@link flushUpdates}). */
|
|
133
318
|
stageFile(fileName, content) {
|
|
@@ -147,9 +332,65 @@ class CompilerContext {
|
|
|
147
332
|
removeFile(fileName) {
|
|
148
333
|
const normalized = this.normalize(fileName);
|
|
149
334
|
this.registry.remove(normalized);
|
|
335
|
+
this.sourceFileCache.delete(normalized);
|
|
150
336
|
this.rootNames.delete(normalized);
|
|
151
337
|
this.rebuildProgram();
|
|
152
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Re-reads files that are in the program only because something imports them
|
|
341
|
+
* (they are not scanned sources): the disk content replaces whatever the
|
|
342
|
+
* registry cached and the program is rebuilt ONCE for the whole batch. A file
|
|
343
|
+
* that no longer exists is dropped. Returns the files whose content really
|
|
344
|
+
* changed (normalized); none of them ever becomes a root.
|
|
345
|
+
*/
|
|
346
|
+
refreshFromDisk(fileNames) {
|
|
347
|
+
const changed = [];
|
|
348
|
+
for (const fileName of fileNames) {
|
|
349
|
+
const normalized = this.normalize(fileName);
|
|
350
|
+
const before = this.registry.getContent(normalized);
|
|
351
|
+
let content;
|
|
352
|
+
try {
|
|
353
|
+
content = fs_1.default.readFileSync(normalized, "utf8");
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
content = undefined;
|
|
357
|
+
}
|
|
358
|
+
if (before === content)
|
|
359
|
+
continue;
|
|
360
|
+
if (content === undefined) {
|
|
361
|
+
this.registry.remove(normalized);
|
|
362
|
+
this.sourceFileCache.delete(normalized);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
this.registry.update(normalized, content);
|
|
366
|
+
}
|
|
367
|
+
changed.push(normalized);
|
|
368
|
+
}
|
|
369
|
+
if (changed.length === 0) {
|
|
370
|
+
return changed;
|
|
371
|
+
}
|
|
372
|
+
this.rebuildProgram();
|
|
373
|
+
// a file nothing imports must not linger in the registry
|
|
374
|
+
for (const normalized of changed) {
|
|
375
|
+
if (this.registry.has(normalized) && !this.programInstance.getSourceFile(normalized)) {
|
|
376
|
+
this.registry.remove(normalized);
|
|
377
|
+
this.sourceFileCache.delete(normalized);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return changed;
|
|
381
|
+
}
|
|
382
|
+
/** True when the file is part of the current program (scanned or only imported). */
|
|
383
|
+
hasFile(fileName) {
|
|
384
|
+
return this.program.getSourceFile(this.normalize(fileName)) !== undefined;
|
|
385
|
+
}
|
|
386
|
+
/** Project files of the program (no TypeScript libs, no node_modules), declaration files included. */
|
|
387
|
+
getProjectFileNames() {
|
|
388
|
+
const program = this.program;
|
|
389
|
+
return program
|
|
390
|
+
.getSourceFiles()
|
|
391
|
+
.filter((sf) => !program.isSourceFileDefaultLibrary(sf) && !program.isSourceFileFromExternalLibrary(sf))
|
|
392
|
+
.map((sf) => sf.fileName);
|
|
393
|
+
}
|
|
153
394
|
getProgram() {
|
|
154
395
|
return this.program;
|
|
155
396
|
}
|
|
@@ -173,8 +414,7 @@ class CompilerContext {
|
|
|
173
414
|
];
|
|
174
415
|
}
|
|
175
416
|
resolveModule(moduleName, fromFile) {
|
|
176
|
-
|
|
177
|
-
return resolved.resolvedModule?.resolvedFileName;
|
|
417
|
+
return this.resolveImport(moduleName, this.normalize(fromFile)).resolvedModule?.resolvedFileName;
|
|
178
418
|
}
|
|
179
419
|
getResolvedModules(fileName) {
|
|
180
420
|
const source = this.getSourceFile(fileName);
|
|
@@ -9,8 +9,22 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const typescript_1 = __importDefault(require("typescript"));
|
|
10
10
|
class DocumentRegistry {
|
|
11
11
|
documents = new Map();
|
|
12
|
+
// path.resolve + regex ran thousands of times per scan on the same inputs;
|
|
13
|
+
// keyed on cwd because relative inputs resolve against it
|
|
14
|
+
normalizedCache = new Map();
|
|
15
|
+
normalizedCwd = "";
|
|
12
16
|
normalize(fileName) {
|
|
13
|
-
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
if (cwd !== this.normalizedCwd) {
|
|
19
|
+
this.normalizedCache.clear();
|
|
20
|
+
this.normalizedCwd = cwd;
|
|
21
|
+
}
|
|
22
|
+
let normalized = this.normalizedCache.get(fileName);
|
|
23
|
+
if (normalized === undefined) {
|
|
24
|
+
normalized = path_1.default.resolve(fileName).replace(/\\/g, "/");
|
|
25
|
+
this.normalizedCache.set(fileName, normalized);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
14
28
|
}
|
|
15
29
|
has(fileName) {
|
|
16
30
|
return this.documents.has(this.normalize(fileName));
|
|
@@ -7,6 +7,13 @@ class ImportGraph {
|
|
|
7
7
|
imports = new Map();
|
|
8
8
|
// import -> importers
|
|
9
9
|
reverse = new Map();
|
|
10
|
+
hasFile(file) {
|
|
11
|
+
return this.imports.has(file);
|
|
12
|
+
}
|
|
13
|
+
getFiles() {
|
|
14
|
+
return [...this.imports.keys()];
|
|
15
|
+
}
|
|
16
|
+
/** Returns true when the set of files this file imports changed. */
|
|
10
17
|
updateFile(file, nextImports) {
|
|
11
18
|
const previous = this.imports.get(file) ?? new Set();
|
|
12
19
|
const next = new Set(nextImports);
|
|
@@ -36,6 +43,7 @@ class ImportGraph {
|
|
|
36
43
|
importers.add(file);
|
|
37
44
|
}
|
|
38
45
|
this.imports.set(file, next);
|
|
46
|
+
return previous.size !== next.size || [...next].some((imported) => !previous.has(imported));
|
|
39
47
|
}
|
|
40
48
|
removeFile(file) {
|
|
41
49
|
const imports = this.imports.get(file);
|
|
@@ -0,0 +1,58 @@
|
|
|
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.findWorkspacePackages = findWorkspacePackages;
|
|
7
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const projectDependencies_1 = require("../cli/projectDependencies");
|
|
11
|
+
function readJson(file) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs_1.default.readFileSync(file, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The packages of a monorepo (`workspaces` in package.json, or pnpm-workspace.yaml) by name, mapped to
|
|
21
|
+
* their folder. `import "@acme/lib/hooks/x"` then resolves to a folder of the repo itself, so a project
|
|
22
|
+
* whose dependencies are not installed still links its own packages together.
|
|
23
|
+
*/
|
|
24
|
+
function findWorkspacePackages(rootDir) {
|
|
25
|
+
const packages = new Map();
|
|
26
|
+
const rootPackage = readJson(path_1.default.join(rootDir, "package.json"));
|
|
27
|
+
if (!rootPackage)
|
|
28
|
+
return packages;
|
|
29
|
+
const workspaces = rootPackage.workspaces;
|
|
30
|
+
const fromPackageJson = Array.isArray(workspaces)
|
|
31
|
+
? (0, projectDependencies_1.stringList)(workspaces)
|
|
32
|
+
: (0, projectDependencies_1.stringList)(workspaces?.packages);
|
|
33
|
+
let fromPnpm = [];
|
|
34
|
+
try {
|
|
35
|
+
fromPnpm = (0, projectDependencies_1.pnpmWorkspacePatterns)(fs_1.default.readFileSync(path_1.default.join(rootDir, "pnpm-workspace.yaml"), "utf8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// no pnpm-workspace.yaml (or an unreadable one): the package.json patterns are all there is
|
|
39
|
+
}
|
|
40
|
+
const patterns = [...fromPackageJson, ...fromPnpm].filter(projectDependencies_1.staysInsideProject);
|
|
41
|
+
if (patterns.length === 0)
|
|
42
|
+
return packages;
|
|
43
|
+
let members;
|
|
44
|
+
try {
|
|
45
|
+
members = fast_glob_1.default.sync(patterns.map(projectDependencies_1.memberGlob), { cwd: rootDir, ignore: ["**/node_modules/**"] });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// a pattern fast-glob rejects: no members
|
|
49
|
+
return packages;
|
|
50
|
+
}
|
|
51
|
+
for (const member of members.slice(0, projectDependencies_1.MAX_WORKSPACE_PACKAGES)) {
|
|
52
|
+
const name = readJson(path_1.default.join(rootDir, member))?.name;
|
|
53
|
+
if (typeof name === "string" && name !== "" && !packages.has(name)) {
|
|
54
|
+
packages.set(name, path_1.default.dirname(path_1.default.join(rootDir, member)));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return packages;
|
|
58
|
+
}
|
package/dist/config/config.js
CHANGED
|
@@ -3,9 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.LOCAL_CONFIG_NAME = exports.CONFIG_NAME = void 0;
|
|
6
|
+
exports.INIT_EXCLUDE = exports.LOCAL_CONFIG_NAME = exports.CONFIG_NAME = void 0;
|
|
7
7
|
exports.normalizeConfig = normalizeConfig;
|
|
8
8
|
exports.loadConfig = loadConfig;
|
|
9
|
+
exports.describeConfigProblem = describeConfigProblem;
|
|
10
|
+
exports.requireConfig = requireConfig;
|
|
9
11
|
exports.saveConfig = saveConfig;
|
|
10
12
|
exports.resolveApiKey = resolveApiKey;
|
|
11
13
|
exports.saveLocalApiKey = saveLocalApiKey;
|
|
@@ -20,7 +22,22 @@ exports.CONFIG_NAME = "eventra.json";
|
|
|
20
22
|
*/
|
|
21
23
|
exports.LOCAL_CONFIG_NAME = "eventra.local.json";
|
|
22
24
|
const DEFAULT_INCLUDE = ["**/*.{ts,tsx,js,jsx}"];
|
|
25
|
+
// what a config without `sync.exclude` has always skipped; changing it would change the scan of every
|
|
26
|
+
// existing project that never wrote its own list
|
|
23
27
|
const DEFAULT_EXCLUDE = ["node_modules", "dist", ".next", ".git"];
|
|
28
|
+
/**
|
|
29
|
+
* What `eventra init` writes into a new eventra.json: the default above plus folders and files whose
|
|
30
|
+
* code is not the product. Events fired from a test are not features, and on a large repository the tests
|
|
31
|
+
* alone can double the files scanned. Written out, so it is visible and editable. `build` and `out` are
|
|
32
|
+
* left out on purpose: they are as likely to be a source folder as output.
|
|
33
|
+
*/
|
|
34
|
+
exports.INIT_EXCLUDE = [
|
|
35
|
+
...DEFAULT_EXCLUDE,
|
|
36
|
+
"coverage",
|
|
37
|
+
"**/*.{test,spec}.{ts,tsx,js,jsx}",
|
|
38
|
+
"**/__tests__/**",
|
|
39
|
+
"**/__mocks__/**",
|
|
40
|
+
];
|
|
24
41
|
function normalizeSinks(sinks) {
|
|
25
42
|
if (!Array.isArray(sinks))
|
|
26
43
|
return [];
|
|
@@ -42,7 +59,9 @@ function normalizeConfig(config) {
|
|
|
42
59
|
apiKey: config.apiKey ?? "",
|
|
43
60
|
endpoint: config.endpoint ?? "",
|
|
44
61
|
events: [...(config.events ?? [])].sort(),
|
|
45
|
-
|
|
62
|
+
// plain comparison, not localeCompare: the order in the committed file must
|
|
63
|
+
// not depend on the machine's ICU locale
|
|
64
|
+
functionWrappers: uniqueWrappers.sort((a, b) => (a.name < b.name ? -1 : 1)),
|
|
46
65
|
plugins: [...(config.plugins ?? [])],
|
|
47
66
|
...(sinks.length > 0 ? { sinks } : {}),
|
|
48
67
|
sync: {
|
|
@@ -64,9 +83,48 @@ async function loadConfig() {
|
|
|
64
83
|
return null;
|
|
65
84
|
}
|
|
66
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Says why `loadConfig` returned null, for the CLI's error message: a missing
|
|
88
|
+
* file and a malformed one are different problems.
|
|
89
|
+
*/
|
|
90
|
+
async function describeConfigProblem() {
|
|
91
|
+
const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
|
|
92
|
+
if (!(await fs_extra_1.default.pathExists(configPath)))
|
|
93
|
+
return `${exports.CONFIG_NAME} not found`;
|
|
94
|
+
try {
|
|
95
|
+
await fs_extra_1.default.readJSON(configPath);
|
|
96
|
+
return `${exports.CONFIG_NAME} could not be read`;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// readJSON only rejects with an Error (ENOENT was ruled out above, the rest is a parse error)
|
|
100
|
+
return `${exports.CONFIG_NAME} is not valid JSON: ${err.message}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Loads the config for a command that cannot run without it. On failure it
|
|
105
|
+
* prints the reason and sets a non-zero exit code, and returns null, so every
|
|
106
|
+
* command ends the same way when there is no usable config.
|
|
107
|
+
*/
|
|
108
|
+
async function requireConfig() {
|
|
109
|
+
const config = await loadConfig();
|
|
110
|
+
if (config)
|
|
111
|
+
return config;
|
|
112
|
+
console.error(await describeConfigProblem());
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/** Writes eventra.json only when its content actually changed (watch mode re-saves often). */
|
|
67
117
|
async function saveConfig(config) {
|
|
68
118
|
const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
|
|
69
|
-
|
|
119
|
+
const next = `${JSON.stringify(normalizeConfig(config), null, 2)}\n`;
|
|
120
|
+
try {
|
|
121
|
+
if ((await fs_extra_1.default.readFile(configPath, "utf8")) === next)
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// missing or unreadable: write it
|
|
126
|
+
}
|
|
127
|
+
await fs_extra_1.default.writeFile(configPath, next);
|
|
70
128
|
}
|
|
71
129
|
async function readLocalSecrets() {
|
|
72
130
|
const localPath = path_1.default.join(process.cwd(), exports.LOCAL_CONFIG_NAME);
|