@laurence79/wireit 0.14.13-shared-cache.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/LICENSE +202 -0
- package/README.md +1062 -0
- package/bin/wireit.js +9 -0
- package/lib/analyzer.js +1600 -0
- package/lib/caching/cache.js +7 -0
- package/lib/caching/github-actions-cache.js +832 -0
- package/lib/caching/local-cache.js +78 -0
- package/lib/caching/shared-cache.js +256 -0
- package/lib/cli-options.js +495 -0
- package/lib/cli.js +177 -0
- package/lib/config.js +18 -0
- package/lib/error.js +160 -0
- package/lib/event.js +7 -0
- package/lib/execution/base.js +108 -0
- package/lib/execution/no-command.js +32 -0
- package/lib/execution/service.js +1017 -0
- package/lib/execution/standard.js +683 -0
- package/lib/executor.js +249 -0
- package/lib/fingerprint.js +164 -0
- package/lib/ide.js +583 -0
- package/lib/language-server.js +135 -0
- package/lib/logging/combination-logger.js +41 -0
- package/lib/logging/debug-logger.js +43 -0
- package/lib/logging/logger.js +38 -0
- package/lib/logging/metrics-logger.js +108 -0
- package/lib/logging/quiet/run-tracker.js +597 -0
- package/lib/logging/quiet/stack-map.js +41 -0
- package/lib/logging/quiet/writeover-line.js +197 -0
- package/lib/logging/quiet-logger.js +78 -0
- package/lib/logging/simple-logger.js +296 -0
- package/lib/logging/watch-logger.js +81 -0
- package/lib/script-child-process.js +270 -0
- package/lib/util/ast.js +71 -0
- package/lib/util/async-cache.js +24 -0
- package/lib/util/copy.js +120 -0
- package/lib/util/deferred.js +35 -0
- package/lib/util/delete.js +120 -0
- package/lib/util/dispose.js +16 -0
- package/lib/util/fs.js +258 -0
- package/lib/util/glob.js +255 -0
- package/lib/util/line-monitor.js +69 -0
- package/lib/util/manifest.js +31 -0
- package/lib/util/optimize-mkdirs.js +55 -0
- package/lib/util/package-json-reader.js +61 -0
- package/lib/util/package-json.js +179 -0
- package/lib/util/script-data-dir.js +19 -0
- package/lib/util/shuffle.js +16 -0
- package/lib/util/unreachable.js +12 -0
- package/lib/util/windows.js +87 -0
- package/lib/util/worker-pool.js +61 -0
- package/lib/watcher.js +396 -0
- package/package.json +470 -0
- package/schema.json +132 -0
- package/wireit.svg +1 -0
package/lib/watcher.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import chokidar from 'chokidar';
|
|
7
|
+
import { Analyzer } from './analyzer.js';
|
|
8
|
+
import { scriptReferenceToString, } from './config.js';
|
|
9
|
+
import { Executor } from './executor.js';
|
|
10
|
+
import { Deferred } from './util/deferred.js';
|
|
11
|
+
import './util/dispose.js';
|
|
12
|
+
function unknownState(state) {
|
|
13
|
+
return new Error(`Unknown watcher state ${String(state)}`);
|
|
14
|
+
}
|
|
15
|
+
function unexpectedState(state) {
|
|
16
|
+
return new Error(`Unexpected watcher state ${state}`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The minimum time that must elapse after the last file change was detected
|
|
20
|
+
* before we begin a new run. Also the minimum time between successive runs.
|
|
21
|
+
*
|
|
22
|
+
* Note even 0 is a useful value here, because that defers new runs to the next
|
|
23
|
+
* JS task. This is important because if multiple scripts are watching the same
|
|
24
|
+
* file that changed, we get a file watcher event for each of them. Without
|
|
25
|
+
* debouncing, a second run will be immediately queued after the first event
|
|
26
|
+
* starts the run.
|
|
27
|
+
*/
|
|
28
|
+
const DEBOUNCE_MS = 0;
|
|
29
|
+
/**
|
|
30
|
+
* Watches a script for changes in its input files, and in the input files of
|
|
31
|
+
* its transitive dependencies, and executes all affected scripts when they
|
|
32
|
+
* change.
|
|
33
|
+
*
|
|
34
|
+
* Also watches all related package.json files and reloads script configuration
|
|
35
|
+
* when they change.
|
|
36
|
+
*/
|
|
37
|
+
export class Watcher {
|
|
38
|
+
/** See {@link WatcherState} */
|
|
39
|
+
#state = 'initial';
|
|
40
|
+
#rootScript;
|
|
41
|
+
#extraArgs;
|
|
42
|
+
#logger;
|
|
43
|
+
#workerPool;
|
|
44
|
+
#cache;
|
|
45
|
+
#failureMode;
|
|
46
|
+
#agent;
|
|
47
|
+
#executor;
|
|
48
|
+
#debounceTimeoutId = undefined;
|
|
49
|
+
#previousIterationServices = undefined;
|
|
50
|
+
#previousIterationFailures = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* The most recent analysis of the root script. As soon as we detect it might
|
|
53
|
+
* be stale because a package.json file was modified, this becomes undefined
|
|
54
|
+
* again.
|
|
55
|
+
*/
|
|
56
|
+
#latestRootScriptConfig;
|
|
57
|
+
/**
|
|
58
|
+
* The file watcher for all package.json files relevant to this build graph.
|
|
59
|
+
*/
|
|
60
|
+
#configFilesWatcher;
|
|
61
|
+
/**
|
|
62
|
+
* File watchers for the input files of all scripts in this build graph.
|
|
63
|
+
*/
|
|
64
|
+
#inputFileWatchers = new Map();
|
|
65
|
+
/**
|
|
66
|
+
* Resolves when this watcher has been aborted and the last run finished.
|
|
67
|
+
*/
|
|
68
|
+
#finished = new Deferred();
|
|
69
|
+
#watchOptions;
|
|
70
|
+
constructor(rootScript, extraArgs, logger, workerPool, cache, failureMode, agent, watchOptions) {
|
|
71
|
+
this.#rootScript = rootScript;
|
|
72
|
+
this.#extraArgs = extraArgs;
|
|
73
|
+
this.#logger = logger.getWatchLogger?.() ?? logger;
|
|
74
|
+
this.#workerPool = workerPool;
|
|
75
|
+
this.#failureMode = failureMode;
|
|
76
|
+
this.#cache = cache;
|
|
77
|
+
this.#agent = agent;
|
|
78
|
+
this.#watchOptions = watchOptions;
|
|
79
|
+
}
|
|
80
|
+
watch() {
|
|
81
|
+
this.#startRun();
|
|
82
|
+
return this.#finished.promise;
|
|
83
|
+
}
|
|
84
|
+
#startDebounce() {
|
|
85
|
+
if (this.#debounceTimeoutId !== undefined) {
|
|
86
|
+
throw new Error('Expected #debounceTimeoutId to be undefined');
|
|
87
|
+
}
|
|
88
|
+
this.#debounceTimeoutId = setTimeout(() => {
|
|
89
|
+
this.#onDebounced();
|
|
90
|
+
}, DEBOUNCE_MS);
|
|
91
|
+
}
|
|
92
|
+
#cancelDebounce() {
|
|
93
|
+
clearTimeout(this.#debounceTimeoutId);
|
|
94
|
+
this.#debounceTimeoutId = undefined;
|
|
95
|
+
}
|
|
96
|
+
#onDebounced() {
|
|
97
|
+
switch (this.#state) {
|
|
98
|
+
case 'debouncing': {
|
|
99
|
+
this.#debounceTimeoutId = undefined;
|
|
100
|
+
this.#startRun();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
case 'initial':
|
|
104
|
+
case 'watching':
|
|
105
|
+
case 'queued':
|
|
106
|
+
case 'running':
|
|
107
|
+
case 'aborted': {
|
|
108
|
+
throw unexpectedState(this.#state);
|
|
109
|
+
}
|
|
110
|
+
default: {
|
|
111
|
+
throw unknownState(this.#state);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
#startRun() {
|
|
116
|
+
switch (this.#state) {
|
|
117
|
+
case 'initial':
|
|
118
|
+
case 'debouncing': {
|
|
119
|
+
this.#state = 'running';
|
|
120
|
+
this.#logger.log({
|
|
121
|
+
script: this.#rootScript,
|
|
122
|
+
type: 'info',
|
|
123
|
+
detail: 'watch-run-start',
|
|
124
|
+
});
|
|
125
|
+
if (this.#latestRootScriptConfig === undefined) {
|
|
126
|
+
void this.#analyze();
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// We already have a valid config, so we can skip the analysis step.
|
|
130
|
+
// but if the logger needs to know about the analysis, this lets
|
|
131
|
+
// it know.
|
|
132
|
+
this.#logger.log({
|
|
133
|
+
type: 'info',
|
|
134
|
+
detail: 'analysis-completed',
|
|
135
|
+
script: this.#rootScript,
|
|
136
|
+
rootScriptConfig: this.#latestRootScriptConfig,
|
|
137
|
+
});
|
|
138
|
+
void this.#execute(this.#latestRootScriptConfig);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
case 'watching':
|
|
143
|
+
case 'queued':
|
|
144
|
+
case 'running':
|
|
145
|
+
case 'aborted': {
|
|
146
|
+
throw unexpectedState(this.#state);
|
|
147
|
+
}
|
|
148
|
+
default: {
|
|
149
|
+
throw unknownState(this.#state);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async #analyze() {
|
|
154
|
+
if (this.#state !== 'running') {
|
|
155
|
+
throw unexpectedState(this.#state);
|
|
156
|
+
}
|
|
157
|
+
const analyzer = new Analyzer(this.#agent, this.#logger);
|
|
158
|
+
const result = await analyzer.analyze(this.#rootScript, this.#extraArgs);
|
|
159
|
+
if (this.#state === 'aborted') {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Set up watchers for all relevant config files even if there were errors
|
|
163
|
+
// so that we'll try again when the user modifies a config file.
|
|
164
|
+
const configFiles = [...result.relevantConfigFilePaths];
|
|
165
|
+
// Order doesn't matter because we know we don't have any !negated patterns,
|
|
166
|
+
// but we're going to compare arrays exactly so the order should be
|
|
167
|
+
// deterministic.
|
|
168
|
+
configFiles.sort();
|
|
169
|
+
const oldWatcher = this.#configFilesWatcher;
|
|
170
|
+
if (!watchPathsEqual(configFiles, oldWatcher?.patterns)) {
|
|
171
|
+
this.#configFilesWatcher = makeWatcher(configFiles, '/', this.#onConfigFileChanged, true, this.#watchOptions);
|
|
172
|
+
if (oldWatcher !== undefined) {
|
|
173
|
+
void oldWatcher[Symbol.asyncDispose]();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!result.config.ok) {
|
|
177
|
+
for (const error of result.config.error) {
|
|
178
|
+
this.#logger.log(error);
|
|
179
|
+
}
|
|
180
|
+
this.#onRunDone();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.#latestRootScriptConfig = result.config.value;
|
|
184
|
+
this.#synchronizeInputFileWatchers(this.#latestRootScriptConfig);
|
|
185
|
+
void this.#execute(this.#latestRootScriptConfig);
|
|
186
|
+
}
|
|
187
|
+
async #execute(script) {
|
|
188
|
+
if (this.#state !== 'running') {
|
|
189
|
+
throw unexpectedState(this.#state);
|
|
190
|
+
}
|
|
191
|
+
this.#executor = new Executor(script, this.#logger, this.#workerPool, this.#cache, this.#failureMode, this.#previousIterationServices, true, this.#previousIterationFailures);
|
|
192
|
+
const result = await this.#executor.execute();
|
|
193
|
+
this.#previousIterationServices = result.persistentServices;
|
|
194
|
+
if (result.errors.length > 0) {
|
|
195
|
+
for (const error of result.errors) {
|
|
196
|
+
this.#logger.log(error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.#previousIterationFailures = new Map();
|
|
201
|
+
}
|
|
202
|
+
this.#onRunDone();
|
|
203
|
+
}
|
|
204
|
+
#onRunDone() {
|
|
205
|
+
this.#logger.log({
|
|
206
|
+
script: this.#rootScript,
|
|
207
|
+
type: 'info',
|
|
208
|
+
detail: 'watch-run-end',
|
|
209
|
+
});
|
|
210
|
+
switch (this.#state) {
|
|
211
|
+
case 'queued': {
|
|
212
|
+
// Note that the debounce time could actually have already elapsed since
|
|
213
|
+
// the last file change while we were running, but we don't start the
|
|
214
|
+
// debounce timer until the run finishes. This means that the debounce
|
|
215
|
+
// interval is also the minimum time between successive runs. This seems
|
|
216
|
+
// fine and probably good, and is simpler than maintaining a separate
|
|
217
|
+
// "queued-debouncing" state.
|
|
218
|
+
this.#state = 'debouncing';
|
|
219
|
+
this.#startDebounce();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
case 'running': {
|
|
223
|
+
this.#state = 'watching';
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
case 'aborted': {
|
|
227
|
+
this.#finished.resolve();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
case 'initial':
|
|
231
|
+
case 'watching':
|
|
232
|
+
case 'debouncing': {
|
|
233
|
+
throw unexpectedState(this.#state);
|
|
234
|
+
}
|
|
235
|
+
default: {
|
|
236
|
+
throw unknownState(this.#state);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
#onConfigFileChanged = () => {
|
|
241
|
+
this.#latestRootScriptConfig = undefined;
|
|
242
|
+
this.#fileChanged();
|
|
243
|
+
};
|
|
244
|
+
#fileChanged = () => {
|
|
245
|
+
switch (this.#state) {
|
|
246
|
+
case 'watching': {
|
|
247
|
+
this.#state = 'debouncing';
|
|
248
|
+
this.#startDebounce();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
case 'debouncing': {
|
|
252
|
+
this.#cancelDebounce();
|
|
253
|
+
this.#startDebounce();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case 'running': {
|
|
257
|
+
this.#state = 'queued';
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
case 'queued':
|
|
261
|
+
case 'aborted': {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case 'initial': {
|
|
265
|
+
throw unexpectedState(this.#state);
|
|
266
|
+
}
|
|
267
|
+
default: {
|
|
268
|
+
throw unknownState(this.#state);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
#synchronizeInputFileWatchers(root) {
|
|
273
|
+
const visited = new Set();
|
|
274
|
+
const visit = (script) => {
|
|
275
|
+
const key = scriptReferenceToString(script);
|
|
276
|
+
if (visited.has(key)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
visited.add(key);
|
|
280
|
+
const newInputFiles = script.files?.values;
|
|
281
|
+
const oldWatcher = this.#inputFileWatchers.get(key);
|
|
282
|
+
if (!watchPathsEqual(newInputFiles, oldWatcher?.patterns)) {
|
|
283
|
+
if (newInputFiles === undefined || newInputFiles.length === 0) {
|
|
284
|
+
this.#inputFileWatchers.delete(key);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const newWatcher = makeWatcher(newInputFiles, script.packageDir, this.#fileChanged, true, this.#watchOptions);
|
|
288
|
+
this.#inputFileWatchers.set(key, newWatcher);
|
|
289
|
+
}
|
|
290
|
+
if (oldWatcher !== undefined) {
|
|
291
|
+
void oldWatcher[Symbol.asyncDispose]();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const dep of script.dependencies) {
|
|
295
|
+
visit(dep.config);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
visit(root);
|
|
299
|
+
// There also could be some scripts that have been removed entirely.
|
|
300
|
+
for (const [oldKey, oldWatcher] of this.#inputFileWatchers) {
|
|
301
|
+
if (!visited.has(oldKey)) {
|
|
302
|
+
void oldWatcher[Symbol.asyncDispose]();
|
|
303
|
+
this.#inputFileWatchers.delete(oldKey);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
abort() {
|
|
308
|
+
if (this.#executor !== undefined) {
|
|
309
|
+
this.#executor.abort();
|
|
310
|
+
this.#executor = undefined;
|
|
311
|
+
}
|
|
312
|
+
switch (this.#state) {
|
|
313
|
+
case 'debouncing':
|
|
314
|
+
case 'watching': {
|
|
315
|
+
if (this.#state === 'debouncing') {
|
|
316
|
+
this.#cancelDebounce();
|
|
317
|
+
}
|
|
318
|
+
this.#state = 'aborted';
|
|
319
|
+
this.#closeAllFileWatchers();
|
|
320
|
+
this.#finished.resolve();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
case 'running':
|
|
324
|
+
case 'queued': {
|
|
325
|
+
this.#state = 'aborted';
|
|
326
|
+
this.#closeAllFileWatchers();
|
|
327
|
+
// Don't resolve #finished immediately so that we will wait for #analyze
|
|
328
|
+
// or #execute to finish.
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
case 'aborted': {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
case 'initial': {
|
|
335
|
+
throw unexpectedState(this.#state);
|
|
336
|
+
}
|
|
337
|
+
default: {
|
|
338
|
+
throw unknownState(this.#state);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
#closeAllFileWatchers() {
|
|
343
|
+
void this.#configFilesWatcher?.[Symbol.asyncDispose]();
|
|
344
|
+
for (const value of this.#inputFileWatchers.values()) {
|
|
345
|
+
void value[Symbol.asyncDispose]();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const watchPathsEqual = (a, b) => {
|
|
350
|
+
if (a === undefined && b === undefined) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
if (a === undefined || b === undefined) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (a.length !== b.length) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
for (let i = 0; i < a.length; i++) {
|
|
360
|
+
if (a[i] !== b[i]) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Exported for testing.
|
|
368
|
+
*
|
|
369
|
+
* @param ignoreInitial Ignore the initial "add" events emitted when chokidar
|
|
370
|
+
* first discovers each file. We already do an initial run, so these events are
|
|
371
|
+
* just noise that may trigger an unnecessary second run.
|
|
372
|
+
* https://github.com/paulmillr/chokidar#path-filtering
|
|
373
|
+
*/
|
|
374
|
+
export const makeWatcher = (patterns, cwd, callback, ignoreInitial, watchOptions) => {
|
|
375
|
+
// TODO(aomarks) chokidar doesn't work exactly like fast-glob, so there are
|
|
376
|
+
// currently various differences in what gets watched vs what actually affects
|
|
377
|
+
// the build. See https://github.com/google/wireit/issues/550.
|
|
378
|
+
const watcher = chokidar.watch(
|
|
379
|
+
// Trim leading slashes from patterns, to "re-root" all paths to the package
|
|
380
|
+
// directory, just as we do when globbing for script execution.
|
|
381
|
+
patterns.map((pattern) => pattern.replace(/^\/+/, '')), {
|
|
382
|
+
cwd,
|
|
383
|
+
ignoreInitial,
|
|
384
|
+
usePolling: watchOptions.strategy === 'poll',
|
|
385
|
+
interval: watchOptions.strategy === 'poll' ? watchOptions.interval : undefined,
|
|
386
|
+
});
|
|
387
|
+
watcher.on('all', callback);
|
|
388
|
+
return {
|
|
389
|
+
patterns,
|
|
390
|
+
watcher,
|
|
391
|
+
async [Symbol.asyncDispose]() {
|
|
392
|
+
await watcher.close();
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
//# sourceMappingURL=watcher.js.map
|