@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
package/dist/cli/watch.js
CHANGED
|
@@ -14,141 +14,242 @@ const eventraSdk_1 = require("../analysis/sdk/eventraSdk");
|
|
|
14
14
|
const EventraEngine_1 = require("../core/EventraEngine");
|
|
15
15
|
const scanResults_1 = require("../core/scanResults");
|
|
16
16
|
const loadPlugins_1 = require("../plugin/loadPlugins");
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const scanFilters_1 = require("../core/scanFilters");
|
|
18
|
+
const skipped_1 = require("../core/skipped");
|
|
19
|
+
const fileLimits_1 = require("../core/fileLimits");
|
|
19
20
|
const getVirtualFile_1 = require("../filesystem/getVirtualFile");
|
|
21
|
+
const SDK_TYPES_FILE = "__eventra_sdk_types__.d.ts";
|
|
22
|
+
// Files outside `include` can still change what a scanned file means when
|
|
23
|
+
// something imports them (a shared constants module, a wrapper in a helper).
|
|
24
|
+
// Only code-like files are worth rebuilding the program for.
|
|
25
|
+
const CODE_FILE = /\.(?:[cm]?[jt]s|[jt]sx|json)$/;
|
|
26
|
+
const isNotFound = (err) => err?.code === "ENOENT";
|
|
20
27
|
async function watch() {
|
|
21
|
-
const config = await (0, config_1.
|
|
22
|
-
if (!config)
|
|
23
|
-
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
28
|
+
const config = await (0, config_1.requireConfig)();
|
|
29
|
+
if (!config)
|
|
30
|
+
return undefined;
|
|
26
31
|
console.log(chalk_1.default.blue("Watching...\n"));
|
|
27
32
|
const plugins = await (0, loadPlugins_1.createPluginRegistry)(config);
|
|
28
33
|
const include = [...config.sync.include, ...plugins.getIncludePatterns()];
|
|
29
34
|
const engine = new EventraEngine_1.EventraEngine(process.cwd(), plugins);
|
|
30
|
-
const fileDeps = new Map();
|
|
31
|
-
const cache = new Map();
|
|
32
35
|
const watchedDeps = new Set();
|
|
33
36
|
const trackedFiles = new Set();
|
|
34
37
|
const sourceFiles = new Set();
|
|
38
|
+
// files whose last analysis failed; while there are any, saved results keep
|
|
39
|
+
// the previously known events (see buildConfigFromScan)
|
|
40
|
+
const skippedNow = new Map();
|
|
35
41
|
const normalize = (file) => {
|
|
36
42
|
return path_1.default.resolve(file).replace(/\\/g, "/");
|
|
37
43
|
};
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
// The same rules as the initial scan (and as `sync`): plain exclude names match
|
|
45
|
+
// at any depth, and a changed file is only handled when it matches `include`.
|
|
46
|
+
// The config files are written by this process, so they must never trigger a run.
|
|
47
|
+
const filter = (0, scanFilters_1.createFileFilter)(include, config.sync.exclude, process.cwd(), [
|
|
48
|
+
config_1.CONFIG_NAME,
|
|
49
|
+
config_1.LOCAL_CONFIG_NAME,
|
|
50
|
+
]);
|
|
51
|
+
const isExcludedPath = (file) => filter.isExcluded(normalize(file));
|
|
52
|
+
const isSource = (abs) => sourceFiles.has(abs) || trackedFiles.has(abs) || filter.isIncluded(abs);
|
|
53
|
+
// an imported (or newly importable) file outside `include`
|
|
54
|
+
const isDependencyCandidate = (abs) => CODE_FILE.test(abs) && !isSource(abs);
|
|
55
|
+
const shouldHandle = (abs) => !filter.isExcluded(abs) && (isSource(abs) || isDependencyCandidate(abs));
|
|
56
|
+
// what eventra.json holds now: "keep the known events" while a file is broken
|
|
57
|
+
// must mean the events this process saved last, not the ones at startup
|
|
58
|
+
let saved = config;
|
|
59
|
+
const persist = async () => {
|
|
60
|
+
saved = await (0, scanResults_1.persistScanResults)(saved, engine, { preserveKnown: skippedNow.size > 0 });
|
|
45
61
|
};
|
|
62
|
+
const signature = () => JSON.stringify([engine.getAllEvents(), engine.getAllFunctionWrappers()]);
|
|
46
63
|
const files = await (0, fast_glob_1.default)(include, {
|
|
47
|
-
ignore: config.sync.exclude,
|
|
64
|
+
ignore: (0, scanFilters_1.expandExcludePatterns)(config.sync.exclude),
|
|
48
65
|
absolute: true,
|
|
49
66
|
});
|
|
67
|
+
const ignoredInitial = [];
|
|
50
68
|
engine.beginPreload();
|
|
51
69
|
await engine.preloadFile(SDK_TYPES_FILE, eventraSdk_1.EVENTRA_SDK_SHIM);
|
|
52
70
|
for (const file of files) {
|
|
53
71
|
try {
|
|
54
72
|
const abs = normalize(file);
|
|
55
|
-
sourceFiles.add(abs);
|
|
56
73
|
const raw = await promises_1.default.readFile(abs, "utf-8");
|
|
74
|
+
// the same limit as `sync`: too big to analyse, but not a failure
|
|
75
|
+
if ((0, fileLimits_1.isOversized)(raw)) {
|
|
76
|
+
ignoredInitial.push({ file: abs, reason: fileLimits_1.OVERSIZED_REASON });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
sourceFiles.add(abs);
|
|
57
80
|
const virtualFiles = await plugins.preprocessFile({ fileName: abs, content: raw });
|
|
58
81
|
for (const virtualFile of virtualFiles) {
|
|
59
82
|
const scanPath = normalize(virtualFile.fileName);
|
|
60
|
-
const parsed = await (0, processFile_1.processFile)(scanPath, virtualFile.content);
|
|
61
|
-
cache.set(scanPath, parsed);
|
|
62
83
|
trackedFiles.add(scanPath);
|
|
63
84
|
await engine.preloadFile((0, getVirtualFile_1.getVirtualFile)(scanPath), virtualFile.content);
|
|
64
|
-
fileDeps.set(scanPath, new Set(parsed.dependencies
|
|
65
|
-
.filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
|
|
66
|
-
.map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(scanPath), dep)))));
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
|
-
catch {
|
|
70
|
-
|
|
87
|
+
catch (err) {
|
|
88
|
+
const skip = (0, skipped_1.skippedFrom)(file, err);
|
|
89
|
+
skippedNow.set(normalize(file), skip);
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
92
|
engine.endPreload();
|
|
93
|
+
(0, skipped_1.reportSkipped)([...skippedNow.values()]);
|
|
94
|
+
(0, skipped_1.reportIgnored)(ignoredInitial);
|
|
74
95
|
await engine.runFullAnalysis([SDK_TYPES_FILE, ...trackedFiles].map((f) => (0, getVirtualFile_1.getVirtualFile)(f)), config);
|
|
75
|
-
await (
|
|
96
|
+
await persist();
|
|
76
97
|
const initialEvents = engine.getAllEvents();
|
|
77
98
|
const initialWrappers = engine.getAllFunctionWrappers();
|
|
78
|
-
console.log(chalk_1.default.gray(`Initial: ${initialEvents.length}
|
|
99
|
+
console.log(chalk_1.default.gray(`Initial: ${(0, skipped_1.plural)(initialEvents.length, "event")}, ${(0, skipped_1.plural)(initialWrappers.length, "wrapper")}\n`));
|
|
79
100
|
// Watch the project root (not just the files found by the initial scan) so
|
|
80
|
-
// that brand-new files matching `include` are noticed as they're created
|
|
101
|
+
// that brand-new files matching `include` are noticed as they're created -
|
|
81
102
|
// a fixed file list only ever detects edits to files chokidar already knows
|
|
82
|
-
// about.
|
|
83
|
-
// are unaffected by this and keep working the same way.
|
|
103
|
+
// about. Project files that live outside the root are added below.
|
|
84
104
|
const watcher = chokidar_1.default.watch(process.cwd(), {
|
|
85
105
|
ignoreInitial: true,
|
|
86
106
|
ignored: isExcludedPath,
|
|
87
107
|
});
|
|
108
|
+
const root = normalize(process.cwd());
|
|
109
|
+
const isOutsideRoot = (abs) => abs !== root && !abs.startsWith(`${root}/`);
|
|
110
|
+
// Imported files outside the project root are not covered by the root
|
|
111
|
+
// watcher: follow exactly the ones the program really resolved.
|
|
112
|
+
const syncOutsideDeps = () => {
|
|
113
|
+
const outside = new Set(engine
|
|
114
|
+
.getProjectFileNames()
|
|
115
|
+
.map(normalize)
|
|
116
|
+
.filter((file) => isOutsideRoot(file) && !filter.isExcluded(file) && CODE_FILE.test(file)));
|
|
117
|
+
for (const dep of outside) {
|
|
118
|
+
if (watchedDeps.has(dep))
|
|
119
|
+
continue;
|
|
120
|
+
watcher.add(dep);
|
|
121
|
+
watchedDeps.add(dep);
|
|
122
|
+
}
|
|
123
|
+
for (const dep of [...watchedDeps]) {
|
|
124
|
+
if (outside.has(dep))
|
|
125
|
+
continue;
|
|
126
|
+
watcher.unwatch(dep);
|
|
127
|
+
watchedDeps.delete(dep);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
syncOutsideDeps();
|
|
88
131
|
const queue = new Map();
|
|
89
132
|
let timer = null;
|
|
133
|
+
// a source file is gone (deleted, or no longer analysable such as one that grew past the size limit)
|
|
134
|
+
const dropSource = async (abs) => {
|
|
135
|
+
sourceFiles.delete(abs);
|
|
136
|
+
skippedNow.delete(abs);
|
|
137
|
+
for (const virtualPath of plugins.getVirtualPathsForSource(abs)) {
|
|
138
|
+
trackedFiles.delete(virtualPath);
|
|
139
|
+
await engine.removeFile((0, getVirtualFile_1.getVirtualFile)(virtualPath), config);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
90
142
|
const run = async () => {
|
|
91
143
|
const batch = [...queue.keys()];
|
|
92
144
|
queue.clear();
|
|
145
|
+
if (batch.length === 0)
|
|
146
|
+
return;
|
|
147
|
+
const before = signature();
|
|
148
|
+
let touchedSource = false;
|
|
149
|
+
const skippedThisRun = [];
|
|
150
|
+
const ignoredThisRun = [];
|
|
151
|
+
// imported files outside `include` that changed, appeared or vanished: one rebuild for all of them
|
|
152
|
+
const external = batch.filter((file) => !isSource(file));
|
|
153
|
+
if (external.length > 0) {
|
|
154
|
+
try {
|
|
155
|
+
await engine.refreshExternalFiles(external, config);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.error(chalk_1.default.red(`watch error: ${(0, skipped_1.errorMessage)(err)}`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
93
161
|
for (const file of batch) {
|
|
162
|
+
if (!isSource(file))
|
|
163
|
+
continue;
|
|
164
|
+
touchedSource = true;
|
|
94
165
|
try {
|
|
95
166
|
const raw = await promises_1.default.readFile(file, "utf-8");
|
|
167
|
+
if ((0, fileLimits_1.isOversized)(raw)) {
|
|
168
|
+
// too big to analyse, like `sync`: not a failure, and its events are not kept
|
|
169
|
+
await dropSource(file);
|
|
170
|
+
ignoredThisRun.push({ file, reason: fileLimits_1.OVERSIZED_REASON });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
96
173
|
const virtualFiles = await plugins.preprocessFile({ fileName: file, content: raw });
|
|
174
|
+
sourceFiles.add(file);
|
|
175
|
+
skippedNow.delete(file);
|
|
97
176
|
for (const virtualFile of virtualFiles) {
|
|
98
177
|
const scanPath = normalize(virtualFile.fileName);
|
|
99
|
-
const parsed = await (0, processFile_1.processFile)(scanPath, virtualFile.content);
|
|
100
|
-
cache.set(scanPath, parsed);
|
|
101
178
|
trackedFiles.add(scanPath);
|
|
102
179
|
await engine.updateFile((0, getVirtualFile_1.getVirtualFile)(scanPath), virtualFile.content, config);
|
|
103
|
-
const prevDeps = fileDeps.get(scanPath) ?? new Set();
|
|
104
|
-
const nextDeps = new Set(parsed.dependencies
|
|
105
|
-
.filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
|
|
106
|
-
.map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(scanPath), dep))));
|
|
107
|
-
for (const dep of nextDeps) {
|
|
108
|
-
if (prevDeps.has(dep))
|
|
109
|
-
continue;
|
|
110
|
-
watcher.add(dep);
|
|
111
|
-
watchedDeps.add(dep);
|
|
112
|
-
}
|
|
113
|
-
for (const dep of prevDeps) {
|
|
114
|
-
if (nextDeps.has(dep))
|
|
115
|
-
continue;
|
|
116
|
-
watcher.unwatch(dep);
|
|
117
|
-
watchedDeps.delete(dep);
|
|
118
|
-
}
|
|
119
|
-
fileDeps.set(scanPath, nextDeps);
|
|
120
180
|
}
|
|
121
181
|
}
|
|
122
|
-
catch {
|
|
123
|
-
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (isNotFound(err)) {
|
|
184
|
+
// gone before it could be read (created and deleted within one debounce, or removed mid-run)
|
|
185
|
+
await dropSource(file);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const skip = (0, skipped_1.skippedFrom)(file, err);
|
|
189
|
+
skippedNow.set(file, skip);
|
|
190
|
+
skippedThisRun.push(skip);
|
|
124
191
|
}
|
|
125
192
|
}
|
|
126
|
-
|
|
127
|
-
|
|
193
|
+
(0, skipped_1.reportSkipped)(skippedThisRun);
|
|
194
|
+
(0, skipped_1.reportIgnored)(ignoredThisRun);
|
|
195
|
+
syncOutsideDeps();
|
|
196
|
+
// a batch of imported-only files that changed nothing about the result is not worth a write or a log line
|
|
197
|
+
if (!touchedSource && signature() === before)
|
|
198
|
+
return;
|
|
199
|
+
await persist();
|
|
200
|
+
console.log(chalk_1.default.green(`Updated: ${(0, skipped_1.plural)(engine.getAllEvents().length, "event")}, ${(0, skipped_1.plural)(engine.getAllFunctionWrappers().length, "wrapper")}`));
|
|
201
|
+
};
|
|
202
|
+
// One run at a time: a timer firing while the previous run is still awaiting
|
|
203
|
+
// would otherwise interleave two runs over the same engine.
|
|
204
|
+
let chain = Promise.resolve();
|
|
205
|
+
const enqueue = (task) => {
|
|
206
|
+
chain = chain.then(task).catch((err) => {
|
|
207
|
+
console.error(chalk_1.default.red(`watch error: ${(0, skipped_1.errorMessage)(err)}`));
|
|
208
|
+
});
|
|
128
209
|
};
|
|
129
210
|
const schedule = (file) => {
|
|
130
211
|
const abs = normalize(file);
|
|
212
|
+
if (!shouldHandle(abs))
|
|
213
|
+
return;
|
|
131
214
|
queue.set(abs, true);
|
|
132
215
|
if (timer)
|
|
133
216
|
clearTimeout(timer);
|
|
134
|
-
timer = setTimeout(run, 200);
|
|
217
|
+
timer = setTimeout(() => enqueue(run), 200);
|
|
135
218
|
};
|
|
136
219
|
watcher
|
|
137
220
|
.on("change", schedule)
|
|
138
221
|
.on("add", schedule)
|
|
139
|
-
.on("unlink",
|
|
222
|
+
.on("unlink", (file) => {
|
|
140
223
|
const abs = normalize(file);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
224
|
+
if (filter.isExcluded(abs))
|
|
225
|
+
return;
|
|
226
|
+
// a file that failed to analyse is in no set but is still remembered as skipped
|
|
227
|
+
if (!sourceFiles.has(abs) && !trackedFiles.has(abs) && !skippedNow.has(abs)) {
|
|
228
|
+
// an imported-only file vanished: dependents resolve differently now
|
|
229
|
+
if (CODE_FILE.test(abs) && engine.hasProgramFile(abs)) {
|
|
230
|
+
queue.set(abs, true);
|
|
231
|
+
if (timer)
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
timer = setTimeout(() => enqueue(run), 200);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
146
236
|
}
|
|
147
|
-
|
|
148
|
-
|
|
237
|
+
enqueue(async () => {
|
|
238
|
+
await dropSource(abs);
|
|
239
|
+
await persist();
|
|
240
|
+
console.log(chalk_1.default.red(`Removed -> ${(0, skipped_1.plural)(engine.getAllEvents().length, "event")}, ${(0, skipped_1.plural)(engine.getAllFunctionWrappers().length, "wrapper")}`));
|
|
241
|
+
});
|
|
149
242
|
});
|
|
150
243
|
process.on("SIGINT", async () => {
|
|
151
244
|
await watcher.close();
|
|
152
245
|
process.exit(0);
|
|
153
246
|
});
|
|
247
|
+
return {
|
|
248
|
+
close: async () => {
|
|
249
|
+
if (timer)
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
await watcher.close();
|
|
252
|
+
await chain;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
154
255
|
}
|
|
@@ -11,10 +11,20 @@ const documentRegistry_1 = require("./documentRegistry");
|
|
|
11
11
|
class CompilerContext {
|
|
12
12
|
rootDir;
|
|
13
13
|
registry = new documentRegistry_1.DocumentRegistry();
|
|
14
|
+
// Parsed files survive a program rebuild: a flush changes a handful of files,
|
|
15
|
+
// and parsing every other file (and the default lib .d.ts files) again was
|
|
16
|
+
// most of the rebuild cost. Reusing a SourceFile object across programs is
|
|
17
|
+
// what the TypeScript language service does too.
|
|
18
|
+
sourceFileCache = new Map();
|
|
14
19
|
compilerOptions;
|
|
15
20
|
host;
|
|
16
|
-
|
|
21
|
+
// Built on first use (or by flushUpdates): the engine stages every file of a
|
|
22
|
+
// scan and flushes once, so an eager build in the constructor was thrown away.
|
|
23
|
+
programInstance;
|
|
17
24
|
rootNames = new Set();
|
|
25
|
+
// Only set while withResolutionCache() runs: it memoises fileExists results,
|
|
26
|
+
// so it must never outlive one batch of resolutions against one program.
|
|
27
|
+
activeResolutionCache;
|
|
18
28
|
constructor(rootDir) {
|
|
19
29
|
this.rootDir = rootDir;
|
|
20
30
|
const localTsconfig = path_1.default.join(rootDir, "tsconfig.json");
|
|
@@ -52,7 +62,6 @@ class CompilerContext {
|
|
|
52
62
|
this.compilerOptions = compilerOptions;
|
|
53
63
|
this.host = typescript_1.default.createCompilerHost(this.compilerOptions, true);
|
|
54
64
|
this.patchCompilerHost();
|
|
55
|
-
this.rebuildProgram();
|
|
56
65
|
}
|
|
57
66
|
normalize(fileName) {
|
|
58
67
|
return this.registry.normalize(fileName);
|
|
@@ -75,7 +84,17 @@ class CompilerContext {
|
|
|
75
84
|
if (!snapshot) {
|
|
76
85
|
return undefined;
|
|
77
86
|
}
|
|
78
|
-
const
|
|
87
|
+
const content = snapshot.getText(0, snapshot.getLength());
|
|
88
|
+
const scriptKind = this.getScriptKind(normalized);
|
|
89
|
+
const cached = this.sourceFileCache.get(normalized);
|
|
90
|
+
if (cached &&
|
|
91
|
+
cached.content === content &&
|
|
92
|
+
cached.languageVersion === languageVersion &&
|
|
93
|
+
cached.scriptKind === scriptKind) {
|
|
94
|
+
return this.attachVersion(cached.sourceFile, normalized);
|
|
95
|
+
}
|
|
96
|
+
const sourceFile = typescript_1.default.createSourceFile(normalized, content, languageVersion, true, scriptKind);
|
|
97
|
+
this.sourceFileCache.set(normalized, { content, languageVersion, scriptKind, sourceFile });
|
|
79
98
|
return this.attachVersion(sourceFile, normalized);
|
|
80
99
|
}
|
|
81
100
|
patchCompilerHost() {
|
|
@@ -127,7 +146,30 @@ class CompilerContext {
|
|
|
127
146
|
return typescript_1.default.ScriptKind.Unknown;
|
|
128
147
|
}
|
|
129
148
|
rebuildProgram() {
|
|
130
|
-
this.
|
|
149
|
+
this.programInstance = typescript_1.default.createProgram([...this.rootNames], this.compilerOptions, this.host);
|
|
150
|
+
}
|
|
151
|
+
get program() {
|
|
152
|
+
if (!this.programInstance) {
|
|
153
|
+
this.rebuildProgram();
|
|
154
|
+
}
|
|
155
|
+
return this.programInstance;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Run `fn` with one shared module resolution cache, so resolving the imports
|
|
159
|
+
* of many files in a row (import graph after a flush) does not repeat the
|
|
160
|
+
* same directory lookups. The cache is dropped when `fn` returns.
|
|
161
|
+
*/
|
|
162
|
+
withResolutionCache(fn) {
|
|
163
|
+
if (this.activeResolutionCache) {
|
|
164
|
+
return fn();
|
|
165
|
+
}
|
|
166
|
+
this.activeResolutionCache = typescript_1.default.createModuleResolutionCache(this.rootDir, (fileName) => this.normalize(fileName), this.compilerOptions);
|
|
167
|
+
try {
|
|
168
|
+
return fn();
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
this.activeResolutionCache = undefined;
|
|
172
|
+
}
|
|
131
173
|
}
|
|
132
174
|
/** Register file content without rebuilding the TS program (batch via {@link flushUpdates}). */
|
|
133
175
|
stageFile(fileName, content) {
|
|
@@ -147,9 +189,65 @@ class CompilerContext {
|
|
|
147
189
|
removeFile(fileName) {
|
|
148
190
|
const normalized = this.normalize(fileName);
|
|
149
191
|
this.registry.remove(normalized);
|
|
192
|
+
this.sourceFileCache.delete(normalized);
|
|
150
193
|
this.rootNames.delete(normalized);
|
|
151
194
|
this.rebuildProgram();
|
|
152
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Re-reads files that are in the program only because something imports them
|
|
198
|
+
* (they are not scanned sources): the disk content replaces whatever the
|
|
199
|
+
* registry cached and the program is rebuilt ONCE for the whole batch. A file
|
|
200
|
+
* that no longer exists is dropped. Returns the files whose content really
|
|
201
|
+
* changed (normalized); none of them ever becomes a root.
|
|
202
|
+
*/
|
|
203
|
+
refreshFromDisk(fileNames) {
|
|
204
|
+
const changed = [];
|
|
205
|
+
for (const fileName of fileNames) {
|
|
206
|
+
const normalized = this.normalize(fileName);
|
|
207
|
+
const before = this.registry.getContent(normalized);
|
|
208
|
+
let content;
|
|
209
|
+
try {
|
|
210
|
+
content = fs_1.default.readFileSync(normalized, "utf8");
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
content = undefined;
|
|
214
|
+
}
|
|
215
|
+
if (before === content)
|
|
216
|
+
continue;
|
|
217
|
+
if (content === undefined) {
|
|
218
|
+
this.registry.remove(normalized);
|
|
219
|
+
this.sourceFileCache.delete(normalized);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.registry.update(normalized, content);
|
|
223
|
+
}
|
|
224
|
+
changed.push(normalized);
|
|
225
|
+
}
|
|
226
|
+
if (changed.length === 0) {
|
|
227
|
+
return changed;
|
|
228
|
+
}
|
|
229
|
+
this.rebuildProgram();
|
|
230
|
+
// a file nothing imports must not linger in the registry
|
|
231
|
+
for (const normalized of changed) {
|
|
232
|
+
if (this.registry.has(normalized) && !this.programInstance.getSourceFile(normalized)) {
|
|
233
|
+
this.registry.remove(normalized);
|
|
234
|
+
this.sourceFileCache.delete(normalized);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return changed;
|
|
238
|
+
}
|
|
239
|
+
/** True when the file is part of the current program (scanned or only imported). */
|
|
240
|
+
hasFile(fileName) {
|
|
241
|
+
return this.program.getSourceFile(this.normalize(fileName)) !== undefined;
|
|
242
|
+
}
|
|
243
|
+
/** Project files of the program (no TypeScript libs, no node_modules), declaration files included. */
|
|
244
|
+
getProjectFileNames() {
|
|
245
|
+
const program = this.program;
|
|
246
|
+
return program
|
|
247
|
+
.getSourceFiles()
|
|
248
|
+
.filter((sf) => !program.isSourceFileDefaultLibrary(sf) && !program.isSourceFileFromExternalLibrary(sf))
|
|
249
|
+
.map((sf) => sf.fileName);
|
|
250
|
+
}
|
|
153
251
|
getProgram() {
|
|
154
252
|
return this.program;
|
|
155
253
|
}
|
|
@@ -173,7 +271,7 @@ class CompilerContext {
|
|
|
173
271
|
];
|
|
174
272
|
}
|
|
175
273
|
resolveModule(moduleName, fromFile) {
|
|
176
|
-
const resolved = typescript_1.default.resolveModuleName(moduleName, this.normalize(fromFile), this.compilerOptions, this.host);
|
|
274
|
+
const resolved = typescript_1.default.resolveModuleName(moduleName, this.normalize(fromFile), this.compilerOptions, this.host, this.activeResolutionCache);
|
|
177
275
|
return resolved.resolvedModule?.resolvedFileName;
|
|
178
276
|
}
|
|
179
277
|
getResolvedModules(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);
|
package/dist/config/config.js
CHANGED
|
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
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;
|
|
@@ -42,7 +44,9 @@ function normalizeConfig(config) {
|
|
|
42
44
|
apiKey: config.apiKey ?? "",
|
|
43
45
|
endpoint: config.endpoint ?? "",
|
|
44
46
|
events: [...(config.events ?? [])].sort(),
|
|
45
|
-
|
|
47
|
+
// plain comparison, not localeCompare: the order in the committed file must
|
|
48
|
+
// not depend on the machine's ICU locale
|
|
49
|
+
functionWrappers: uniqueWrappers.sort((a, b) => (a.name < b.name ? -1 : 1)),
|
|
46
50
|
plugins: [...(config.plugins ?? [])],
|
|
47
51
|
...(sinks.length > 0 ? { sinks } : {}),
|
|
48
52
|
sync: {
|
|
@@ -64,9 +68,48 @@ async function loadConfig() {
|
|
|
64
68
|
return null;
|
|
65
69
|
}
|
|
66
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Says why `loadConfig` returned null, for the CLI's error message: a missing
|
|
73
|
+
* file and a malformed one are different problems.
|
|
74
|
+
*/
|
|
75
|
+
async function describeConfigProblem() {
|
|
76
|
+
const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
|
|
77
|
+
if (!(await fs_extra_1.default.pathExists(configPath)))
|
|
78
|
+
return `${exports.CONFIG_NAME} not found`;
|
|
79
|
+
try {
|
|
80
|
+
await fs_extra_1.default.readJSON(configPath);
|
|
81
|
+
return `${exports.CONFIG_NAME} could not be read`;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// readJSON only rejects with an Error (ENOENT was ruled out above, the rest is a parse error)
|
|
85
|
+
return `${exports.CONFIG_NAME} is not valid JSON: ${err.message}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Loads the config for a command that cannot run without it. On failure it
|
|
90
|
+
* prints the reason and sets a non-zero exit code, and returns null, so every
|
|
91
|
+
* command ends the same way when there is no usable config.
|
|
92
|
+
*/
|
|
93
|
+
async function requireConfig() {
|
|
94
|
+
const config = await loadConfig();
|
|
95
|
+
if (config)
|
|
96
|
+
return config;
|
|
97
|
+
console.error(await describeConfigProblem());
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** Writes eventra.json only when its content actually changed (watch mode re-saves often). */
|
|
67
102
|
async function saveConfig(config) {
|
|
68
103
|
const configPath = path_1.default.join(process.cwd(), exports.CONFIG_NAME);
|
|
69
|
-
|
|
104
|
+
const next = `${JSON.stringify(normalizeConfig(config), null, 2)}\n`;
|
|
105
|
+
try {
|
|
106
|
+
if ((await fs_extra_1.default.readFile(configPath, "utf8")) === next)
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// missing or unreadable: write it
|
|
111
|
+
}
|
|
112
|
+
await fs_extra_1.default.writeFile(configPath, next);
|
|
70
113
|
}
|
|
71
114
|
async function readLocalSecrets() {
|
|
72
115
|
const localPath = path_1.default.join(process.cwd(), exports.LOCAL_CONFIG_NAME);
|