@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.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +1062 -0
  3. package/bin/wireit.js +9 -0
  4. package/lib/analyzer.js +1600 -0
  5. package/lib/caching/cache.js +7 -0
  6. package/lib/caching/github-actions-cache.js +832 -0
  7. package/lib/caching/local-cache.js +78 -0
  8. package/lib/caching/shared-cache.js +256 -0
  9. package/lib/cli-options.js +495 -0
  10. package/lib/cli.js +177 -0
  11. package/lib/config.js +18 -0
  12. package/lib/error.js +160 -0
  13. package/lib/event.js +7 -0
  14. package/lib/execution/base.js +108 -0
  15. package/lib/execution/no-command.js +32 -0
  16. package/lib/execution/service.js +1017 -0
  17. package/lib/execution/standard.js +683 -0
  18. package/lib/executor.js +249 -0
  19. package/lib/fingerprint.js +164 -0
  20. package/lib/ide.js +583 -0
  21. package/lib/language-server.js +135 -0
  22. package/lib/logging/combination-logger.js +41 -0
  23. package/lib/logging/debug-logger.js +43 -0
  24. package/lib/logging/logger.js +38 -0
  25. package/lib/logging/metrics-logger.js +108 -0
  26. package/lib/logging/quiet/run-tracker.js +597 -0
  27. package/lib/logging/quiet/stack-map.js +41 -0
  28. package/lib/logging/quiet/writeover-line.js +197 -0
  29. package/lib/logging/quiet-logger.js +78 -0
  30. package/lib/logging/simple-logger.js +296 -0
  31. package/lib/logging/watch-logger.js +81 -0
  32. package/lib/script-child-process.js +270 -0
  33. package/lib/util/ast.js +71 -0
  34. package/lib/util/async-cache.js +24 -0
  35. package/lib/util/copy.js +120 -0
  36. package/lib/util/deferred.js +35 -0
  37. package/lib/util/delete.js +120 -0
  38. package/lib/util/dispose.js +16 -0
  39. package/lib/util/fs.js +258 -0
  40. package/lib/util/glob.js +255 -0
  41. package/lib/util/line-monitor.js +69 -0
  42. package/lib/util/manifest.js +31 -0
  43. package/lib/util/optimize-mkdirs.js +55 -0
  44. package/lib/util/package-json-reader.js +61 -0
  45. package/lib/util/package-json.js +179 -0
  46. package/lib/util/script-data-dir.js +19 -0
  47. package/lib/util/shuffle.js +16 -0
  48. package/lib/util/unreachable.js +12 -0
  49. package/lib/util/windows.js +87 -0
  50. package/lib/util/worker-pool.js +61 -0
  51. package/lib/watcher.js +396 -0
  52. package/package.json +470 -0
  53. package/schema.json +132 -0
  54. 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