@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
|
@@ -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
|
package/lib/util/ast.js
ADDED
|
@@ -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
|
package/lib/util/copy.js
ADDED
|
@@ -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
|