@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,270 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as pathlib from 'path';
7
+ import { spawn } from 'child_process';
8
+ import { augmentProcessEnvSafelyIfOnWindows, IS_WINDOWS, } from './util/windows.js';
9
+ import { Deferred } from './util/deferred.js';
10
+ /**
11
+ * The PATH environment variable of this process, minus all of the leading
12
+ * "node_modules/.bin" entries that the incoming "npm run" command already set.
13
+ *
14
+ * We want full control over which "node_modules/.bin" paths are in the PATH of
15
+ * the processes we spawn, so that cross-package dependencies act as though we
16
+ * are running "npm run" with each package as the cwd.
17
+ *
18
+ * We only need to do this once per Wireit process, because process.env never
19
+ * changes.
20
+ */
21
+ const PATH_ENV_SUFFIX = (() => {
22
+ const path = process.env.PATH ?? '';
23
+ // Note the PATH delimiter is platform-dependent.
24
+ const entries = path.split(pathlib.delimiter);
25
+ const nodeModulesBinSuffix = pathlib.join('node_modules', '.bin');
26
+ const endOfNodeModuleBins = entries.findIndex((entry) => !entry.endsWith(nodeModulesBinSuffix));
27
+ return entries.slice(endOfNodeModuleBins).join(pathlib.delimiter);
28
+ })();
29
+ /**
30
+ * A child process spawned during execution of a script.
31
+ */
32
+ export class ScriptChildProcess {
33
+ #script;
34
+ #child;
35
+ #started;
36
+ #completed;
37
+ #state;
38
+ get stdout() {
39
+ return this.#child.stdout;
40
+ }
41
+ get stderr() {
42
+ return this.#child.stderr;
43
+ }
44
+ constructor(script) {
45
+ this.#started = new Deferred();
46
+ this.#completed = new Deferred();
47
+ this.#state = 'starting';
48
+ /**
49
+ * Resolves when this process starts
50
+ */
51
+ this.started = this.#started.promise;
52
+ /**
53
+ * Resolves when this child process ends.
54
+ */
55
+ this.completed = this.#completed.promise;
56
+ // Copy only the fields we actually require from the script config, because
57
+ // the full script config contains references to the full config, which we
58
+ // want to allow to be garbage-collected across watch iterations.
59
+ this.#script = {
60
+ packageDir: script.packageDir,
61
+ name: script.name,
62
+ command: script.command,
63
+ extraArgs: script.extraArgs,
64
+ env: script.env,
65
+ };
66
+ // TODO(aomarks) Update npm_ environment variables to reflect the new
67
+ // package.
68
+ this.#child = spawn(this.#script.command.value, this.#script.extraArgs, {
69
+ cwd: this.#script.packageDir,
70
+ // Conveniently, "shell:true" has the same shell-selection behavior as
71
+ // "npm run", where on macOS and Linux it is "sh", and on Windows it is
72
+ // %COMSPEC% || "cmd.exe".
73
+ //
74
+ // References:
75
+ // https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
76
+ // https://nodejs.org/api/child_process.html#default-windows-shell
77
+ // https://github.com/npm/run-script/blob/a5b03bdfc3a499bf7587d7414d5ea712888bfe93/lib/make-spawn-args.js#L11
78
+ shell: true,
79
+ env: augmentProcessEnvSafelyIfOnWindows({
80
+ FORCE_COLOR: process.stdout.isTTY && process.env.FORCE_COLOR === undefined
81
+ ? 'true'
82
+ : process.env.FORCE_COLOR,
83
+ PATH: this.#pathEnvironmentVariable,
84
+ ...this.#script.env,
85
+ }),
86
+ // Set "detached" on Linux and macOS so that we create a new process
87
+ // group, instead of being added to the process group for this Wireit
88
+ // process.
89
+ //
90
+ // We need a new process group so that we can use "kill(-pid)" to kill all
91
+ // of the processes in the process group, instead of just the group leader
92
+ // "sh" process. "sh" does not forward signals to child processes, so a
93
+ // regular "kill(pid)" would not kill the actual process we care about.
94
+ //
95
+ // On Windows this works differently, and we use the "\t" flag to
96
+ // "taskkill" to kill child processes. However, if we do set "detached" on
97
+ // Windows, it causes the child process to open in a new terminal window,
98
+ // which we don't want.
99
+ detached: !IS_WINDOWS,
100
+ });
101
+ this.#child.on('spawn', () => {
102
+ switch (this.#state) {
103
+ case 'starting': {
104
+ this.#started.resolve({ ok: true, value: undefined });
105
+ this.#state = 'started';
106
+ break;
107
+ }
108
+ case 'killing': {
109
+ this.#started.resolve({ ok: true, value: undefined });
110
+ // We received a kill request while we were still starting. Kill now
111
+ // that we're started.
112
+ this.#actuallyKill();
113
+ break;
114
+ }
115
+ case 'started':
116
+ case 'stopped': {
117
+ const exception = new Error(`Internal error: Expected ScriptChildProcessState ` +
118
+ `to be "started" or "killing" but was "${this.#state}"`);
119
+ this.#started.reject(exception);
120
+ this.#completed.reject(exception);
121
+ break;
122
+ }
123
+ default: {
124
+ const never = this.#state;
125
+ const exception = new Error(`Internal error: unexpected ScriptChildProcessState: ${String(never)}`);
126
+ this.#started.reject(exception);
127
+ this.#completed.reject(exception);
128
+ }
129
+ }
130
+ });
131
+ this.#child.on('error', (error) => {
132
+ const result = {
133
+ ok: false,
134
+ error: {
135
+ script,
136
+ type: 'failure',
137
+ reason: 'spawn-error',
138
+ message: error.message,
139
+ },
140
+ };
141
+ this.#started.resolve(result);
142
+ this.#completed.resolve(result);
143
+ this.#state = 'stopped';
144
+ });
145
+ this.#child.on('close', (status, signal) => {
146
+ if (this.#state === 'killing') {
147
+ this.#completed.resolve({
148
+ ok: false,
149
+ error: {
150
+ script,
151
+ type: 'failure',
152
+ reason: 'killed',
153
+ },
154
+ });
155
+ }
156
+ else if (signal !== null) {
157
+ this.#completed.resolve({
158
+ ok: false,
159
+ error: {
160
+ script,
161
+ type: 'failure',
162
+ reason: 'signal',
163
+ signal,
164
+ },
165
+ });
166
+ }
167
+ else if (status !== 0) {
168
+ this.#completed.resolve({
169
+ ok: false,
170
+ error: {
171
+ script,
172
+ type: 'failure',
173
+ reason: 'exit-non-zero',
174
+ // status should only ever be null if signal was not null, but
175
+ // this isn't reflected in the TypeScript types. Just in case, and
176
+ // to make TypeScript happy, fall back to -1 (which is a
177
+ // conventional exit status used for "exited with signal").
178
+ status: status ?? -1,
179
+ },
180
+ });
181
+ }
182
+ else {
183
+ this.#completed.resolve({ ok: true, value: undefined });
184
+ }
185
+ this.#state = 'stopped';
186
+ });
187
+ }
188
+ /**
189
+ * Kill this child process. On Linux/macOS, sends a `SIGINT` signal. On
190
+ * Windows, invokes `taskkill /pid PID /t`.
191
+ *
192
+ * Note this function returns immediately. To find out when the process was
193
+ * actually killed, use the {@link completed} promise.
194
+ */
195
+ kill() {
196
+ switch (this.#state) {
197
+ case 'started': {
198
+ this.#actuallyKill();
199
+ return;
200
+ }
201
+ case 'starting': {
202
+ // We're still starting up, and it's not possible to abort. When we get
203
+ // the "spawn" event, we'll notice the "killing" state and actually kill
204
+ // then.
205
+ this.#state = 'killing';
206
+ return;
207
+ }
208
+ case 'killing':
209
+ case 'stopped': {
210
+ // No-op.
211
+ return;
212
+ }
213
+ default: {
214
+ const never = this.#state;
215
+ throw new Error(`Internal error: unexpected ScriptChildProcessState: ${String(never)}`);
216
+ }
217
+ }
218
+ }
219
+ #actuallyKill() {
220
+ if (this.#child.pid === undefined) {
221
+ throw new Error(`Internal error: Can't kill child process because it has no pid. ` +
222
+ `Command: ${JSON.stringify(this.#script.command)}.`);
223
+ }
224
+ if (IS_WINDOWS) {
225
+ // Windows doesn't have signals. Node ChildProcess.kill() sort of emulates
226
+ // the behavior of SIGKILL (and ignores the signal you pass in), but this
227
+ // doesn't end child processes. We have child processes because the parent
228
+ // process is the shell (cmd.exe or PowerShell).
229
+ // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/taskkill
230
+ spawn('taskkill', [
231
+ '/pid',
232
+ this.#child.pid.toString(),
233
+ /* End child processes */ '/t',
234
+ /* Force. Killing does not seem reliable otherwise. */ '/f',
235
+ ]);
236
+ }
237
+ else {
238
+ // We used "detached" when we spawned, so our child is the leader of a
239
+ // process group. Passing the negative of a pid kills all processes in
240
+ // that group (without the negative, only the leader "sh" process would be
241
+ // killed).
242
+ process.kill(-this.#child.pid, 'SIGINT');
243
+ }
244
+ this.#state = 'killing';
245
+ }
246
+ /**
247
+ * Generates the PATH environment variable that should be set when this
248
+ * script's command is spawned.
249
+ */
250
+ get #pathEnvironmentVariable() {
251
+ // Given package "/foo/bar", walk up the path hierarchy to generate
252
+ // "/foo/bar/node_modules/.bin:/foo/node_modules/.bin:/node_modules/.bin".
253
+ const entries = [];
254
+ let cur = this.#script.packageDir;
255
+ while (true) {
256
+ entries.push(pathlib.join(cur, 'node_modules', '.bin'));
257
+ const parent = pathlib.dirname(cur);
258
+ if (parent === cur) {
259
+ break;
260
+ }
261
+ cur = parent;
262
+ }
263
+ // Add the inherited PATH variable, minus any "node_modules/.bin" entries
264
+ // that were set by the "npm run" command that spawned Wireit.
265
+ entries.push(PATH_ENV_SUFFIX);
266
+ // Note the PATH delimiter is platform-dependent.
267
+ return entries.join(pathlib.delimiter);
268
+ }
269
+ }
270
+ //# sourceMappingURL=script-child-process.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as jsonParser from 'jsonc-parser';
7
+ import { parseTree as parseTreeInternal } from 'jsonc-parser';
8
+ import * as pathlib from 'path';
9
+ export function findNamedNodeAtLocation(astNode, path, file) {
10
+ const node = findNodeAtLocation(astNode, path);
11
+ const parent = node?.parent;
12
+ if (node === undefined || parent === undefined) {
13
+ return { ok: true, value: undefined };
14
+ }
15
+ const name = parent.children?.[0];
16
+ if (parent.type !== 'property' || name === undefined) {
17
+ return {
18
+ ok: false,
19
+ error: {
20
+ type: 'failure',
21
+ reason: 'invalid-config-syntax',
22
+ script: { packageDir: pathlib.dirname(file.path) },
23
+ diagnostic: {
24
+ severity: 'error',
25
+ message: `Expected a property, but got a ${parent.type}`,
26
+ location: {
27
+ file,
28
+ range: { offset: astNode.offset, length: astNode.length },
29
+ },
30
+ },
31
+ },
32
+ };
33
+ }
34
+ node.name = name;
35
+ return { ok: true, value: node };
36
+ }
37
+ export function findNodeAtLocation(astNode, path) {
38
+ return jsonParser.findNodeAtLocation(astNode, path);
39
+ }
40
+ export function parseTree(filePath, json) {
41
+ const errors = [];
42
+ const result = parseTreeInternal(json, errors);
43
+ if (errors.length > 0) {
44
+ const diagnostics = errors.map((error) => ({
45
+ severity: 'error',
46
+ message: `JSON syntax error`,
47
+ location: {
48
+ file: {
49
+ path: filePath,
50
+ contents: json,
51
+ ast: result,
52
+ },
53
+ range: {
54
+ offset: error.offset,
55
+ length: error.length,
56
+ },
57
+ },
58
+ }));
59
+ return {
60
+ ok: false,
61
+ error: {
62
+ type: 'failure',
63
+ reason: 'invalid-json-syntax',
64
+ script: { packageDir: pathlib.dirname(filePath) },
65
+ diagnostics,
66
+ },
67
+ };
68
+ }
69
+ return { ok: true, value: result };
70
+ }
71
+ //# sourceMappingURL=ast.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * A cache for values that are asynchronously computed that ensures that we
8
+ * will compute the value for each key at most once.
9
+ */
10
+ export class AsyncCache {
11
+ #cache = new Map();
12
+ async getOrCompute(key, compute) {
13
+ let result = this.#cache.get(key);
14
+ if (result === undefined) {
15
+ result = compute();
16
+ this.#cache.set(key, result);
17
+ }
18
+ return result;
19
+ }
20
+ get values() {
21
+ return this.#cache.values();
22
+ }
23
+ }
24
+ //# sourceMappingURL=async-cache.js.map
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as fs from './fs.js';
7
+ import * as pathlib from 'path';
8
+ import { optimizeMkdirs } from './optimize-mkdirs.js';
9
+ import { IS_WINDOWS } from '../util/windows.js';
10
+ /**
11
+ * Copy all of the given files and directories from one directory to another.
12
+ *
13
+ * Directories are NOT copied recursively. If a directory is listed in
14
+ * {@link entries} without any of its children being listed, then an empty
15
+ * directory will be created.
16
+ *
17
+ * Parent directories are created automatically. E.g. listing "foo/bar" will
18
+ * automatically create "foo/", even if "foo/" wasn't listed.
19
+ */
20
+ export const copyEntries = async (entries, sourceDir, destDir) => {
21
+ if (entries.length === 0) {
22
+ return;
23
+ }
24
+ const files = new Set();
25
+ const symlinks = new Set();
26
+ const directories = new Set();
27
+ for (const { path: absolutePath, dirent } of entries) {
28
+ const relativePath = pathlib.relative(sourceDir, absolutePath);
29
+ if (dirent.isDirectory()) {
30
+ directories.add(pathlib.join(destDir, relativePath));
31
+ }
32
+ else {
33
+ directories.add(pathlib.join(destDir, pathlib.dirname(relativePath)));
34
+ if (dirent.isSymbolicLink()) {
35
+ symlinks.add(relativePath);
36
+ }
37
+ else {
38
+ files.add(relativePath);
39
+ }
40
+ }
41
+ }
42
+ await Promise.all(optimizeMkdirs([...directories]).map((path) => fs.mkdir(path, { recursive: true })));
43
+ const copyPromises = [];
44
+ for (const path of files) {
45
+ copyPromises.push(copyFileGracefully(pathlib.join(sourceDir, path), pathlib.join(destDir, path)));
46
+ }
47
+ for (const path of symlinks) {
48
+ copyPromises.push(copySymlinkGracefully(pathlib.join(sourceDir, path), pathlib.join(destDir, path)));
49
+ }
50
+ await Promise.all(copyPromises);
51
+ };
52
+ /**
53
+ * Copy a file. If the source doesn't exist, do nothing. If the destination
54
+ * already exists, throw an error.
55
+ */
56
+ const copyFileGracefully = async (src, dest) => {
57
+ try {
58
+ await fs.copyFile(src, dest,
59
+ // COPYFILE_FICLONE: Copy the file using copy-on-write semantics, so that
60
+ // the copy takes constant time and space. This is a noop currently
61
+ // on some platforms, but it's a nice optimization to have.
62
+ // See https://github.com/libuv/libuv/issues/2936 for macos support.
63
+ fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE);
64
+ }
65
+ catch (error) {
66
+ const { code } = error;
67
+ if (code === /* does not exist */ 'ENOENT') {
68
+ return;
69
+ }
70
+ throw error;
71
+ }
72
+ };
73
+ /**
74
+ * Copy a symlink verbatim without following or resolving the target. If the
75
+ * source doesn't exist, do nothing.
76
+ */
77
+ const copySymlinkGracefully = async (src, dest) => {
78
+ try {
79
+ const target = await fs.readlink(src, { encoding: 'buffer' });
80
+ // Windows symlinks need to be flagged for whether the target is a file or a
81
+ // directory. We can't derive that from the symlink itself, so we instead
82
+ // need to check the type of the target.
83
+ const windowsType = IS_WINDOWS
84
+ ? // The target could be in the source or the destination, check both.
85
+ ((await detectWindowsSymlinkType(target, src)) ??
86
+ (await detectWindowsSymlinkType(target, dest)) ??
87
+ // It doesn't exist in either place, so there's no way to know. Just
88
+ // assume "file".
89
+ 'file')
90
+ : undefined;
91
+ await fs.symlink(target, dest, windowsType);
92
+ }
93
+ catch (error) {
94
+ const { code } = error;
95
+ if (code === /* does not exist */ 'ENOENT') {
96
+ return;
97
+ }
98
+ throw error;
99
+ }
100
+ };
101
+ /**
102
+ * Resolve symlink {@link target} relative to {@link linkPath} and try to detect
103
+ * whether the target is a file or directory. If the target doesn't exist,
104
+ * returns undefined.
105
+ */
106
+ const detectWindowsSymlinkType = async (target, linkPath) => {
107
+ const resolved = pathlib.resolve(pathlib.dirname(linkPath), target.toString());
108
+ try {
109
+ const stats = await fs.stat(resolved);
110
+ return stats.isDirectory() ? 'dir' : 'file';
111
+ }
112
+ catch (error) {
113
+ const { code } = error;
114
+ if (code === /* does not exist */ 'ENOENT') {
115
+ return undefined;
116
+ }
117
+ throw error;
118
+ }
119
+ };
120
+ //# sourceMappingURL=copy.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Convenience class for tracking a promise alongside its resolve and reject
8
+ * functions.
9
+ */
10
+ export class Deferred {
11
+ #resolve;
12
+ #reject;
13
+ #settled = false;
14
+ constructor() {
15
+ let res, rej;
16
+ this.promise = new Promise((resolve, reject) => {
17
+ res = resolve;
18
+ rej = reject;
19
+ });
20
+ this.#resolve = res;
21
+ this.#reject = rej;
22
+ }
23
+ get settled() {
24
+ return this.#settled;
25
+ }
26
+ resolve(value) {
27
+ this.#settled = true;
28
+ this.#resolve(value);
29
+ }
30
+ reject(reason) {
31
+ this.#settled = true;
32
+ this.#reject(reason);
33
+ }
34
+ }
35
+ //# sourceMappingURL=deferred.js.map
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as fs from './fs.js';
7
+ import * as pathlib from 'path';
8
+ /**
9
+ * Delete all of the given files and directories.
10
+ *
11
+ * Directories are NOT deleted recursively. To delete a directory, it must
12
+ * either already be empty, or all of its transitive children must be explicitly
13
+ * provided in the {@link entries} parameter. Deletes are performed depth-first
14
+ * so that children are deleted before their parents. If a directory is still
15
+ * not empty after all of the provided children have been deleted, then it is
16
+ * left in-place.
17
+ */
18
+ export const deleteEntries = async (entries) => {
19
+ if (entries.length === 0) {
20
+ return;
21
+ }
22
+ const directories = [];
23
+ const unlinkPromises = [];
24
+ for (const { path, dirent } of entries) {
25
+ if (dirent.isDirectory()) {
26
+ // Don't delete directories yet.
27
+ directories.push(path);
28
+ }
29
+ else {
30
+ // Files can start deleting immediately.
31
+ unlinkPromises.push(unlinkGracefully(path));
32
+ }
33
+ }
34
+ // Wait for all files to be deleted before we start deleting directories,
35
+ // because directories need to be empty to be deleted.
36
+ await Promise.all(unlinkPromises);
37
+ if (directories.length === 0) {
38
+ return;
39
+ }
40
+ if (directories.length === 1) {
41
+ // Minor optimization for the common case of 1 directory. We've already
42
+ // deleted all regular files, and we don't delete directories recursively.
43
+ // So either [1] this directory is empty and we should delete it, or [2] it
44
+ // has a child directory that was not explicitly listed so we should leave
45
+ // it in-place.
46
+ await rmdirGracefully(directories[0]);
47
+ return;
48
+ }
49
+ // We have multiple directories to delete. We must delete child directories
50
+ // before their parents, because directories need to be empty to be deleted.
51
+ //
52
+ // Sorting from longest to shortest path and deleting in serial is a simple
53
+ // solution, but we prefer to go in parallel.
54
+ //
55
+ // Build a tree from the path hierarchy, then delete depth-first.
56
+ const root = { children: {} };
57
+ for (const path of directories) {
58
+ let cur = root;
59
+ for (const part of path.split(pathlib.sep)) {
60
+ cur = cur.children[part] ??= { children: {} };
61
+ }
62
+ cur.pathIfShouldDelete = path;
63
+ }
64
+ await deleteDirectoriesDepthFirst(root);
65
+ };
66
+ /**
67
+ * Walk a {@link Directory} tree depth-first, deleting any directories that were
68
+ * scheduled for deletion as long as they are empty.
69
+ */
70
+ const deleteDirectoriesDepthFirst = async (directory) => {
71
+ const childrenDeleted = await Promise.all(Object.values(directory.children).map((child) => deleteDirectoriesDepthFirst(child)));
72
+ if (directory.pathIfShouldDelete === undefined) {
73
+ // This directory wasn't scheduled for deletion.
74
+ return false;
75
+ }
76
+ if (childrenDeleted.some((deleted) => !deleted)) {
77
+ // A child directory wasn't deleted, so there's no point trying to delete
78
+ // this directory, because we know we're not empty and would fail.
79
+ return false;
80
+ }
81
+ return rmdirGracefully(directory.pathIfShouldDelete);
82
+ };
83
+ /**
84
+ * Delete a file. If it doesn't exist, do nothing.
85
+ */
86
+ const unlinkGracefully = async (path) => {
87
+ try {
88
+ await fs.unlink(path);
89
+ }
90
+ catch (error) {
91
+ const { code } = error;
92
+ if (code === /* does not exist */ 'ENOENT') {
93
+ return;
94
+ }
95
+ throw error;
96
+ }
97
+ };
98
+ /**
99
+ * Delete a directory. If it doesn't exist or isn't empty, do nothing.
100
+ *
101
+ * @returns True if the directory was deleted or already didn't exist. False
102
+ * otherwise.
103
+ */
104
+ const rmdirGracefully = async (path) => {
105
+ try {
106
+ await fs.rmdir(path);
107
+ }
108
+ catch (error) {
109
+ const { code } = error;
110
+ if (code === /* does not exist */ 'ENOENT') {
111
+ return true;
112
+ }
113
+ if (code === /* not empty */ 'ENOTEMPTY') {
114
+ return false;
115
+ }
116
+ throw error;
117
+ }
118
+ return true;
119
+ };
120
+ //# sourceMappingURL=delete.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ if (!Symbol.dispose) {
7
+ Symbol.dispose = Symbol('dispose');
8
+ }
9
+ if (!Symbol.asyncDispose) {
10
+ Symbol.asyncDispose = Symbol('asyncDispose');
11
+ }
12
+ if (!Symbol.asyncDispose) {
13
+ Symbol.asyncDispose = Symbol('asyncDispose');
14
+ }
15
+ export {};
16
+ //# sourceMappingURL=dispose.js.map