@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
@@ -0,0 +1,683 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as pathlib from 'path';
7
+ import * as fs from '../util/fs.js';
8
+ import { getScriptDataDir } from '../util/script-data-dir.js';
9
+ import { unreachable } from '../util/unreachable.js';
10
+ import { glob, GlobOutsideCwdError } from '../util/glob.js';
11
+ import { deleteEntries } from '../util/delete.js';
12
+ import lockfile from 'proper-lockfile';
13
+ import { ScriptChildProcess } from '../script-child-process.js';
14
+ import { BaseExecutionWithCommand } from './base.js';
15
+ import { Fingerprint } from '../fingerprint.js';
16
+ import { computeManifestEntry } from '../util/manifest.js';
17
+ /**
18
+ * Execution for a {@link StandardScriptConfig}.
19
+ */
20
+ export class StandardScriptExecution extends BaseExecutionWithCommand {
21
+ #state = 'before-running';
22
+ #cache;
23
+ #workerPool;
24
+ constructor(config, executor, workerPool, cache, logger) {
25
+ super(config, executor, logger);
26
+ this.#workerPool = workerPool;
27
+ this.#cache = cache;
28
+ }
29
+ #ensureState(state) {
30
+ if (this.#state !== state) {
31
+ throw new Error(`Expected state ${state} but was ${this.#state}`);
32
+ }
33
+ }
34
+ async _execute() {
35
+ try {
36
+ this.#ensureState('before-running');
37
+ const dependencyFingerprints = await this._executeDependencies();
38
+ if (!dependencyFingerprints.ok) {
39
+ dependencyFingerprints.error.push(this.#startCancelledEvent);
40
+ return dependencyFingerprints;
41
+ }
42
+ // Significant time could have elapsed since we last checked because our
43
+ // dependencies had to finish.
44
+ if (this.#shouldNotStart) {
45
+ return { ok: false, error: [this.#startCancelledEvent] };
46
+ }
47
+ return await this.#acquireSystemLockIfNeeded(async () => {
48
+ // Note we must wait for dependencies to finish before generating the
49
+ // cache key, because a dependency could create or modify an input file to
50
+ // this script, which would affect the key.
51
+ const fingerprintResponse = await Fingerprint.compute(this._config, dependencyFingerprints.value);
52
+ if (!fingerprintResponse.ok) {
53
+ return {
54
+ ok: false,
55
+ error: [fingerprintResponse.error],
56
+ };
57
+ }
58
+ const fingerprint = fingerprintResponse.value;
59
+ if (this._executor.failedInPreviousWatchIteration(this._config, fingerprint)) {
60
+ return {
61
+ ok: false,
62
+ error: [
63
+ {
64
+ script: this._config,
65
+ type: 'failure',
66
+ reason: 'failed-previous-watch-iteration',
67
+ },
68
+ ],
69
+ };
70
+ }
71
+ if (await this.#fingerprintIsFresh(fingerprint)) {
72
+ const manifestFresh = await this.#outputManifestIsFresh();
73
+ if (!manifestFresh.ok) {
74
+ return { ok: false, error: [manifestFresh.error] };
75
+ }
76
+ if (manifestFresh.value) {
77
+ return this.#handleFresh(fingerprint);
78
+ }
79
+ }
80
+ // Computing the fingerprint can take some time, and the next operation is
81
+ // destructive. Another good opportunity to check if we should still
82
+ // start.
83
+ if (this.#shouldNotStart) {
84
+ return { ok: false, error: [this.#startCancelledEvent] };
85
+ }
86
+ const cacheHit = fingerprint.data.fullyTracked
87
+ ? await this.#cache?.get(this._config, fingerprint)
88
+ : undefined;
89
+ if (this.#shouldNotStart) {
90
+ return { ok: false, error: [this.#startCancelledEvent] };
91
+ }
92
+ if (cacheHit !== undefined) {
93
+ return this.#handleCacheHit(cacheHit, fingerprint);
94
+ }
95
+ return this.#handleNeedsRun(fingerprint);
96
+ });
97
+ }
98
+ finally {
99
+ this._servicesNotNeeded.resolve();
100
+ }
101
+ }
102
+ /**
103
+ * Whether we should return early instead of starting this script.
104
+ *
105
+ * We should check this as the first thing we do, and then after any
106
+ * significant amount of time might have elapsed.
107
+ */
108
+ get #shouldNotStart() {
109
+ return this._executor.shouldStopStartingNewScripts;
110
+ }
111
+ /**
112
+ * Convenience to generate a cancellation failure event for this script.
113
+ */
114
+ get #startCancelledEvent() {
115
+ return {
116
+ script: this._config,
117
+ type: 'failure',
118
+ reason: 'start-cancelled',
119
+ };
120
+ }
121
+ /**
122
+ * Acquire a system-wide lock on the execution of this script, if the script
123
+ * has any output files that require it.
124
+ */
125
+ async #acquireSystemLockIfNeeded(workFn) {
126
+ if (this._config.output?.values.length === 0) {
127
+ return workFn();
128
+ }
129
+ // The proper-lockfile library is designed to give an exclusive lock for a
130
+ // *file*. That's slightly misaligned with our use-case, because there's no
131
+ // particular file we need a lock for -- our lock is for the execution of
132
+ // this script.
133
+ //
134
+ // We can still use the library, we just need to pick some arbitrary file to
135
+ // ask it to lock for us. It actually errors if the file doesn't exist. So
136
+ // we end up with a mostly pointless file, and an adjacent "<file>.lock"
137
+ // directory that manages the lock (to acquire a lock, it does a mkdir for
138
+ // "<file>.lock", which will atomically succeed or fail depending on whether
139
+ // it already existed).
140
+ //
141
+ // TODO(aomarks) We could make our own implementation that directly takes a
142
+ // directory to mkdir and doesn't care about the file. There are some nice
143
+ // details proper-lockfile handles.
144
+ const lockFile = pathlib.join(this.#dataDir, 'lock');
145
+ await fs.mkdir(pathlib.dirname(lockFile), { recursive: true });
146
+ await fs.writeFile(lockFile, '', 'utf8');
147
+ let loggedLocked = false;
148
+ while (true) {
149
+ try {
150
+ const release = await lockfile.lock(lockFile, {
151
+ // If this many milliseconds has elapsed since the lock mtime was last
152
+ // updated, proper-lockfile will delete it and attempt to acquire the
153
+ // lock again. This handles the case where a process holding the lock
154
+ // hard-crashed.
155
+ stale: 10_000,
156
+ // How frequently the mtime for the lock will be updated while it is
157
+ // being held. This should be some smallish factor of "stale" so that
158
+ // we're unlikely to appear stale when we're actually still working on
159
+ // the script.
160
+ update: 2000,
161
+ });
162
+ try {
163
+ return await workFn();
164
+ }
165
+ finally {
166
+ await release();
167
+ }
168
+ }
169
+ catch (error) {
170
+ if (error.code === 'ELOCKED') {
171
+ if (!loggedLocked) {
172
+ // Only log this once.
173
+ this._logger.log({
174
+ script: this._config,
175
+ type: 'info',
176
+ detail: 'locked',
177
+ });
178
+ loggedLocked = true;
179
+ }
180
+ // Wait a moment before attempting to acquire the lock again.
181
+ await new Promise((resolve) => setTimeout(resolve, 200));
182
+ if (this.#shouldNotStart) {
183
+ return { ok: false, error: [this.#startCancelledEvent] };
184
+ }
185
+ }
186
+ else {
187
+ throw error;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * Check whether the given fingerprint matches the current one from the
194
+ * `.wireit` directory.
195
+ */
196
+ async #fingerprintIsFresh(fingerprint) {
197
+ if (!fingerprint.data.fullyTracked) {
198
+ return false;
199
+ }
200
+ const prevFingerprint = await this.#readPreviousFingerprint();
201
+ return prevFingerprint !== undefined && fingerprint.equal(prevFingerprint);
202
+ }
203
+ /**
204
+ * Handle the outcome where the script is already fresh.
205
+ */
206
+ #handleFresh(fingerprint) {
207
+ this._logger.log({
208
+ script: this._config,
209
+ type: 'success',
210
+ reason: 'fresh',
211
+ });
212
+ return { ok: true, value: fingerprint };
213
+ }
214
+ /**
215
+ * Handle the outcome where the script was stale and we got a cache hit.
216
+ */
217
+ async #handleCacheHit(cacheHit, fingerprint) {
218
+ // Optimization: early signal that services are not needed while we're still
219
+ // restoring from cache.
220
+ this._servicesNotNeeded.resolve();
221
+ // Delete the fingerprint and other files. It's important we do this before
222
+ // restoring from cache, because we don't want to think that the previous
223
+ // fingerprint is still valid when it no longer is.
224
+ await this.#prepareDataDir();
225
+ // If we are restoring from cache, we should always delete existing output.
226
+ // The purpose of "clean:false" and "clean:if-file-deleted" is to allow
227
+ // tools with incremental build (like tsc --build) to work.
228
+ //
229
+ // However, this only applies when the tool is able to observe each
230
+ // incremental change to the input files. When we restore from cache, we are
231
+ // directly replacing the output files, and not invoking the tool at all, so
232
+ // there is no way for the tool to do any cleanup.
233
+ await this.#cleanOutput();
234
+ await cacheHit.apply();
235
+ this.#state = 'after-running';
236
+ const writeFingerprintPromise = this.#writeFingerprintFile(fingerprint);
237
+ const outputFilesAfterRunning = await this.#globOutputFilesAfterRunning();
238
+ if (!outputFilesAfterRunning.ok) {
239
+ return { ok: false, error: [outputFilesAfterRunning.error] };
240
+ }
241
+ if (outputFilesAfterRunning.value !== undefined) {
242
+ const outputManifest = await this.#computeOutputManifest(outputFilesAfterRunning.value);
243
+ if (!outputManifest.ok) {
244
+ return { ok: false, error: [outputManifest.error] };
245
+ }
246
+ await this.#writeOutputManifest(outputManifest.value);
247
+ }
248
+ await writeFingerprintPromise;
249
+ this._logger.log({
250
+ script: this._config,
251
+ type: 'success',
252
+ reason: 'cached',
253
+ });
254
+ return { ok: true, value: fingerprint };
255
+ }
256
+ /**
257
+ * Handle the outcome where the script was stale and we need to run it.
258
+ */
259
+ async #handleNeedsRun(fingerprint) {
260
+ // Check if we should clean before we delete the fingerprint file, because
261
+ // we sometimes need to read the previous fingerprint file to determine
262
+ // this.
263
+ const shouldClean = await this.#shouldClean(fingerprint);
264
+ // Delete the fingerprint and other files. It's important we do this before
265
+ // starting the command, because we don't want to think that the previous
266
+ // fingerprint is still valid when it no longer is.
267
+ await this.#prepareDataDir();
268
+ if (shouldClean) {
269
+ const result = await this.#cleanOutput();
270
+ if (!result.ok) {
271
+ return { ok: false, error: [result.error] };
272
+ }
273
+ }
274
+ const childResult = await this.#workerPool.run(async () => {
275
+ // Significant time could have elapsed since we last checked because of
276
+ // parallelism limits.
277
+ if (this.#shouldNotStart) {
278
+ return { ok: false, error: this.#startCancelledEvent };
279
+ }
280
+ let earlyServiceTermination;
281
+ if (this._config.services.length > 0) {
282
+ const servicesStarted = await this._startServices();
283
+ if (!servicesStarted.ok) {
284
+ return servicesStarted;
285
+ }
286
+ void this._anyServiceTerminated.then(() => {
287
+ if (this.#state === 'after-running') {
288
+ // This is expected after we're done.
289
+ return;
290
+ }
291
+ earlyServiceTermination = {
292
+ script: this._config,
293
+ type: 'failure',
294
+ reason: 'dependency-service-exited-unexpectedly',
295
+ };
296
+ // Stop running. If a service we depend on is down, then we know we're
297
+ // in an invalid state too.
298
+ child.kill();
299
+ this._executor.notifyFailure();
300
+ });
301
+ }
302
+ this.#state = 'running';
303
+ this._logger.log({
304
+ script: this._config,
305
+ type: 'info',
306
+ detail: 'running',
307
+ });
308
+ const child = new ScriptChildProcess(
309
+ // Unfortunately TypeScript doesn't automatically narrow this type
310
+ // based on the undefined-command check we did just above.
311
+ this._config);
312
+ void this._executor.shouldKillRunningScripts.then(() => {
313
+ child.kill();
314
+ });
315
+ child.stdout.on('data', (data) => {
316
+ this._logger.log({
317
+ script: this._config,
318
+ type: 'output',
319
+ stream: 'stdout',
320
+ data,
321
+ });
322
+ });
323
+ child.stderr.on('data', (data) => {
324
+ this._logger.log({
325
+ script: this._config,
326
+ type: 'output',
327
+ stream: 'stderr',
328
+ data,
329
+ });
330
+ });
331
+ const result = await child.completed;
332
+ if (result.ok) {
333
+ if (earlyServiceTermination !== undefined) {
334
+ return { ok: false, error: earlyServiceTermination };
335
+ }
336
+ else {
337
+ this._logger.log({
338
+ script: this._config,
339
+ type: 'success',
340
+ reason: 'exit-zero',
341
+ });
342
+ }
343
+ }
344
+ else {
345
+ this._logger.log(result.error);
346
+ // This failure will propagate to the Executor eventually anyway, but
347
+ // asynchronously.
348
+ //
349
+ // The problem with that is that when parallelism is constrained, the
350
+ // next script waiting on this WorkerPool might start running before
351
+ // the failure information propagates, because returning from this
352
+ // function immediately unblocks the next worker.
353
+ //
354
+ // By directly notifying the Executor about the failure while we are
355
+ // still inside the WorkerPool callback, we prevent this race
356
+ // condition.
357
+ this._executor.notifyFailure();
358
+ }
359
+ return result;
360
+ });
361
+ this.#state = 'after-running';
362
+ if (!childResult.ok) {
363
+ this._executor.registerWatchIterationFailure(this._config, fingerprint);
364
+ return {
365
+ ok: false,
366
+ error: Array.isArray(childResult.error)
367
+ ? childResult.error
368
+ : [childResult.error],
369
+ };
370
+ }
371
+ // Optimization: early signal that services are no longer needed while we're
372
+ // still writing the fingerprint file etc.
373
+ this._servicesNotNeeded.resolve();
374
+ const writeFingerprintPromise = this.#writeFingerprintFile(fingerprint);
375
+ const outputFilesAfterRunning = await this.#globOutputFilesAfterRunning();
376
+ if (!outputFilesAfterRunning.ok) {
377
+ return { ok: false, error: [outputFilesAfterRunning.error] };
378
+ }
379
+ if (outputFilesAfterRunning.value !== undefined) {
380
+ const outputManifest = await this.#computeOutputManifest(outputFilesAfterRunning.value);
381
+ if (!outputManifest.ok) {
382
+ return { ok: false, error: [outputManifest.error] };
383
+ }
384
+ await this.#writeOutputManifest(outputManifest.value);
385
+ }
386
+ await writeFingerprintPromise;
387
+ if (fingerprint.data.fullyTracked) {
388
+ const result = await this.#saveToCacheIfPossible(fingerprint);
389
+ if (!result.ok) {
390
+ return { ok: false, error: [result.error] };
391
+ }
392
+ }
393
+ return { ok: true, value: fingerprint };
394
+ }
395
+ async #shouldClean(fingerprint) {
396
+ const cleanValue = this._config.clean;
397
+ switch (cleanValue) {
398
+ case true: {
399
+ return true;
400
+ }
401
+ case false: {
402
+ return false;
403
+ }
404
+ case 'if-file-deleted': {
405
+ const prevFingerprint = await this.#readPreviousFingerprint();
406
+ if (prevFingerprint === undefined) {
407
+ // If we don't know the previous fingerprint, then we can't know
408
+ // whether any input files were removed. It's safer to err on the
409
+ // side of cleaning.
410
+ return true;
411
+ }
412
+ return this.#anyInputFilesDeletedSinceLastRun(fingerprint, prevFingerprint);
413
+ }
414
+ default: {
415
+ throw new Error(`Unhandled clean setting: ${unreachable(cleanValue)}`);
416
+ }
417
+ }
418
+ }
419
+ /**
420
+ * Compares the current set of input file names to the previous set of input
421
+ * file names, and returns whether any files have been removed.
422
+ */
423
+ #anyInputFilesDeletedSinceLastRun(curFingerprint, prevFingerprint) {
424
+ const curFiles = Object.keys(curFingerprint.data.files);
425
+ const prevFiles = Object.keys(prevFingerprint.data.files);
426
+ if (curFiles.length < prevFiles.length) {
427
+ return true;
428
+ }
429
+ const newFilesSet = new Set(curFiles);
430
+ for (const oldFile of prevFiles) {
431
+ if (!newFilesSet.has(oldFile)) {
432
+ return true;
433
+ }
434
+ }
435
+ return false;
436
+ }
437
+ /**
438
+ * Save the current output files to the configured cache if possible.
439
+ */
440
+ async #saveToCacheIfPossible(fingerprint) {
441
+ if (this.#cache === undefined) {
442
+ return { ok: true, value: undefined };
443
+ }
444
+ const paths = await this.#globOutputFilesAfterRunning();
445
+ if (!paths.ok) {
446
+ return paths;
447
+ }
448
+ if (paths.value === undefined) {
449
+ return { ok: true, value: undefined };
450
+ }
451
+ await this.#cache.set(this._config, fingerprint, paths.value);
452
+ return { ok: true, value: undefined };
453
+ }
454
+ /**
455
+ * Glob the output files for this script and cache them, but throw unless the
456
+ * script has not yet started running or been restored from cache.
457
+ */
458
+ #globOutputFilesBeforeRunning() {
459
+ this.#ensureState('before-running');
460
+ return (this.#cachedOutputFilesBeforeRunning ??= this.#globOutputFiles());
461
+ }
462
+ #cachedOutputFilesBeforeRunning;
463
+ /**
464
+ * Glob the output files for this script and cache them, but throw unless the
465
+ * script has finished running or been restored from cache.
466
+ */
467
+ #globOutputFilesAfterRunning() {
468
+ this.#ensureState('after-running');
469
+ return (this.#cachedOutputFilesAfterRunning ??= this.#globOutputFiles());
470
+ }
471
+ #cachedOutputFilesAfterRunning;
472
+ /**
473
+ * Glob the output files for this script, or return undefined if output files
474
+ * are not defined.
475
+ */
476
+ async #globOutputFiles() {
477
+ if (this._config.output === undefined) {
478
+ return { ok: true, value: undefined };
479
+ }
480
+ try {
481
+ return {
482
+ ok: true,
483
+ value: await glob(this._config.output.values, {
484
+ cwd: this._config.packageDir,
485
+ followSymlinks: false,
486
+ includeDirectories: true,
487
+ expandDirectories: true,
488
+ throwIfOutsideCwd: true,
489
+ }),
490
+ };
491
+ }
492
+ catch (error) {
493
+ if (error instanceof GlobOutsideCwdError) {
494
+ // TODO(aomarks) It would be better to do this in the Analyzer by
495
+ // looking at the output glob patterns. See
496
+ // https://github.com/google/wireit/issues/64.
497
+ return {
498
+ ok: false,
499
+ error: {
500
+ type: 'failure',
501
+ reason: 'invalid-config-syntax',
502
+ script: this._config,
503
+ diagnostic: {
504
+ severity: 'error',
505
+ message: `Output files must be within the package: ${error.message}`,
506
+ location: {
507
+ file: this._config.declaringFile,
508
+ range: {
509
+ offset: this._config.output.node.offset,
510
+ length: this._config.output.node.length,
511
+ },
512
+ },
513
+ },
514
+ },
515
+ };
516
+ }
517
+ throw error;
518
+ }
519
+ }
520
+ /**
521
+ * Get the directory name where Wireit data can be saved for this script.
522
+ */
523
+ get #dataDir() {
524
+ return getScriptDataDir(this._config);
525
+ }
526
+ /**
527
+ * Get the path where the current fingerprint is saved for this script.
528
+ */
529
+ get #fingerprintFilePath() {
530
+ return pathlib.join(this.#dataDir, 'fingerprint');
531
+ }
532
+ /**
533
+ * Read this script's previous fingerprint from `fingerprint` file in the
534
+ * `.wireit` directory. Cached after first call.
535
+ */
536
+ async #readPreviousFingerprint() {
537
+ if (this.#cachedPreviousFingerprint === undefined) {
538
+ this.#cachedPreviousFingerprint = (async () => {
539
+ try {
540
+ return Fingerprint.fromString((await fs.readFile(this.#fingerprintFilePath, 'utf8')));
541
+ }
542
+ catch (error) {
543
+ if (error.code === 'ENOENT') {
544
+ return undefined;
545
+ }
546
+ throw error;
547
+ }
548
+ })();
549
+ }
550
+ return this.#cachedPreviousFingerprint;
551
+ }
552
+ #cachedPreviousFingerprint;
553
+ /**
554
+ * Write this script's fingerprint file.
555
+ */
556
+ async #writeFingerprintFile(fingerprint) {
557
+ await fs.mkdir(this.#dataDir, { recursive: true });
558
+ await fs.writeFile(this.#fingerprintFilePath, fingerprint.string, 'utf8');
559
+ }
560
+ /**
561
+ * Delete the fingerprint and other files for this script from the previous
562
+ * run, and ensure the data directory exists.
563
+ */
564
+ async #prepareDataDir() {
565
+ await Promise.all([
566
+ fs.rm(this.#fingerprintFilePath, { force: true }),
567
+ fs.mkdir(this.#dataDir, { recursive: true }),
568
+ ]);
569
+ }
570
+ /**
571
+ * Delete all files matched by this script's "output" glob patterns.
572
+ */
573
+ async #cleanOutput() {
574
+ const files = await this.#globOutputFilesBeforeRunning();
575
+ if (!files.ok) {
576
+ return files;
577
+ }
578
+ if (files.value === undefined) {
579
+ return { ok: true, value: undefined };
580
+ }
581
+ await deleteEntries(files.value);
582
+ return { ok: true, value: undefined };
583
+ }
584
+ /**
585
+ * Compute the output manifest for this script, which is the sorted list of
586
+ * all output filenames, along with filesystem metadata that we assume is good
587
+ * enough for checking that a file hasn't changed: ctime, mtime, and bytes.
588
+ */
589
+ async #computeOutputManifest(outputEntries) {
590
+ outputEntries.sort((a, b) => a.path.localeCompare(b.path));
591
+ const stats = [];
592
+ const deleted = [];
593
+ await Promise.all(outputEntries.map(async (entry, i) => {
594
+ try {
595
+ stats[i] = await fs.lstat(entry.path);
596
+ }
597
+ catch (e) {
598
+ if (e.code === 'ENOENT') {
599
+ deleted.push(entry.path);
600
+ }
601
+ else {
602
+ throw e;
603
+ }
604
+ }
605
+ }));
606
+ if (deleted.length > 0) {
607
+ return {
608
+ ok: false,
609
+ error: {
610
+ type: 'failure',
611
+ reason: 'output-file-deleted-unexpectedly',
612
+ script: this._config,
613
+ filePaths: deleted.sort(),
614
+ },
615
+ };
616
+ }
617
+ const manifest = {};
618
+ for (let i = 0; i < outputEntries.length; i++) {
619
+ manifest[outputEntries[i].path] = computeManifestEntry(stats[i]);
620
+ }
621
+ return { ok: true, value: JSON.stringify(manifest) };
622
+ }
623
+ /**
624
+ * Check whether the current manifest of output files matches the one from the
625
+ * `.wireit` directory.
626
+ */
627
+ async #outputManifestIsFresh() {
628
+ const oldManifestPromise = this.#readPreviousOutputManifest();
629
+ const outputFilesBeforeRunning = await this.#globOutputFilesBeforeRunning();
630
+ if (!outputFilesBeforeRunning.ok) {
631
+ return outputFilesBeforeRunning;
632
+ }
633
+ if (outputFilesBeforeRunning.value === undefined) {
634
+ return { ok: true, value: false };
635
+ }
636
+ const newManifest = await this.#computeOutputManifest(outputFilesBeforeRunning.value);
637
+ if (!newManifest.ok) {
638
+ return newManifest;
639
+ }
640
+ const oldManifest = await oldManifestPromise;
641
+ if (oldManifest === undefined) {
642
+ return { ok: true, value: false };
643
+ }
644
+ const equal = newManifest.value === oldManifest;
645
+ if (!equal) {
646
+ this._logger.log({
647
+ script: this._config,
648
+ type: 'info',
649
+ detail: 'output-modified',
650
+ });
651
+ }
652
+ return { ok: true, value: equal };
653
+ }
654
+ /**
655
+ * Read this script's previous output manifest file from the `manifest` file
656
+ * in the `.wireit` directory. Not cached.
657
+ */
658
+ async #readPreviousOutputManifest() {
659
+ try {
660
+ return (await fs.readFile(this.#outputManifestFilePath, 'utf8'));
661
+ }
662
+ catch (error) {
663
+ if (error.code === 'ENOENT') {
664
+ return undefined;
665
+ }
666
+ throw error;
667
+ }
668
+ }
669
+ /**
670
+ * Write this script's output manifest file.
671
+ */
672
+ async #writeOutputManifest(outputManifest) {
673
+ await fs.mkdir(this.#dataDir, { recursive: true });
674
+ await fs.writeFile(this.#outputManifestFilePath, outputManifest, 'utf8');
675
+ }
676
+ /**
677
+ * Get the path where the current output manifest is saved for this script.
678
+ */
679
+ get #outputManifestFilePath() {
680
+ return pathlib.join(this.#dataDir, 'manifest');
681
+ }
682
+ }
683
+ //# sourceMappingURL=standard.js.map