@jsenv/test 1.5.2 → 1.6.1
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/dist/jsenv_test.js +1229 -981
- package/package.json +1 -1
- package/src/execution/execute.js +5 -1
- package/src/execution/execute_steps.js +8 -22
- package/src/execution/execute_test_plan.js +29 -3
- package/src/execution/web_server/start_using_command.js +132 -0
- package/src/execution/web_server/start_using_module_url.js +79 -0
- package/src/execution/web_server/worker_importing_module_starting_web_server.mjs +5 -0
- package/src/execution/web_server_param.js +53 -32
- package/src/helpers/teardown.js +15 -0
- package/src/runtime_browsers/using_playwright.js +6 -9
package/dist/jsenv_test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, stat, lstat, readdir, promises, unlink, openSync, closeSync, rmdir, readFile as readFile$1, writeFile as writeFile$1, writeFileSync as writeFileSync$1, mkdirSync, readFileSync,
|
|
1
|
+
import { chmod, stat, lstat, readdir, promises, unlink, openSync, closeSync, rmdir, readFile as readFile$1, writeFile as writeFile$1, writeFileSync as writeFileSync$1, mkdirSync, readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { URL_META, filterV8Coverage } from "./js/v8_coverage.js";
|
|
3
3
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
4
4
|
import "node:crypto";
|
|
@@ -8,7 +8,9 @@ import os from "node:os";
|
|
|
8
8
|
import tty from "node:tty";
|
|
9
9
|
import stringWidth from "string-width";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
|
+
import { Worker } from "node:worker_threads";
|
|
11
12
|
import { createServer } from "node:net";
|
|
13
|
+
import { spawn, spawnSync, fork } from "node:child_process";
|
|
12
14
|
import v8, { takeCoverage } from "node:v8";
|
|
13
15
|
import stripAnsi from "strip-ansi";
|
|
14
16
|
import { applyBabelPlugins } from "@jsenv/ast";
|
|
@@ -17,1068 +19,1066 @@ import { runInNewContext } from "node:vm";
|
|
|
17
19
|
import wrapAnsi from "wrap-ansi";
|
|
18
20
|
import { injectSupervisorIntoHTML, supervisorFileUrl } from "@jsenv/plugin-supervisor";
|
|
19
21
|
import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap";
|
|
20
|
-
import { fork } from "node:child_process";
|
|
21
22
|
import { findFreePort } from "@jsenv/server";
|
|
22
|
-
import { Worker } from "node:worker_threads";
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (colonIndex === -1) {
|
|
28
|
-
return "";
|
|
29
|
-
}
|
|
24
|
+
/*
|
|
25
|
+
* See callback_race.md
|
|
26
|
+
*/
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const raceCallbacks = (raceDescription, winnerCallback) => {
|
|
29
|
+
let cleanCallbacks = [];
|
|
30
|
+
let status = "racing";
|
|
34
31
|
|
|
35
|
-
const
|
|
36
|
-
|
|
32
|
+
const clean = () => {
|
|
33
|
+
cleanCallbacks.forEach((clean) => {
|
|
34
|
+
clean();
|
|
35
|
+
});
|
|
36
|
+
cleanCallbacks = null;
|
|
37
|
+
};
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const cancel = () => {
|
|
40
|
+
if (status !== "racing") {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
status = "cancelled";
|
|
44
|
+
clean();
|
|
45
|
+
};
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
Object.keys(raceDescription).forEach((candidateName) => {
|
|
48
|
+
const register = raceDescription[candidateName];
|
|
49
|
+
const returnValue = register((data) => {
|
|
50
|
+
if (status !== "racing") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
status = "done";
|
|
54
|
+
clean();
|
|
55
|
+
winnerCallback({
|
|
56
|
+
name: candidateName,
|
|
57
|
+
data,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
if (typeof returnValue === "function") {
|
|
61
|
+
cleanCallbacks.push(returnValue);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
return urlAsStringWithoutProtocol;
|
|
65
|
+
return cancel;
|
|
53
66
|
};
|
|
54
67
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
68
|
+
const createCallbackListNotifiedOnce = () => {
|
|
69
|
+
let callbacks = [];
|
|
70
|
+
let status = "waiting";
|
|
71
|
+
let currentCallbackIndex = -1;
|
|
60
72
|
|
|
61
|
-
const
|
|
62
|
-
const searchSeparatorIndex = resource.indexOf("?");
|
|
63
|
-
if (searchSeparatorIndex > -1) {
|
|
64
|
-
return resource.slice(0, searchSeparatorIndex);
|
|
65
|
-
}
|
|
66
|
-
const hashIndex = resource.indexOf("#");
|
|
67
|
-
if (hashIndex > -1) {
|
|
68
|
-
return resource.slice(0, hashIndex);
|
|
69
|
-
}
|
|
70
|
-
return resource;
|
|
71
|
-
};
|
|
73
|
+
const callbackListOnce = {};
|
|
72
74
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const add = (callback) => {
|
|
76
|
+
if (status !== "waiting") {
|
|
77
|
+
emitUnexpectedActionWarning({ action: "add", status });
|
|
78
|
+
return removeNoop;
|
|
79
|
+
}
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
pathname = pathname.slice(slashLastIndex + 1);
|
|
82
|
-
}
|
|
81
|
+
if (typeof callback !== "function") {
|
|
82
|
+
throw new Error(`callback must be a function, got ${callback}`);
|
|
83
|
+
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// don't register twice
|
|
86
|
+
const existingCallback = callbacks.find((callbackCandidate) => {
|
|
87
|
+
return callbackCandidate === callback;
|
|
88
|
+
});
|
|
89
|
+
if (existingCallback) {
|
|
90
|
+
emitCallbackDuplicationWarning();
|
|
91
|
+
return removeNoop;
|
|
92
|
+
}
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
callbacks.push(callback);
|
|
95
|
+
return () => {
|
|
96
|
+
if (status === "notified") {
|
|
97
|
+
// once called removing does nothing
|
|
98
|
+
// as the callbacks array is frozen to null
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const index = callbacks.indexOf(callback);
|
|
103
|
+
if (index === -1) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (status === "looping") {
|
|
108
|
+
if (index <= currentCallbackIndex) {
|
|
109
|
+
// The callback was already called (or is the current callback)
|
|
110
|
+
// We don't want to mutate the callbacks array
|
|
111
|
+
// or it would alter the looping done in "call" and the next callback
|
|
112
|
+
// would be skipped
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Callback is part of the next callback to call,
|
|
117
|
+
// we mutate the callbacks array to prevent this callback to be called
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
callbacks.splice(index, 1);
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const notify = (param) => {
|
|
125
|
+
if (status !== "waiting") {
|
|
126
|
+
emitUnexpectedActionWarning({ action: "call", status });
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
status = "looping";
|
|
130
|
+
const values = callbacks.map((callback, index) => {
|
|
131
|
+
currentCallbackIndex = index;
|
|
132
|
+
return callback(param);
|
|
133
|
+
});
|
|
134
|
+
callbackListOnce.notified = true;
|
|
135
|
+
status = "notified";
|
|
136
|
+
// we reset callbacks to null after looping
|
|
137
|
+
// so that it's possible to remove during the loop
|
|
138
|
+
callbacks = null;
|
|
139
|
+
currentCallbackIndex = -1;
|
|
140
|
+
|
|
141
|
+
return values;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
callbackListOnce.notified = false;
|
|
145
|
+
callbackListOnce.add = add;
|
|
146
|
+
callbackListOnce.notify = notify;
|
|
147
|
+
|
|
148
|
+
return callbackListOnce;
|
|
104
149
|
};
|
|
105
150
|
|
|
106
|
-
const
|
|
107
|
-
if (typeof
|
|
108
|
-
|
|
109
|
-
`
|
|
151
|
+
const emitUnexpectedActionWarning = ({ action, status }) => {
|
|
152
|
+
if (typeof process.emitWarning === "function") {
|
|
153
|
+
process.emitWarning(
|
|
154
|
+
`"${action}" should not happen when callback list is ${status}`,
|
|
155
|
+
{
|
|
156
|
+
CODE: "UNEXPECTED_ACTION_ON_CALLBACK_LIST",
|
|
157
|
+
detail: `Code is potentially executed when it should not`,
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
console.warn(
|
|
162
|
+
`"${action}" should not happen when callback list is ${status}`,
|
|
110
163
|
);
|
|
111
164
|
}
|
|
112
|
-
|
|
113
|
-
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const emitCallbackDuplicationWarning = () => {
|
|
168
|
+
if (typeof process.emitWarning === "function") {
|
|
169
|
+
process.emitWarning(`Trying to add a callback already in the list`, {
|
|
170
|
+
CODE: "CALLBACK_DUPLICATION",
|
|
171
|
+
detail: `Code is potentially executed more than it should`,
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
console.warn(`Trying to add same callback twice`);
|
|
114
175
|
}
|
|
115
|
-
return startsWithWindowsDriveLetter(value);
|
|
116
176
|
};
|
|
117
177
|
|
|
118
|
-
const
|
|
119
|
-
const firstChar = string[0];
|
|
120
|
-
if (!/[a-zA-Z]/.test(firstChar)) return false;
|
|
178
|
+
const removeNoop = () => {};
|
|
121
179
|
|
|
122
|
-
|
|
123
|
-
|
|
180
|
+
/*
|
|
181
|
+
* https://github.com/whatwg/dom/issues/920
|
|
182
|
+
*/
|
|
124
183
|
|
|
125
|
-
return true;
|
|
126
|
-
};
|
|
127
184
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
return String(pathToFileURL(value));
|
|
133
|
-
};
|
|
185
|
+
const Abort = {
|
|
186
|
+
isAbortError: (error) => {
|
|
187
|
+
return error && error.name === "AbortError";
|
|
188
|
+
},
|
|
134
189
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
190
|
+
startOperation: () => {
|
|
191
|
+
return createOperation();
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
throwIfAborted: (signal) => {
|
|
195
|
+
if (signal.aborted) {
|
|
196
|
+
const error = new Error(`The operation was aborted`);
|
|
197
|
+
error.name = "AbortError";
|
|
198
|
+
error.type = "aborted";
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
140
202
|
};
|
|
141
203
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let commonPart = "";
|
|
147
|
-
let commonPathname = "";
|
|
148
|
-
let i = 0;
|
|
149
|
-
const length = pathname.length;
|
|
150
|
-
const otherLength = otherPathname.length;
|
|
151
|
-
while (i < length) {
|
|
152
|
-
const char = pathname.charAt(i);
|
|
153
|
-
const otherChar = otherPathname.charAt(i);
|
|
154
|
-
i++;
|
|
155
|
-
if (char === otherChar) {
|
|
156
|
-
if (char === "/") {
|
|
157
|
-
commonPart += "/";
|
|
158
|
-
commonPathname += commonPart;
|
|
159
|
-
commonPart = "";
|
|
160
|
-
} else {
|
|
161
|
-
commonPart += char;
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
if (char === "/" && i - 1 === otherLength) {
|
|
165
|
-
commonPart += "/";
|
|
166
|
-
commonPathname += commonPart;
|
|
167
|
-
}
|
|
168
|
-
return commonPathname;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (length === otherLength) {
|
|
172
|
-
commonPathname += commonPart;
|
|
173
|
-
} else if (otherPathname.charAt(i) === "/") {
|
|
174
|
-
commonPathname += commonPart;
|
|
175
|
-
}
|
|
176
|
-
return commonPathname;
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const urlToRelativeUrl = (url, baseUrl) => {
|
|
180
|
-
const urlObject = new URL(url);
|
|
181
|
-
const baseUrlObject = new URL(baseUrl);
|
|
182
|
-
|
|
183
|
-
if (urlObject.protocol !== baseUrlObject.protocol) {
|
|
184
|
-
const urlAsString = String(url);
|
|
185
|
-
return urlAsString;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
urlObject.username !== baseUrlObject.username ||
|
|
190
|
-
urlObject.password !== baseUrlObject.password ||
|
|
191
|
-
urlObject.host !== baseUrlObject.host
|
|
192
|
-
) {
|
|
193
|
-
const afterUrlScheme = String(url).slice(urlObject.protocol.length);
|
|
194
|
-
return afterUrlScheme;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const { pathname, hash, search } = urlObject;
|
|
198
|
-
if (pathname === "/") {
|
|
199
|
-
const baseUrlResourceWithoutLeadingSlash = baseUrlObject.pathname.slice(1);
|
|
200
|
-
return baseUrlResourceWithoutLeadingSlash;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const basePathname = baseUrlObject.pathname;
|
|
204
|
-
const commonPathname = getCommonPathname(pathname, basePathname);
|
|
205
|
-
if (!commonPathname) {
|
|
206
|
-
const urlAsString = String(url);
|
|
207
|
-
return urlAsString;
|
|
208
|
-
}
|
|
209
|
-
const specificPathname = pathname.slice(commonPathname.length);
|
|
210
|
-
const baseSpecificPathname = basePathname.slice(commonPathname.length);
|
|
211
|
-
if (baseSpecificPathname.includes("/")) {
|
|
212
|
-
const baseSpecificParentPathname =
|
|
213
|
-
pathnameToParentPathname(baseSpecificPathname);
|
|
214
|
-
const relativeDirectoriesNotation = baseSpecificParentPathname.replace(
|
|
215
|
-
/.*?\//g,
|
|
216
|
-
"../",
|
|
217
|
-
);
|
|
218
|
-
const relativeUrl = `${relativeDirectoriesNotation}${specificPathname}${search}${hash}`;
|
|
219
|
-
return relativeUrl;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const relativeUrl = `${specificPathname}${search}${hash}`;
|
|
223
|
-
return relativeUrl;
|
|
224
|
-
};
|
|
204
|
+
const createOperation = () => {
|
|
205
|
+
const operationAbortController = new AbortController();
|
|
206
|
+
// const abortOperation = (value) => abortController.abort(value)
|
|
207
|
+
const operationSignal = operationAbortController.signal;
|
|
225
208
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
209
|
+
// abortCallbackList is used to ignore the max listeners warning from Node.js
|
|
210
|
+
// this warning is useful but becomes problematic when it's expected
|
|
211
|
+
// (a function doing 20 http call in parallel)
|
|
212
|
+
// To be 100% sure we don't have memory leak, only Abortable.asyncCallback
|
|
213
|
+
// uses abortCallbackList to know when something is aborted
|
|
214
|
+
const abortCallbackList = createCallbackListNotifiedOnce();
|
|
215
|
+
const endCallbackList = createCallbackListNotifiedOnce();
|
|
233
216
|
|
|
234
|
-
|
|
235
|
-
let relativeUrl = urlToRelativeUrl(url, from);
|
|
236
|
-
if (relativeUrl.slice(0, 2) === "//") {
|
|
237
|
-
// restore the protocol
|
|
238
|
-
relativeUrl = new URL(relativeUrl, url).href;
|
|
239
|
-
}
|
|
240
|
-
const absoluteUrl = new URL(relativeUrl, to).href;
|
|
241
|
-
if (preferRelative) {
|
|
242
|
-
return urlToRelativeUrl(absoluteUrl, to);
|
|
243
|
-
}
|
|
244
|
-
return absoluteUrl;
|
|
245
|
-
};
|
|
217
|
+
let isAbortAfterEnd = false;
|
|
246
218
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const otherUrlObject = new URL(otherUrl);
|
|
219
|
+
operationSignal.onabort = () => {
|
|
220
|
+
operationSignal.onabort = null;
|
|
250
221
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
222
|
+
const allAbortCallbacksPromise = Promise.all(abortCallbackList.notify());
|
|
223
|
+
if (!isAbortAfterEnd) {
|
|
224
|
+
addEndCallback(async () => {
|
|
225
|
+
await allAbortCallbacksPromise;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
254
229
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
230
|
+
const throwIfAborted = () => {
|
|
231
|
+
Abort.throwIfAborted(operationSignal);
|
|
232
|
+
};
|
|
260
233
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
234
|
+
// add a callback called on abort
|
|
235
|
+
// differences with signal.addEventListener('abort')
|
|
236
|
+
// - operation.end awaits the return value of this callback
|
|
237
|
+
// - It won't increase the count of listeners for "abort" that would
|
|
238
|
+
// trigger max listeners warning when count > 10
|
|
239
|
+
const addAbortCallback = (callback) => {
|
|
240
|
+
// It would be painful and not super redable to check if signal is aborted
|
|
241
|
+
// before deciding if it's an abort or end callback
|
|
242
|
+
// with pseudo-code below where we want to stop server either
|
|
243
|
+
// on abort or when ended because signal is aborted
|
|
244
|
+
// operation[operation.signal.aborted ? 'addAbortCallback': 'addEndCallback'](async () => {
|
|
245
|
+
// await server.stop()
|
|
246
|
+
// })
|
|
247
|
+
if (operationSignal.aborted) {
|
|
248
|
+
return addEndCallback(callback);
|
|
249
|
+
}
|
|
250
|
+
return abortCallbackList.add(callback);
|
|
251
|
+
};
|
|
264
252
|
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// remove trailing / so that nodejs path becomes predictable otherwise it logs
|
|
269
|
-
// the trailing slash on linux but does not on windows
|
|
270
|
-
urlString = urlString.slice(0, -1);
|
|
271
|
-
}
|
|
272
|
-
const fileSystemPath = fileURLToPath(urlString);
|
|
273
|
-
return fileSystemPath;
|
|
274
|
-
};
|
|
253
|
+
const addEndCallback = (callback) => {
|
|
254
|
+
return endCallbackList.add(callback);
|
|
255
|
+
};
|
|
275
256
|
|
|
276
|
-
const
|
|
277
|
-
|
|
257
|
+
const end = async ({ abortAfterEnd = false } = {}) => {
|
|
258
|
+
await Promise.all(endCallbackList.notify());
|
|
278
259
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
message: `must be a valid url`,
|
|
292
|
-
};
|
|
260
|
+
// "abortAfterEnd" can be handy to ensure "abort" callbacks
|
|
261
|
+
// added with { once: true } are removed
|
|
262
|
+
// It might also help garbage collection because
|
|
263
|
+
// runtime implementing AbortSignal (Node.js, browsers) can consider abortSignal
|
|
264
|
+
// as settled and clean up things
|
|
265
|
+
if (abortAfterEnd) {
|
|
266
|
+
// because of operationSignal.onabort = null
|
|
267
|
+
// + abortCallbackList.clear() this won't re-call
|
|
268
|
+
// callbacks
|
|
269
|
+
if (!operationSignal.aborted) {
|
|
270
|
+
isAbortAfterEnd = true;
|
|
271
|
+
operationAbortController.abort();
|
|
293
272
|
}
|
|
294
273
|
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const addAbortSignal = (
|
|
277
|
+
signal,
|
|
278
|
+
{ onAbort = callbackNoop, onRemove = callbackNoop } = {},
|
|
279
|
+
) => {
|
|
280
|
+
const applyAbortEffects = () => {
|
|
281
|
+
const onAbortCallback = onAbort;
|
|
282
|
+
onAbort = callbackNoop;
|
|
283
|
+
onAbortCallback();
|
|
300
284
|
};
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
message: 'must start with "file://"',
|
|
285
|
+
const applyRemoveEffects = () => {
|
|
286
|
+
const onRemoveCallback = onRemove;
|
|
287
|
+
onRemove = callbackNoop;
|
|
288
|
+
onAbort = callbackNoop;
|
|
289
|
+
onRemoveCallback();
|
|
307
290
|
};
|
|
308
|
-
}
|
|
309
|
-
return {
|
|
310
|
-
valid: true,
|
|
311
|
-
value: ensurePathnameTrailingSlash(urlString),
|
|
312
|
-
};
|
|
313
|
-
};
|
|
314
291
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (!valid) {
|
|
321
|
-
throw new TypeError(`${name} ${message}, got ${value}`);
|
|
322
|
-
}
|
|
323
|
-
return value;
|
|
324
|
-
};
|
|
292
|
+
if (operationSignal.aborted) {
|
|
293
|
+
applyAbortEffects();
|
|
294
|
+
applyRemoveEffects();
|
|
295
|
+
return callbackNoop;
|
|
296
|
+
}
|
|
325
297
|
|
|
326
|
-
|
|
327
|
-
|
|
298
|
+
if (signal.aborted) {
|
|
299
|
+
operationAbortController.abort();
|
|
300
|
+
applyAbortEffects();
|
|
301
|
+
applyRemoveEffects();
|
|
302
|
+
return callbackNoop;
|
|
303
|
+
}
|
|
328
304
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
305
|
+
const cancelRace = raceCallbacks(
|
|
306
|
+
{
|
|
307
|
+
operation_abort: (cb) => {
|
|
308
|
+
return addAbortCallback(cb);
|
|
309
|
+
},
|
|
310
|
+
operation_end: (cb) => {
|
|
311
|
+
return addEndCallback(cb);
|
|
312
|
+
},
|
|
313
|
+
child_abort: (cb) => {
|
|
314
|
+
return addEventListener(signal, "abort", cb);
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
(winner) => {
|
|
318
|
+
const raceEffects = {
|
|
319
|
+
// Both "operation_abort" and "operation_end"
|
|
320
|
+
// means we don't care anymore if the child aborts.
|
|
321
|
+
// So we can:
|
|
322
|
+
// - remove "abort" event listener on child (done by raceCallback)
|
|
323
|
+
// - remove abort callback on operation (done by raceCallback)
|
|
324
|
+
// - remove end callback on operation (done by raceCallback)
|
|
325
|
+
// - call any custom cancel function
|
|
326
|
+
operation_abort: () => {
|
|
327
|
+
applyAbortEffects();
|
|
328
|
+
applyRemoveEffects();
|
|
329
|
+
},
|
|
330
|
+
operation_end: () => {
|
|
331
|
+
// Exists to
|
|
332
|
+
// - remove abort callback on operation
|
|
333
|
+
// - remove "abort" event listener on child
|
|
334
|
+
// - call any custom cancel function
|
|
335
|
+
applyRemoveEffects();
|
|
336
|
+
},
|
|
337
|
+
child_abort: () => {
|
|
338
|
+
applyAbortEffects();
|
|
339
|
+
operationAbortController.abort();
|
|
340
|
+
},
|
|
342
341
|
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
valid: false,
|
|
348
|
-
value,
|
|
349
|
-
message: "must be a string or an url",
|
|
350
|
-
};
|
|
351
|
-
}
|
|
342
|
+
raceEffects[winner.name](winner.value);
|
|
343
|
+
},
|
|
344
|
+
);
|
|
352
345
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
value,
|
|
357
|
-
message: 'must start with "file://"',
|
|
346
|
+
return () => {
|
|
347
|
+
cancelRace();
|
|
348
|
+
applyRemoveEffects();
|
|
358
349
|
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
valid: true,
|
|
363
|
-
value: urlString,
|
|
364
350
|
};
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const assertAndNormalizeFileUrl = (
|
|
368
|
-
fileUrl,
|
|
369
|
-
baseUrl,
|
|
370
|
-
name = "fileUrl",
|
|
371
|
-
) => {
|
|
372
|
-
const { valid, message, value } = validateFileUrl(fileUrl, baseUrl);
|
|
373
|
-
if (!valid) {
|
|
374
|
-
throw new TypeError(`${name} ${message}, got ${fileUrl}`);
|
|
375
|
-
}
|
|
376
|
-
return value;
|
|
377
|
-
};
|
|
378
351
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
352
|
+
const addAbortSource = (abortSourceCallback) => {
|
|
353
|
+
const abortSource = {
|
|
354
|
+
cleaned: false,
|
|
355
|
+
signal: null,
|
|
356
|
+
remove: callbackNoop,
|
|
357
|
+
};
|
|
358
|
+
const abortSourceController = new AbortController();
|
|
359
|
+
const abortSourceSignal = abortSourceController.signal;
|
|
360
|
+
abortSource.signal = abortSourceSignal;
|
|
361
|
+
if (operationSignal.aborted) {
|
|
362
|
+
return abortSource;
|
|
363
|
+
}
|
|
364
|
+
const returnValue = abortSourceCallback((value) => {
|
|
365
|
+
abortSourceController.abort(value);
|
|
366
|
+
});
|
|
367
|
+
const removeAbortSignal = addAbortSignal(abortSourceSignal, {
|
|
368
|
+
onRemove: () => {
|
|
369
|
+
if (typeof returnValue === "function") {
|
|
370
|
+
returnValue();
|
|
371
|
+
}
|
|
372
|
+
abortSource.cleaned = true;
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
abortSource.remove = removeAbortSignal;
|
|
376
|
+
return abortSource;
|
|
377
|
+
};
|
|
389
378
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
379
|
+
const timeout = (ms) => {
|
|
380
|
+
return addAbortSource((abort) => {
|
|
381
|
+
const timeoutId = setTimeout(abort, ms);
|
|
382
|
+
// an abort source return value is called when:
|
|
383
|
+
// - operation is aborted (by an other source)
|
|
384
|
+
// - operation ends
|
|
385
|
+
return () => {
|
|
386
|
+
clearTimeout(timeoutId);
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
};
|
|
393
390
|
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
391
|
+
const withSignal = async (asyncCallback) => {
|
|
392
|
+
const abortController = new AbortController();
|
|
393
|
+
const signal = abortController.signal;
|
|
394
|
+
const removeAbortSignal = addAbortSignal(signal, {
|
|
395
|
+
onAbort: () => {
|
|
396
|
+
abortController.abort();
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
try {
|
|
400
|
+
const value = await asyncCallback(signal);
|
|
401
|
+
removeAbortSignal();
|
|
402
|
+
return value;
|
|
403
|
+
} catch (e) {
|
|
404
|
+
removeAbortSignal();
|
|
405
|
+
throw e;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
404
408
|
|
|
405
|
-
const
|
|
406
|
-
|
|
409
|
+
const withSignalSync = (callback) => {
|
|
410
|
+
const abortController = new AbortController();
|
|
411
|
+
const signal = abortController.signal;
|
|
412
|
+
const removeAbortSignal = addAbortSignal(signal, {
|
|
413
|
+
onAbort: () => {
|
|
414
|
+
abortController.abort();
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
const value = callback(signal);
|
|
419
|
+
removeAbortSignal();
|
|
420
|
+
return value;
|
|
421
|
+
} catch (e) {
|
|
422
|
+
removeAbortSignal();
|
|
423
|
+
throw e;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
407
426
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
427
|
+
return {
|
|
428
|
+
// We could almost hide the operationSignal
|
|
429
|
+
// But it can be handy for 2 things:
|
|
430
|
+
// - know if operation is aborted (operation.signal.aborted)
|
|
431
|
+
// - forward the operation.signal directly (not using "withSignal" or "withSignalSync")
|
|
432
|
+
signal: operationSignal,
|
|
411
433
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
434
|
+
throwIfAborted,
|
|
435
|
+
addAbortCallback,
|
|
436
|
+
addAbortSignal,
|
|
437
|
+
addAbortSource,
|
|
438
|
+
timeout,
|
|
439
|
+
withSignal,
|
|
440
|
+
withSignalSync,
|
|
441
|
+
addEndCallback,
|
|
442
|
+
end,
|
|
443
|
+
};
|
|
444
|
+
};
|
|
415
445
|
|
|
416
|
-
|
|
417
|
-
if (others.write) binaryFlags |= S_IWOTH;
|
|
418
|
-
if (others.execute) binaryFlags |= S_IXOTH;
|
|
446
|
+
const callbackNoop = () => {};
|
|
419
447
|
|
|
420
|
-
|
|
448
|
+
const addEventListener = (target, eventName, cb) => {
|
|
449
|
+
target.addEventListener(eventName, cb);
|
|
450
|
+
return () => {
|
|
451
|
+
target.removeEventListener(eventName, cb);
|
|
452
|
+
};
|
|
421
453
|
};
|
|
422
454
|
|
|
423
|
-
const
|
|
424
|
-
|
|
455
|
+
const raceProcessTeardownEvents = (processTeardownEvents, callback) => {
|
|
456
|
+
return raceCallbacks(
|
|
457
|
+
{
|
|
458
|
+
...(processTeardownEvents.SIGHUP ? SIGHUP_CALLBACK : {}),
|
|
459
|
+
...(processTeardownEvents.SIGTERM ? SIGTERM_CALLBACK : {}),
|
|
460
|
+
...(processTeardownEvents.SIGINT ? SIGINT_CALLBACK : {}),
|
|
461
|
+
...(processTeardownEvents.beforeExit ? BEFORE_EXIT_CALLBACK : {}),
|
|
462
|
+
...(processTeardownEvents.exit ? EXIT_CALLBACK : {}),
|
|
463
|
+
},
|
|
464
|
+
callback,
|
|
465
|
+
);
|
|
466
|
+
};
|
|
425
467
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
write: getPermissionOrComputeDefault("write", "owner", permissions),
|
|
432
|
-
execute: getPermissionOrComputeDefault("execute", "owner", permissions),
|
|
433
|
-
},
|
|
434
|
-
group: {
|
|
435
|
-
read: getPermissionOrComputeDefault("read", "group", permissions),
|
|
436
|
-
write: getPermissionOrComputeDefault("write", "group", permissions),
|
|
437
|
-
execute: getPermissionOrComputeDefault("execute", "group", permissions),
|
|
438
|
-
},
|
|
439
|
-
others: {
|
|
440
|
-
read: getPermissionOrComputeDefault("read", "others", permissions),
|
|
441
|
-
write: getPermissionOrComputeDefault("write", "others", permissions),
|
|
442
|
-
execute: getPermissionOrComputeDefault(
|
|
443
|
-
"execute",
|
|
444
|
-
"others",
|
|
445
|
-
permissions,
|
|
446
|
-
),
|
|
447
|
-
},
|
|
468
|
+
const SIGHUP_CALLBACK = {
|
|
469
|
+
SIGHUP: (cb) => {
|
|
470
|
+
process.on("SIGHUP", cb);
|
|
471
|
+
return () => {
|
|
472
|
+
process.removeListener("SIGHUP", cb);
|
|
448
473
|
};
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
binaryFlags = permissions;
|
|
452
|
-
}
|
|
474
|
+
},
|
|
475
|
+
};
|
|
453
476
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
});
|
|
462
|
-
});
|
|
477
|
+
const SIGTERM_CALLBACK = {
|
|
478
|
+
SIGTERM: (cb) => {
|
|
479
|
+
process.on("SIGTERM", cb);
|
|
480
|
+
return () => {
|
|
481
|
+
process.removeListener("SIGTERM", cb);
|
|
482
|
+
};
|
|
483
|
+
},
|
|
463
484
|
};
|
|
464
485
|
|
|
465
|
-
const
|
|
466
|
-
|
|
486
|
+
const BEFORE_EXIT_CALLBACK = {
|
|
487
|
+
beforeExit: (cb) => {
|
|
488
|
+
process.on("beforeExit", cb);
|
|
489
|
+
return () => {
|
|
490
|
+
process.removeListener("beforeExit", cb);
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
};
|
|
467
494
|
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
495
|
+
const EXIT_CALLBACK = {
|
|
496
|
+
exit: (cb) => {
|
|
497
|
+
process.on("exit", cb);
|
|
498
|
+
return () => {
|
|
499
|
+
process.removeListener("exit", cb);
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
};
|
|
474
503
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
}
|
|
504
|
+
const SIGINT_CALLBACK = {
|
|
505
|
+
SIGINT: (cb) => {
|
|
506
|
+
process.on("SIGINT", cb);
|
|
507
|
+
return () => {
|
|
508
|
+
process.removeListener("SIGINT", cb);
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
};
|
|
485
512
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
subjectLevels[subjectFallbackCandidate] > subjectLevel &&
|
|
492
|
-
subjectFallbackCandidate in permissions,
|
|
493
|
-
);
|
|
494
|
-
if (subjectFallback) {
|
|
495
|
-
const subjectPermissions = permissions[subjectFallback];
|
|
496
|
-
return action in subjectPermissions
|
|
497
|
-
? subjectPermissions[action]
|
|
498
|
-
: getPermissionOrComputeDefault(action, subjectFallback, permissions);
|
|
513
|
+
const urlToScheme = (url) => {
|
|
514
|
+
const urlString = String(url);
|
|
515
|
+
const colonIndex = urlString.indexOf(":");
|
|
516
|
+
if (colonIndex === -1) {
|
|
517
|
+
return "";
|
|
499
518
|
}
|
|
500
519
|
|
|
501
|
-
|
|
520
|
+
const scheme = urlString.slice(0, colonIndex);
|
|
521
|
+
return scheme;
|
|
502
522
|
};
|
|
503
523
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
* https://nodejs.org/docs/latest-v13.x/api/fs.html#fs_class_fs_stats
|
|
507
|
-
*/
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const isWindows$2 = process.platform === "win32";
|
|
511
|
-
|
|
512
|
-
const readEntryStat = async (
|
|
513
|
-
source,
|
|
514
|
-
{ nullIfNotFound = false, followLink = true } = {},
|
|
515
|
-
) => {
|
|
516
|
-
let sourceUrl = assertAndNormalizeFileUrl(source);
|
|
517
|
-
if (sourceUrl.endsWith("/")) sourceUrl = sourceUrl.slice(0, -1);
|
|
518
|
-
|
|
519
|
-
const sourcePath = urlToFileSystemPath(sourceUrl);
|
|
524
|
+
const urlToResource = (url) => {
|
|
525
|
+
const scheme = urlToScheme(url);
|
|
520
526
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
: {};
|
|
527
|
+
if (scheme === "file") {
|
|
528
|
+
const urlAsStringWithoutFileProtocol = String(url).slice("file://".length);
|
|
529
|
+
return urlAsStringWithoutFileProtocol;
|
|
530
|
+
}
|
|
526
531
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
console.error(
|
|
535
|
-
`trying to fix windows EPERM after stats on ${sourcePath}`,
|
|
536
|
-
);
|
|
532
|
+
if (scheme === "https" || scheme === "http") {
|
|
533
|
+
// remove origin
|
|
534
|
+
const afterProtocol = String(url).slice(scheme.length + "://".length);
|
|
535
|
+
const pathnameSlashIndex = afterProtocol.indexOf("/", "://".length);
|
|
536
|
+
const urlAsStringWithoutOrigin = afterProtocol.slice(pathnameSlashIndex);
|
|
537
|
+
return urlAsStringWithoutOrigin;
|
|
538
|
+
}
|
|
537
539
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
// without being able to restore them to the previous value
|
|
541
|
-
// (because reading current permission would also throw)
|
|
542
|
-
await writeEntryPermissions(sourceUrl, 0o666);
|
|
543
|
-
const stats = await readStat(sourcePath, {
|
|
544
|
-
followLink,
|
|
545
|
-
...handleNotFoundOption,
|
|
546
|
-
// could not fix the permission error, give up and throw original error
|
|
547
|
-
handlePermissionDeniedError: () => {
|
|
548
|
-
console.error(`still got EPERM after stats on ${sourcePath}`);
|
|
549
|
-
throw error;
|
|
550
|
-
},
|
|
551
|
-
});
|
|
552
|
-
return stats;
|
|
553
|
-
} catch (e) {
|
|
554
|
-
console.error(
|
|
555
|
-
`error while trying to fix windows EPERM after stats on ${sourcePath}: ${e.stack}`,
|
|
556
|
-
);
|
|
557
|
-
throw error;
|
|
558
|
-
}
|
|
559
|
-
},
|
|
560
|
-
}
|
|
561
|
-
: {}),
|
|
562
|
-
});
|
|
540
|
+
const urlAsStringWithoutProtocol = String(url).slice(scheme.length + 1);
|
|
541
|
+
return urlAsStringWithoutProtocol;
|
|
563
542
|
};
|
|
564
543
|
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
handleNotFoundError = null,
|
|
570
|
-
handlePermissionDeniedError = null,
|
|
571
|
-
} = {},
|
|
572
|
-
) => {
|
|
573
|
-
const nodeMethod = followLink ? stat : lstat;
|
|
574
|
-
|
|
575
|
-
return new Promise((resolve, reject) => {
|
|
576
|
-
nodeMethod(sourcePath, (error, statsObject) => {
|
|
577
|
-
if (error) {
|
|
578
|
-
if (handleNotFoundError && error.code === "ENOENT") {
|
|
579
|
-
resolve(handleNotFoundError(error));
|
|
580
|
-
} else if (
|
|
581
|
-
handlePermissionDeniedError &&
|
|
582
|
-
(error.code === "EPERM" || error.code === "EACCES")
|
|
583
|
-
) {
|
|
584
|
-
resolve(handlePermissionDeniedError(error));
|
|
585
|
-
} else {
|
|
586
|
-
reject(error);
|
|
587
|
-
}
|
|
588
|
-
} else {
|
|
589
|
-
resolve(statsObject);
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
});
|
|
544
|
+
const urlToPathname = (url) => {
|
|
545
|
+
const resource = urlToResource(url);
|
|
546
|
+
const pathname = resourceToPathname(resource);
|
|
547
|
+
return pathname;
|
|
593
548
|
};
|
|
594
549
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
550
|
+
const resourceToPathname = (resource) => {
|
|
551
|
+
const searchSeparatorIndex = resource.indexOf("?");
|
|
552
|
+
if (searchSeparatorIndex > -1) {
|
|
553
|
+
return resource.slice(0, searchSeparatorIndex);
|
|
554
|
+
}
|
|
555
|
+
const hashIndex = resource.indexOf("#");
|
|
556
|
+
if (hashIndex > -1) {
|
|
557
|
+
return resource.slice(0, hashIndex);
|
|
558
|
+
}
|
|
559
|
+
return resource;
|
|
560
|
+
};
|
|
598
561
|
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
562
|
+
const urlToExtension = (url) => {
|
|
563
|
+
const pathname = urlToPathname(url);
|
|
564
|
+
return pathnameToExtension(pathname);
|
|
565
|
+
};
|
|
602
566
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
};
|
|
567
|
+
const pathnameToExtension = (pathname) => {
|
|
568
|
+
const slashLastIndex = pathname.lastIndexOf("/");
|
|
569
|
+
if (slashLastIndex !== -1) {
|
|
570
|
+
pathname = pathname.slice(slashLastIndex + 1);
|
|
571
|
+
}
|
|
609
572
|
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
};
|
|
573
|
+
const dotLastIndex = pathname.lastIndexOf(".");
|
|
574
|
+
if (dotLastIndex === -1) return "";
|
|
575
|
+
// if (dotLastIndex === pathname.length - 1) return ""
|
|
576
|
+
const extension = pathname.slice(dotLastIndex);
|
|
577
|
+
return extension;
|
|
578
|
+
};
|
|
617
579
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
cleanCallbacks.push(returnValue);
|
|
633
|
-
}
|
|
634
|
-
});
|
|
580
|
+
const ensurePathnameTrailingSlash = (url) => {
|
|
581
|
+
const urlObject = new URL(url);
|
|
582
|
+
const { pathname } = urlObject;
|
|
583
|
+
if (pathname.endsWith("/")) {
|
|
584
|
+
return url;
|
|
585
|
+
}
|
|
586
|
+
let { origin } = urlObject;
|
|
587
|
+
// origin is "null" for "file://" urls with Node.js
|
|
588
|
+
if (origin === "null" && urlObject.href.startsWith("file:")) {
|
|
589
|
+
origin = "file://";
|
|
590
|
+
}
|
|
591
|
+
const { search, hash } = urlObject;
|
|
592
|
+
return `${origin}${pathname}/${search}${hash}`;
|
|
593
|
+
};
|
|
635
594
|
|
|
636
|
-
|
|
595
|
+
const isFileSystemPath = (value) => {
|
|
596
|
+
if (typeof value !== "string") {
|
|
597
|
+
throw new TypeError(
|
|
598
|
+
`isFileSystemPath first arg must be a string, got ${value}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
if (value[0] === "/") {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
return startsWithWindowsDriveLetter(value);
|
|
637
605
|
};
|
|
638
606
|
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
let currentCallbackIndex = -1;
|
|
607
|
+
const startsWithWindowsDriveLetter = (string) => {
|
|
608
|
+
const firstChar = string[0];
|
|
609
|
+
if (!/[a-zA-Z]/.test(firstChar)) return false;
|
|
643
610
|
|
|
644
|
-
const
|
|
611
|
+
const secondChar = string[1];
|
|
612
|
+
if (secondChar !== ":") return false;
|
|
645
613
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
emitUnexpectedActionWarning({ action: "add", status });
|
|
649
|
-
return removeNoop;
|
|
650
|
-
}
|
|
614
|
+
return true;
|
|
615
|
+
};
|
|
651
616
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
617
|
+
const fileSystemPathToUrl = (value) => {
|
|
618
|
+
if (!isFileSystemPath(value)) {
|
|
619
|
+
throw new Error(`value must be a filesystem path, got ${value}`);
|
|
620
|
+
}
|
|
621
|
+
return String(pathToFileURL(value));
|
|
622
|
+
};
|
|
655
623
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return removeNoop;
|
|
663
|
-
}
|
|
624
|
+
const resolveUrl = (specifier, baseUrl) => {
|
|
625
|
+
if (typeof baseUrl === "undefined") {
|
|
626
|
+
throw new TypeError(`baseUrl missing to resolve ${specifier}`);
|
|
627
|
+
}
|
|
628
|
+
return String(new URL(specifier, baseUrl));
|
|
629
|
+
};
|
|
664
630
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
631
|
+
const getCommonPathname = (pathname, otherPathname) => {
|
|
632
|
+
if (pathname === otherPathname) {
|
|
633
|
+
return pathname;
|
|
634
|
+
}
|
|
635
|
+
let commonPart = "";
|
|
636
|
+
let commonPathname = "";
|
|
637
|
+
let i = 0;
|
|
638
|
+
const length = pathname.length;
|
|
639
|
+
const otherLength = otherPathname.length;
|
|
640
|
+
while (i < length) {
|
|
641
|
+
const char = pathname.charAt(i);
|
|
642
|
+
const otherChar = otherPathname.charAt(i);
|
|
643
|
+
i++;
|
|
644
|
+
if (char === otherChar) {
|
|
645
|
+
if (char === "/") {
|
|
646
|
+
commonPart += "/";
|
|
647
|
+
commonPathname += commonPart;
|
|
648
|
+
commonPart = "";
|
|
649
|
+
} else {
|
|
650
|
+
commonPart += char;
|
|
671
651
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
652
|
+
} else {
|
|
653
|
+
if (char === "/" && i - 1 === otherLength) {
|
|
654
|
+
commonPart += "/";
|
|
655
|
+
commonPathname += commonPart;
|
|
676
656
|
}
|
|
657
|
+
return commonPathname;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (length === otherLength) {
|
|
661
|
+
commonPathname += commonPart;
|
|
662
|
+
} else if (otherPathname.charAt(i) === "/") {
|
|
663
|
+
commonPathname += commonPart;
|
|
664
|
+
}
|
|
665
|
+
return commonPathname;
|
|
666
|
+
};
|
|
677
667
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
// We don't want to mutate the callbacks array
|
|
682
|
-
// or it would alter the looping done in "call" and the next callback
|
|
683
|
-
// would be skipped
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
668
|
+
const urlToRelativeUrl = (url, baseUrl) => {
|
|
669
|
+
const urlObject = new URL(url);
|
|
670
|
+
const baseUrlObject = new URL(baseUrl);
|
|
686
671
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
672
|
+
if (urlObject.protocol !== baseUrlObject.protocol) {
|
|
673
|
+
const urlAsString = String(url);
|
|
674
|
+
return urlAsString;
|
|
675
|
+
}
|
|
690
676
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
677
|
+
if (
|
|
678
|
+
urlObject.username !== baseUrlObject.username ||
|
|
679
|
+
urlObject.password !== baseUrlObject.password ||
|
|
680
|
+
urlObject.host !== baseUrlObject.host
|
|
681
|
+
) {
|
|
682
|
+
const afterUrlScheme = String(url).slice(urlObject.protocol.length);
|
|
683
|
+
return afterUrlScheme;
|
|
684
|
+
}
|
|
694
685
|
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
status = "looping";
|
|
701
|
-
const values = callbacks.map((callback, index) => {
|
|
702
|
-
currentCallbackIndex = index;
|
|
703
|
-
return callback(param);
|
|
704
|
-
});
|
|
705
|
-
callbackListOnce.notified = true;
|
|
706
|
-
status = "notified";
|
|
707
|
-
// we reset callbacks to null after looping
|
|
708
|
-
// so that it's possible to remove during the loop
|
|
709
|
-
callbacks = null;
|
|
710
|
-
currentCallbackIndex = -1;
|
|
686
|
+
const { pathname, hash, search } = urlObject;
|
|
687
|
+
if (pathname === "/") {
|
|
688
|
+
const baseUrlResourceWithoutLeadingSlash = baseUrlObject.pathname.slice(1);
|
|
689
|
+
return baseUrlResourceWithoutLeadingSlash;
|
|
690
|
+
}
|
|
711
691
|
|
|
712
|
-
|
|
713
|
-
|
|
692
|
+
const basePathname = baseUrlObject.pathname;
|
|
693
|
+
const commonPathname = getCommonPathname(pathname, basePathname);
|
|
694
|
+
if (!commonPathname) {
|
|
695
|
+
const urlAsString = String(url);
|
|
696
|
+
return urlAsString;
|
|
697
|
+
}
|
|
698
|
+
const specificPathname = pathname.slice(commonPathname.length);
|
|
699
|
+
const baseSpecificPathname = basePathname.slice(commonPathname.length);
|
|
700
|
+
if (baseSpecificPathname.includes("/")) {
|
|
701
|
+
const baseSpecificParentPathname =
|
|
702
|
+
pathnameToParentPathname(baseSpecificPathname);
|
|
703
|
+
const relativeDirectoriesNotation = baseSpecificParentPathname.replace(
|
|
704
|
+
/.*?\//g,
|
|
705
|
+
"../",
|
|
706
|
+
);
|
|
707
|
+
const relativeUrl = `${relativeDirectoriesNotation}${specificPathname}${search}${hash}`;
|
|
708
|
+
return relativeUrl;
|
|
709
|
+
}
|
|
714
710
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
711
|
+
const relativeUrl = `${specificPathname}${search}${hash}`;
|
|
712
|
+
return relativeUrl;
|
|
713
|
+
};
|
|
718
714
|
|
|
719
|
-
|
|
715
|
+
const pathnameToParentPathname = (pathname) => {
|
|
716
|
+
const slashLastIndex = pathname.lastIndexOf("/");
|
|
717
|
+
if (slashLastIndex === -1) {
|
|
718
|
+
return "/";
|
|
719
|
+
}
|
|
720
|
+
return pathname.slice(0, slashLastIndex + 1);
|
|
720
721
|
};
|
|
721
722
|
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
);
|
|
731
|
-
} else {
|
|
732
|
-
console.warn(
|
|
733
|
-
`"${action}" should not happen when callback list is ${status}`,
|
|
734
|
-
);
|
|
723
|
+
const moveUrl = ({ url, from, to, preferRelative }) => {
|
|
724
|
+
let relativeUrl = urlToRelativeUrl(url, from);
|
|
725
|
+
if (relativeUrl.slice(0, 2) === "//") {
|
|
726
|
+
// restore the protocol
|
|
727
|
+
relativeUrl = new URL(relativeUrl, url).href;
|
|
728
|
+
}
|
|
729
|
+
const absoluteUrl = new URL(relativeUrl, to).href;
|
|
730
|
+
if (preferRelative) {
|
|
731
|
+
return urlToRelativeUrl(absoluteUrl, to);
|
|
735
732
|
}
|
|
733
|
+
return absoluteUrl;
|
|
736
734
|
};
|
|
737
735
|
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
CODE: "CALLBACK_DUPLICATION",
|
|
742
|
-
detail: `Code is potentially executed more than it should`,
|
|
743
|
-
});
|
|
744
|
-
} else {
|
|
745
|
-
console.warn(`Trying to add same callback twice`);
|
|
746
|
-
}
|
|
747
|
-
};
|
|
736
|
+
const urlIsInsideOf = (url, otherUrl) => {
|
|
737
|
+
const urlObject = new URL(url);
|
|
738
|
+
const otherUrlObject = new URL(otherUrl);
|
|
748
739
|
|
|
749
|
-
|
|
740
|
+
if (urlObject.origin !== otherUrlObject.origin) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
750
743
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
744
|
+
const urlPathname = urlObject.pathname;
|
|
745
|
+
const otherUrlPathname = otherUrlObject.pathname;
|
|
746
|
+
if (urlPathname === otherUrlPathname) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
754
749
|
|
|
750
|
+
const isInside = urlPathname.startsWith(otherUrlPathname);
|
|
751
|
+
return isInside;
|
|
752
|
+
};
|
|
755
753
|
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
754
|
+
const urlToFileSystemPath = (url) => {
|
|
755
|
+
let urlString = String(url);
|
|
756
|
+
if (urlString[urlString.length - 1] === "/") {
|
|
757
|
+
// remove trailing / so that nodejs path becomes predictable otherwise it logs
|
|
758
|
+
// the trailing slash on linux but does not on windows
|
|
759
|
+
urlString = urlString.slice(0, -1);
|
|
760
|
+
}
|
|
761
|
+
const fileSystemPath = fileURLToPath(urlString);
|
|
762
|
+
return fileSystemPath;
|
|
763
|
+
};
|
|
760
764
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
},
|
|
765
|
+
const validateDirectoryUrl = (value) => {
|
|
766
|
+
let urlString;
|
|
764
767
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
768
|
+
if (value instanceof URL) {
|
|
769
|
+
urlString = value.href;
|
|
770
|
+
} else if (typeof value === "string") {
|
|
771
|
+
if (isFileSystemPath(value)) {
|
|
772
|
+
urlString = fileSystemPathToUrl(value);
|
|
773
|
+
} else {
|
|
774
|
+
try {
|
|
775
|
+
urlString = String(new URL(value));
|
|
776
|
+
} catch (e) {
|
|
777
|
+
return {
|
|
778
|
+
valid: false,
|
|
779
|
+
value,
|
|
780
|
+
message: `must be a valid url`,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
771
783
|
}
|
|
772
|
-
}
|
|
784
|
+
} else {
|
|
785
|
+
return {
|
|
786
|
+
valid: false,
|
|
787
|
+
value,
|
|
788
|
+
message: `must be a string or an url`,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
if (!urlString.startsWith("file://")) {
|
|
792
|
+
return {
|
|
793
|
+
valid: false,
|
|
794
|
+
value,
|
|
795
|
+
message: 'must start with "file://"',
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
valid: true,
|
|
800
|
+
value: ensurePathnameTrailingSlash(urlString),
|
|
801
|
+
};
|
|
773
802
|
};
|
|
774
803
|
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
804
|
+
const assertAndNormalizeDirectoryUrl = (
|
|
805
|
+
directoryUrl,
|
|
806
|
+
name = "directoryUrl",
|
|
807
|
+
) => {
|
|
808
|
+
const { valid, message, value } = validateDirectoryUrl(directoryUrl);
|
|
809
|
+
if (!valid) {
|
|
810
|
+
throw new TypeError(`${name} ${message}, got ${value}`);
|
|
811
|
+
}
|
|
812
|
+
return value;
|
|
813
|
+
};
|
|
779
814
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
// (a function doing 20 http call in parallel)
|
|
783
|
-
// To be 100% sure we don't have memory leak, only Abortable.asyncCallback
|
|
784
|
-
// uses abortCallbackList to know when something is aborted
|
|
785
|
-
const abortCallbackList = createCallbackListNotifiedOnce();
|
|
786
|
-
const endCallbackList = createCallbackListNotifiedOnce();
|
|
815
|
+
const validateFileUrl = (value, baseUrl) => {
|
|
816
|
+
let urlString;
|
|
787
817
|
|
|
788
|
-
|
|
818
|
+
if (value instanceof URL) {
|
|
819
|
+
urlString = value.href;
|
|
820
|
+
} else if (typeof value === "string") {
|
|
821
|
+
if (isFileSystemPath(value)) {
|
|
822
|
+
urlString = fileSystemPathToUrl(value);
|
|
823
|
+
} else {
|
|
824
|
+
try {
|
|
825
|
+
urlString = String(new URL(value, baseUrl));
|
|
826
|
+
} catch (e) {
|
|
827
|
+
return {
|
|
828
|
+
valid: false,
|
|
829
|
+
value,
|
|
830
|
+
message: "must be a valid url",
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
return {
|
|
836
|
+
valid: false,
|
|
837
|
+
value,
|
|
838
|
+
message: "must be a string or an url",
|
|
839
|
+
};
|
|
840
|
+
}
|
|
789
841
|
|
|
790
|
-
|
|
791
|
-
|
|
842
|
+
if (!urlString.startsWith("file://")) {
|
|
843
|
+
return {
|
|
844
|
+
valid: false,
|
|
845
|
+
value,
|
|
846
|
+
message: 'must start with "file://"',
|
|
847
|
+
};
|
|
848
|
+
}
|
|
792
849
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
await allAbortCallbacksPromise;
|
|
797
|
-
});
|
|
798
|
-
}
|
|
850
|
+
return {
|
|
851
|
+
valid: true,
|
|
852
|
+
value: urlString,
|
|
799
853
|
};
|
|
854
|
+
};
|
|
800
855
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
856
|
+
const assertAndNormalizeFileUrl = (
|
|
857
|
+
fileUrl,
|
|
858
|
+
baseUrl,
|
|
859
|
+
name = "fileUrl",
|
|
860
|
+
) => {
|
|
861
|
+
const { valid, message, value } = validateFileUrl(fileUrl, baseUrl);
|
|
862
|
+
if (!valid) {
|
|
863
|
+
throw new TypeError(`${name} ${message}, got ${fileUrl}`);
|
|
864
|
+
}
|
|
865
|
+
return value;
|
|
866
|
+
};
|
|
804
867
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
// operation[operation.signal.aborted ? 'addAbortCallback': 'addEndCallback'](async () => {
|
|
816
|
-
// await server.stop()
|
|
817
|
-
// })
|
|
818
|
-
if (operationSignal.aborted) {
|
|
819
|
-
return addEndCallback(callback);
|
|
820
|
-
}
|
|
821
|
-
return abortCallbackList.add(callback);
|
|
822
|
-
};
|
|
868
|
+
const statsToType = (stats) => {
|
|
869
|
+
if (stats.isFile()) return "file";
|
|
870
|
+
if (stats.isDirectory()) return "directory";
|
|
871
|
+
if (stats.isSymbolicLink()) return "symbolic-link";
|
|
872
|
+
if (stats.isFIFO()) return "fifo";
|
|
873
|
+
if (stats.isSocket()) return "socket";
|
|
874
|
+
if (stats.isCharacterDevice()) return "character-device";
|
|
875
|
+
if (stats.isBlockDevice()) return "block-device";
|
|
876
|
+
return undefined;
|
|
877
|
+
};
|
|
823
878
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
879
|
+
// https://github.com/coderaiser/cloudcmd/issues/63#issuecomment-195478143
|
|
880
|
+
// https://nodejs.org/api/fs.html#fs_file_modes
|
|
881
|
+
// https://github.com/TooTallNate/stat-mode
|
|
827
882
|
|
|
828
|
-
|
|
829
|
-
|
|
883
|
+
// cannot get from fs.constants because they are not available on windows
|
|
884
|
+
const S_IRUSR = 256; /* 0000400 read permission, owner */
|
|
885
|
+
const S_IWUSR = 128; /* 0000200 write permission, owner */
|
|
886
|
+
const S_IXUSR = 64; /* 0000100 execute/search permission, owner */
|
|
887
|
+
const S_IRGRP = 32; /* 0000040 read permission, group */
|
|
888
|
+
const S_IWGRP = 16; /* 0000020 write permission, group */
|
|
889
|
+
const S_IXGRP = 8; /* 0000010 execute/search permission, group */
|
|
890
|
+
const S_IROTH = 4; /* 0000004 read permission, others */
|
|
891
|
+
const S_IWOTH = 2; /* 0000002 write permission, others */
|
|
892
|
+
const S_IXOTH = 1; /* 0000001 execute/search permission, others */
|
|
830
893
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// It might also help garbage collection because
|
|
834
|
-
// runtime implementing AbortSignal (Node.js, browsers) can consider abortSignal
|
|
835
|
-
// as settled and clean up things
|
|
836
|
-
if (abortAfterEnd) {
|
|
837
|
-
// because of operationSignal.onabort = null
|
|
838
|
-
// + abortCallbackList.clear() this won't re-call
|
|
839
|
-
// callbacks
|
|
840
|
-
if (!operationSignal.aborted) {
|
|
841
|
-
isAbortAfterEnd = true;
|
|
842
|
-
operationAbortController.abort();
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
};
|
|
894
|
+
const permissionsToBinaryFlags = ({ owner, group, others }) => {
|
|
895
|
+
let binaryFlags = 0;
|
|
846
896
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
onAbortCallback();
|
|
855
|
-
};
|
|
856
|
-
const applyRemoveEffects = () => {
|
|
857
|
-
const onRemoveCallback = onRemove;
|
|
858
|
-
onRemove = callbackNoop;
|
|
859
|
-
onAbort = callbackNoop;
|
|
860
|
-
onRemoveCallback();
|
|
861
|
-
};
|
|
897
|
+
if (owner.read) binaryFlags |= S_IRUSR;
|
|
898
|
+
if (owner.write) binaryFlags |= S_IWUSR;
|
|
899
|
+
if (owner.execute) binaryFlags |= S_IXUSR;
|
|
900
|
+
|
|
901
|
+
if (group.read) binaryFlags |= S_IRGRP;
|
|
902
|
+
if (group.write) binaryFlags |= S_IWGRP;
|
|
903
|
+
if (group.execute) binaryFlags |= S_IXGRP;
|
|
862
904
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
return callbackNoop;
|
|
867
|
-
}
|
|
905
|
+
if (others.read) binaryFlags |= S_IROTH;
|
|
906
|
+
if (others.write) binaryFlags |= S_IWOTH;
|
|
907
|
+
if (others.execute) binaryFlags |= S_IXOTH;
|
|
868
908
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
applyAbortEffects();
|
|
872
|
-
applyRemoveEffects();
|
|
873
|
-
return callbackNoop;
|
|
874
|
-
}
|
|
909
|
+
return binaryFlags;
|
|
910
|
+
};
|
|
875
911
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
},
|
|
912
|
+
const writeEntryPermissions = async (source, permissions) => {
|
|
913
|
+
const sourceUrl = assertAndNormalizeFileUrl(source);
|
|
914
|
+
|
|
915
|
+
let binaryFlags;
|
|
916
|
+
if (typeof permissions === "object") {
|
|
917
|
+
permissions = {
|
|
918
|
+
owner: {
|
|
919
|
+
read: getPermissionOrComputeDefault("read", "owner", permissions),
|
|
920
|
+
write: getPermissionOrComputeDefault("write", "owner", permissions),
|
|
921
|
+
execute: getPermissionOrComputeDefault("execute", "owner", permissions),
|
|
887
922
|
},
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
operation_end: () => {
|
|
902
|
-
// Exists to
|
|
903
|
-
// - remove abort callback on operation
|
|
904
|
-
// - remove "abort" event listener on child
|
|
905
|
-
// - call any custom cancel function
|
|
906
|
-
applyRemoveEffects();
|
|
907
|
-
},
|
|
908
|
-
child_abort: () => {
|
|
909
|
-
applyAbortEffects();
|
|
910
|
-
operationAbortController.abort();
|
|
911
|
-
},
|
|
912
|
-
};
|
|
913
|
-
raceEffects[winner.name](winner.value);
|
|
923
|
+
group: {
|
|
924
|
+
read: getPermissionOrComputeDefault("read", "group", permissions),
|
|
925
|
+
write: getPermissionOrComputeDefault("write", "group", permissions),
|
|
926
|
+
execute: getPermissionOrComputeDefault("execute", "group", permissions),
|
|
927
|
+
},
|
|
928
|
+
others: {
|
|
929
|
+
read: getPermissionOrComputeDefault("read", "others", permissions),
|
|
930
|
+
write: getPermissionOrComputeDefault("write", "others", permissions),
|
|
931
|
+
execute: getPermissionOrComputeDefault(
|
|
932
|
+
"execute",
|
|
933
|
+
"others",
|
|
934
|
+
permissions,
|
|
935
|
+
),
|
|
914
936
|
},
|
|
915
|
-
);
|
|
916
|
-
|
|
917
|
-
return () => {
|
|
918
|
-
cancelRace();
|
|
919
|
-
applyRemoveEffects();
|
|
920
937
|
};
|
|
921
|
-
|
|
938
|
+
binaryFlags = permissionsToBinaryFlags(permissions);
|
|
939
|
+
} else {
|
|
940
|
+
binaryFlags = permissions;
|
|
941
|
+
}
|
|
922
942
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const abortSourceSignal = abortSourceController.signal;
|
|
931
|
-
abortSource.signal = abortSourceSignal;
|
|
932
|
-
if (operationSignal.aborted) {
|
|
933
|
-
return abortSource;
|
|
934
|
-
}
|
|
935
|
-
const returnValue = abortSourceCallback((value) => {
|
|
936
|
-
abortSourceController.abort(value);
|
|
937
|
-
});
|
|
938
|
-
const removeAbortSignal = addAbortSignal(abortSourceSignal, {
|
|
939
|
-
onRemove: () => {
|
|
940
|
-
if (typeof returnValue === "function") {
|
|
941
|
-
returnValue();
|
|
942
|
-
}
|
|
943
|
-
abortSource.cleaned = true;
|
|
944
|
-
},
|
|
943
|
+
return new Promise((resolve, reject) => {
|
|
944
|
+
chmod(new URL(sourceUrl), binaryFlags, (error) => {
|
|
945
|
+
if (error) {
|
|
946
|
+
reject(error);
|
|
947
|
+
} else {
|
|
948
|
+
resolve();
|
|
949
|
+
}
|
|
945
950
|
});
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
};
|
|
951
|
+
});
|
|
952
|
+
};
|
|
949
953
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const timeoutId = setTimeout(abort, ms);
|
|
953
|
-
// an abort source return value is called when:
|
|
954
|
-
// - operation is aborted (by an other source)
|
|
955
|
-
// - operation ends
|
|
956
|
-
return () => {
|
|
957
|
-
clearTimeout(timeoutId);
|
|
958
|
-
};
|
|
959
|
-
});
|
|
960
|
-
};
|
|
954
|
+
const actionLevels = { read: 0, write: 1, execute: 2 };
|
|
955
|
+
const subjectLevels = { others: 0, group: 1, owner: 2 };
|
|
961
956
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
abortController.abort();
|
|
968
|
-
},
|
|
969
|
-
});
|
|
970
|
-
try {
|
|
971
|
-
const value = await asyncCallback(signal);
|
|
972
|
-
removeAbortSignal();
|
|
973
|
-
return value;
|
|
974
|
-
} catch (e) {
|
|
975
|
-
removeAbortSignal();
|
|
976
|
-
throw e;
|
|
957
|
+
const getPermissionOrComputeDefault = (action, subject, permissions) => {
|
|
958
|
+
if (subject in permissions) {
|
|
959
|
+
const subjectPermissions = permissions[subject];
|
|
960
|
+
if (action in subjectPermissions) {
|
|
961
|
+
return subjectPermissions[action];
|
|
977
962
|
}
|
|
978
|
-
};
|
|
979
963
|
|
|
980
|
-
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
try {
|
|
989
|
-
const value = callback(signal);
|
|
990
|
-
removeAbortSignal();
|
|
991
|
-
return value;
|
|
992
|
-
} catch (e) {
|
|
993
|
-
removeAbortSignal();
|
|
994
|
-
throw e;
|
|
964
|
+
const actionLevel = actionLevels[action];
|
|
965
|
+
const actionFallback = Object.keys(actionLevels).find(
|
|
966
|
+
(actionFallbackCandidate) =>
|
|
967
|
+
actionLevels[actionFallbackCandidate] > actionLevel &&
|
|
968
|
+
actionFallbackCandidate in subjectPermissions,
|
|
969
|
+
);
|
|
970
|
+
if (actionFallback) {
|
|
971
|
+
return subjectPermissions[actionFallback];
|
|
995
972
|
}
|
|
996
|
-
}
|
|
973
|
+
}
|
|
997
974
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
975
|
+
const subjectLevel = subjectLevels[subject];
|
|
976
|
+
// do we have a subject with a stronger level (group or owner)
|
|
977
|
+
// where we could read the action permission ?
|
|
978
|
+
const subjectFallback = Object.keys(subjectLevels).find(
|
|
979
|
+
(subjectFallbackCandidate) =>
|
|
980
|
+
subjectLevels[subjectFallbackCandidate] > subjectLevel &&
|
|
981
|
+
subjectFallbackCandidate in permissions,
|
|
982
|
+
);
|
|
983
|
+
if (subjectFallback) {
|
|
984
|
+
const subjectPermissions = permissions[subjectFallback];
|
|
985
|
+
return action in subjectPermissions
|
|
986
|
+
? subjectPermissions[action]
|
|
987
|
+
: getPermissionOrComputeDefault(action, subjectFallback, permissions);
|
|
988
|
+
}
|
|
1004
989
|
|
|
1005
|
-
|
|
1006
|
-
addAbortCallback,
|
|
1007
|
-
addAbortSignal,
|
|
1008
|
-
addAbortSource,
|
|
1009
|
-
timeout,
|
|
1010
|
-
withSignal,
|
|
1011
|
-
withSignalSync,
|
|
1012
|
-
addEndCallback,
|
|
1013
|
-
end,
|
|
1014
|
-
};
|
|
990
|
+
return false;
|
|
1015
991
|
};
|
|
1016
992
|
|
|
1017
|
-
|
|
993
|
+
/*
|
|
994
|
+
* - stats object documentation on Node.js
|
|
995
|
+
* https://nodejs.org/docs/latest-v13.x/api/fs.html#fs_class_fs_stats
|
|
996
|
+
*/
|
|
1018
997
|
|
|
1019
|
-
const addEventListener = (target, eventName, cb) => {
|
|
1020
|
-
target.addEventListener(eventName, cb);
|
|
1021
|
-
return () => {
|
|
1022
|
-
target.removeEventListener(eventName, cb);
|
|
1023
|
-
};
|
|
1024
|
-
};
|
|
1025
998
|
|
|
1026
|
-
const
|
|
1027
|
-
return raceCallbacks(
|
|
1028
|
-
{
|
|
1029
|
-
...(processTeardownEvents.SIGHUP ? SIGHUP_CALLBACK : {}),
|
|
1030
|
-
...(processTeardownEvents.SIGTERM ? SIGTERM_CALLBACK : {}),
|
|
1031
|
-
...(processTeardownEvents.SIGINT ? SIGINT_CALLBACK : {}),
|
|
1032
|
-
...(processTeardownEvents.beforeExit ? BEFORE_EXIT_CALLBACK : {}),
|
|
1033
|
-
...(processTeardownEvents.exit ? EXIT_CALLBACK : {}),
|
|
1034
|
-
},
|
|
1035
|
-
callback,
|
|
1036
|
-
);
|
|
1037
|
-
};
|
|
999
|
+
const isWindows$2 = process.platform === "win32";
|
|
1038
1000
|
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1001
|
+
const readEntryStat = async (
|
|
1002
|
+
source,
|
|
1003
|
+
{ nullIfNotFound = false, followLink = true } = {},
|
|
1004
|
+
) => {
|
|
1005
|
+
let sourceUrl = assertAndNormalizeFileUrl(source);
|
|
1006
|
+
if (sourceUrl.endsWith("/")) sourceUrl = sourceUrl.slice(0, -1);
|
|
1007
|
+
|
|
1008
|
+
const sourcePath = urlToFileSystemPath(sourceUrl);
|
|
1009
|
+
|
|
1010
|
+
const handleNotFoundOption = nullIfNotFound
|
|
1011
|
+
? {
|
|
1012
|
+
handleNotFoundError: () => null,
|
|
1013
|
+
}
|
|
1014
|
+
: {};
|
|
1047
1015
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1016
|
+
return readStat(sourcePath, {
|
|
1017
|
+
followLink,
|
|
1018
|
+
...handleNotFoundOption,
|
|
1019
|
+
...(isWindows$2
|
|
1020
|
+
? {
|
|
1021
|
+
// Windows can EPERM on stat
|
|
1022
|
+
handlePermissionDeniedError: async (error) => {
|
|
1023
|
+
console.error(
|
|
1024
|
+
`trying to fix windows EPERM after stats on ${sourcePath}`,
|
|
1025
|
+
);
|
|
1056
1026
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1027
|
+
try {
|
|
1028
|
+
// unfortunately it means we mutate the permissions
|
|
1029
|
+
// without being able to restore them to the previous value
|
|
1030
|
+
// (because reading current permission would also throw)
|
|
1031
|
+
await writeEntryPermissions(sourceUrl, 0o666);
|
|
1032
|
+
const stats = await readStat(sourcePath, {
|
|
1033
|
+
followLink,
|
|
1034
|
+
...handleNotFoundOption,
|
|
1035
|
+
// could not fix the permission error, give up and throw original error
|
|
1036
|
+
handlePermissionDeniedError: () => {
|
|
1037
|
+
console.error(`still got EPERM after stats on ${sourcePath}`);
|
|
1038
|
+
throw error;
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
return stats;
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
console.error(
|
|
1044
|
+
`error while trying to fix windows EPERM after stats on ${sourcePath}: ${e.stack}`,
|
|
1045
|
+
);
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
}
|
|
1050
|
+
: {}),
|
|
1051
|
+
});
|
|
1064
1052
|
};
|
|
1065
1053
|
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
},
|
|
1073
|
-
|
|
1054
|
+
const readStat = (
|
|
1055
|
+
sourcePath,
|
|
1056
|
+
{
|
|
1057
|
+
followLink,
|
|
1058
|
+
handleNotFoundError = null,
|
|
1059
|
+
handlePermissionDeniedError = null,
|
|
1060
|
+
} = {},
|
|
1061
|
+
) => {
|
|
1062
|
+
const nodeMethod = followLink ? stat : lstat;
|
|
1074
1063
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1064
|
+
return new Promise((resolve, reject) => {
|
|
1065
|
+
nodeMethod(sourcePath, (error, statsObject) => {
|
|
1066
|
+
if (error) {
|
|
1067
|
+
if (handleNotFoundError && error.code === "ENOENT") {
|
|
1068
|
+
resolve(handleNotFoundError(error));
|
|
1069
|
+
} else if (
|
|
1070
|
+
handlePermissionDeniedError &&
|
|
1071
|
+
(error.code === "EPERM" || error.code === "EACCES")
|
|
1072
|
+
) {
|
|
1073
|
+
resolve(handlePermissionDeniedError(error));
|
|
1074
|
+
} else {
|
|
1075
|
+
reject(error);
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
resolve(statsObject);
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
1082
|
};
|
|
1083
1083
|
|
|
1084
1084
|
const readDirectory = async (url, { emfileMaxWait = 1000 } = {}) => {
|
|
@@ -2708,6 +2708,22 @@ const startSpinner = ({
|
|
|
2708
2708
|
return spinner;
|
|
2709
2709
|
};
|
|
2710
2710
|
|
|
2711
|
+
const createTeardown = () => {
|
|
2712
|
+
const teardownCallbackSet = new Set();
|
|
2713
|
+
return {
|
|
2714
|
+
addCallback: (callback) => {
|
|
2715
|
+
teardownCallbackSet.add(callback);
|
|
2716
|
+
},
|
|
2717
|
+
trigger: async () => {
|
|
2718
|
+
await Promise.all(
|
|
2719
|
+
Array.from(teardownCallbackSet.values()).map(async (callback) => {
|
|
2720
|
+
await callback();
|
|
2721
|
+
}),
|
|
2722
|
+
);
|
|
2723
|
+
},
|
|
2724
|
+
};
|
|
2725
|
+
};
|
|
2726
|
+
|
|
2711
2727
|
const generateCoverageJsonFile = async ({
|
|
2712
2728
|
coverage,
|
|
2713
2729
|
coverageJsonFileUrl,
|
|
@@ -2812,6 +2828,208 @@ const pingServer = async (url) => {
|
|
|
2812
2828
|
return false;
|
|
2813
2829
|
};
|
|
2814
2830
|
|
|
2831
|
+
const startServerUsingModuleUrl = async (
|
|
2832
|
+
webServer,
|
|
2833
|
+
{ signal, teardown, logger, allocatedMs },
|
|
2834
|
+
) => {
|
|
2835
|
+
if (!existsSync(new URL(webServer.moduleUrl))) {
|
|
2836
|
+
throw new Error(
|
|
2837
|
+
`webServer.moduleUrl does not lead to a file at "${webServer.moduleUrl}"`,
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
const worker = new Worker(webServer.moduleUrl, {
|
|
2841
|
+
env: {
|
|
2842
|
+
// IMPORTED_BY_TEST_PLAN: "1",
|
|
2843
|
+
},
|
|
2844
|
+
stdin: true,
|
|
2845
|
+
stdout: true,
|
|
2846
|
+
});
|
|
2847
|
+
let errorReceived = false;
|
|
2848
|
+
const errorPromise = new Promise((resolve, reject) => {
|
|
2849
|
+
worker.on("error", (e) => {
|
|
2850
|
+
errorReceived = true;
|
|
2851
|
+
reject(e);
|
|
2852
|
+
});
|
|
2853
|
+
});
|
|
2854
|
+
|
|
2855
|
+
const killWorker = async () => {
|
|
2856
|
+
await worker.terminate();
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
const startOperation = Abort.startOperation();
|
|
2860
|
+
startOperation.addAbortSignal(signal);
|
|
2861
|
+
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
2862
|
+
startOperation.addAbortCallback(killWorker);
|
|
2863
|
+
teardown.addCallback(killWorker);
|
|
2864
|
+
|
|
2865
|
+
const startedPromise = (async () => {
|
|
2866
|
+
const logScale = [100, 250, 500];
|
|
2867
|
+
// eslint-disable-next-line no-constant-condition
|
|
2868
|
+
while (true) {
|
|
2869
|
+
if (errorReceived) {
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2872
|
+
const connected = await pingServer(webServer.origin);
|
|
2873
|
+
if (connected) {
|
|
2874
|
+
break;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
startOperation.throwIfAborted();
|
|
2878
|
+
const delay = logScale.shift() || 1000;
|
|
2879
|
+
logger.debug(`Waiting ${delay}ms`);
|
|
2880
|
+
await new Promise((x) => setTimeout(x, delay));
|
|
2881
|
+
}
|
|
2882
|
+
})();
|
|
2883
|
+
|
|
2884
|
+
try {
|
|
2885
|
+
await Promise.race([errorPromise, startedPromise]);
|
|
2886
|
+
} catch (e) {
|
|
2887
|
+
if (Abort.isAbortError(e)) {
|
|
2888
|
+
if (timeoutAbortSource.signal.aborted) {
|
|
2889
|
+
// aborted by timeout
|
|
2890
|
+
throw new Error(
|
|
2891
|
+
`"${webServer.moduleUrl}" did not start a server in less than ${allocatedMs}ms (webServer.moduleUrl)`,
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
if (signal.aborted) {
|
|
2895
|
+
// aborted from outside
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
throw e;
|
|
2900
|
+
} finally {
|
|
2901
|
+
await startOperation.end();
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
|
|
2905
|
+
const startServerUsingCommand = async (
|
|
2906
|
+
webServer,
|
|
2907
|
+
{ signal, allocatedMs, logger, teardown },
|
|
2908
|
+
) => {
|
|
2909
|
+
const spawnedProcess = spawn(webServer.command, [], {
|
|
2910
|
+
// On non-windows platforms, `detached: true` makes child process a leader of a new
|
|
2911
|
+
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
|
|
2912
|
+
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
|
|
2913
|
+
detached: process.platform !== "win32",
|
|
2914
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2915
|
+
shell: true,
|
|
2916
|
+
cwd: webServer.cwd,
|
|
2917
|
+
});
|
|
2918
|
+
if (!spawnedProcess.pid) {
|
|
2919
|
+
await new Promise((resolve, reject) => {
|
|
2920
|
+
spawnedProcess.once("error", (error) => {
|
|
2921
|
+
reject(new Error(`Failed to launch: ${error}`));
|
|
2922
|
+
});
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
let errorReceived = false;
|
|
2927
|
+
const errorPromise = new Promise((resolve, reject) => {
|
|
2928
|
+
spawnedProcess.on("error", (e) => {
|
|
2929
|
+
errorReceived = true;
|
|
2930
|
+
reject(e);
|
|
2931
|
+
});
|
|
2932
|
+
});
|
|
2933
|
+
|
|
2934
|
+
// const stdout = readline.createInterface({ input: spawnedProcess.stdout });
|
|
2935
|
+
// stdout.on("line", () => {
|
|
2936
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][out] ${data}`);
|
|
2937
|
+
// });
|
|
2938
|
+
// const stderr = readline.createInterface({ input: spawnedProcess.stderr });
|
|
2939
|
+
// stderr.on("line", (data) => {
|
|
2940
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][err] ${data}`);
|
|
2941
|
+
// });
|
|
2942
|
+
let processClosed = false;
|
|
2943
|
+
const closedPromise = new Promise((resolve) => {
|
|
2944
|
+
spawnedProcess.once("exit", (exitCode, signal) => {
|
|
2945
|
+
logger.info(
|
|
2946
|
+
`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`,
|
|
2947
|
+
);
|
|
2948
|
+
processClosed = true;
|
|
2949
|
+
resolve();
|
|
2950
|
+
});
|
|
2951
|
+
});
|
|
2952
|
+
const killProcess = async () => {
|
|
2953
|
+
logger.info(`[pid=${spawnedProcess.pid}] <kill>`);
|
|
2954
|
+
if (!spawnedProcess.pid || spawnedProcess.killed || processClosed) {
|
|
2955
|
+
logger.info(
|
|
2956
|
+
`[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`,
|
|
2957
|
+
);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
logger.info(`[pid=${spawnedProcess.pid}] <will force kill>`);
|
|
2961
|
+
// Force kill the browser.
|
|
2962
|
+
try {
|
|
2963
|
+
if (process.platform === "win32") {
|
|
2964
|
+
const taskkillProcess = spawnSync(
|
|
2965
|
+
`taskkill /pid ${spawnedProcess.pid} /T /F`,
|
|
2966
|
+
{ shell: true },
|
|
2967
|
+
);
|
|
2968
|
+
const [stdout, stderr] = [
|
|
2969
|
+
taskkillProcess.stdout.toString(),
|
|
2970
|
+
taskkillProcess.stderr.toString(),
|
|
2971
|
+
];
|
|
2972
|
+
if (stdout)
|
|
2973
|
+
logger.info(`[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`);
|
|
2974
|
+
if (stderr)
|
|
2975
|
+
logger.info(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
|
|
2976
|
+
} else {
|
|
2977
|
+
process.kill(-spawnedProcess.pid, "SIGKILL");
|
|
2978
|
+
}
|
|
2979
|
+
} catch (e) {
|
|
2980
|
+
logger.info(
|
|
2981
|
+
`[pid=${spawnedProcess.pid}] exception while trying to kill process: ${e}`,
|
|
2982
|
+
);
|
|
2983
|
+
// the process might have already stopped
|
|
2984
|
+
}
|
|
2985
|
+
await closedPromise;
|
|
2986
|
+
};
|
|
2987
|
+
|
|
2988
|
+
const startOperation = Abort.startOperation();
|
|
2989
|
+
startOperation.addAbortSignal(signal);
|
|
2990
|
+
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
2991
|
+
startOperation.addAbortCallback(killProcess);
|
|
2992
|
+
teardown.addCallback(killProcess);
|
|
2993
|
+
|
|
2994
|
+
const startedPromise = (async () => {
|
|
2995
|
+
const logScale = [100, 250, 500];
|
|
2996
|
+
// eslint-disable-next-line no-constant-condition
|
|
2997
|
+
while (true) {
|
|
2998
|
+
if (errorReceived) {
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
const connected = await pingServer(webServer.origin);
|
|
3002
|
+
if (connected) {
|
|
3003
|
+
break;
|
|
3004
|
+
}
|
|
3005
|
+
startOperation.throwIfAborted();
|
|
3006
|
+
const delay = logScale.shift() || 1000;
|
|
3007
|
+
logger.debug(`Waiting ${delay}ms`);
|
|
3008
|
+
await new Promise((x) => setTimeout(x, delay));
|
|
3009
|
+
}
|
|
3010
|
+
})();
|
|
3011
|
+
|
|
3012
|
+
try {
|
|
3013
|
+
await Promise.race([errorPromise, startedPromise]);
|
|
3014
|
+
} catch (e) {
|
|
3015
|
+
if (Abort.isAbortError(e)) {
|
|
3016
|
+
if (timeoutAbortSource.signal.aborted) {
|
|
3017
|
+
// aborted by timeout
|
|
3018
|
+
throw new Error(
|
|
3019
|
+
`"${webServer.command}" command did not start a server in less than ${allocatedMs}ms (webServer.command)`,
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
if (signal.aborted) {
|
|
3023
|
+
// aborted from outside
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
throw e;
|
|
3028
|
+
} finally {
|
|
3029
|
+
await startOperation.end();
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
|
|
2815
3033
|
const basicFetch = async (
|
|
2816
3034
|
url,
|
|
2817
3035
|
{ rejectUnauthorized = true, method = "GET", headers = {} } = {},
|
|
@@ -2866,50 +3084,39 @@ const basicFetch = async (
|
|
|
2866
3084
|
});
|
|
2867
3085
|
};
|
|
2868
3086
|
|
|
2869
|
-
const assertAndNormalizeWebServer = async (
|
|
3087
|
+
const assertAndNormalizeWebServer = async (
|
|
3088
|
+
webServer,
|
|
3089
|
+
{ signal, logger, teardown },
|
|
3090
|
+
) => {
|
|
2870
3091
|
if (!webServer) {
|
|
2871
3092
|
throw new TypeError(
|
|
2872
3093
|
`webServer is required when running tests on browser(s)`,
|
|
2873
3094
|
);
|
|
2874
3095
|
}
|
|
2875
3096
|
const unexpectedParamNames = Object.keys(webServer).filter((key) => {
|
|
2876
|
-
return ![
|
|
3097
|
+
return ![
|
|
3098
|
+
"origin",
|
|
3099
|
+
"moduleUrl",
|
|
3100
|
+
"command",
|
|
3101
|
+
"cwd",
|
|
3102
|
+
"rootDirectoryUrl",
|
|
3103
|
+
].includes(key);
|
|
2877
3104
|
});
|
|
2878
3105
|
if (unexpectedParamNames.length > 0) {
|
|
2879
3106
|
throw new TypeError(
|
|
2880
3107
|
`${unexpectedParamNames.join(",")}: there is no such param to webServer`,
|
|
2881
3108
|
);
|
|
2882
3109
|
}
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
throw new TypeError(
|
|
2888
|
-
`webServer.moduleUrl is required as there is no server listening "${webServer.origin}"`,
|
|
2889
|
-
);
|
|
2890
|
-
}
|
|
2891
|
-
try {
|
|
2892
|
-
process.env.IMPORTED_BY_TEST_PLAN = "1";
|
|
2893
|
-
await import(webServer.moduleUrl);
|
|
2894
|
-
delete process.env.IMPORTED_BY_TEST_PLAN;
|
|
2895
|
-
} catch (e) {
|
|
2896
|
-
if (
|
|
2897
|
-
e.code === "ERR_MODULE_NOT_FOUND" &&
|
|
2898
|
-
e.message.includes(fileURLToPath(webServer.moduleUrl))
|
|
2899
|
-
) {
|
|
2900
|
-
throw new Error(
|
|
2901
|
-
`webServer.moduleUrl does not lead to a file at "${webServer.moduleUrl}"`,
|
|
2902
|
-
);
|
|
2903
|
-
}
|
|
2904
|
-
throw e;
|
|
2905
|
-
}
|
|
2906
|
-
aServerIsListening = await pingServer(webServer.origin);
|
|
2907
|
-
if (!aServerIsListening) {
|
|
2908
|
-
throw new Error(
|
|
2909
|
-
`webServer.moduleUrl did not start a server listening at "${webServer.origin}", check file at "${webServer.moduleUrl}"`,
|
|
2910
|
-
);
|
|
2911
|
-
}
|
|
3110
|
+
if (typeof webServer.origin !== "string") {
|
|
3111
|
+
throw new TypeError(
|
|
3112
|
+
`webServer.origin must be a string, got ${webServer.origin}`,
|
|
3113
|
+
);
|
|
2912
3114
|
}
|
|
3115
|
+
await ensureWebServerIsStarted(webServer, {
|
|
3116
|
+
signal,
|
|
3117
|
+
teardown,
|
|
3118
|
+
logger,
|
|
3119
|
+
});
|
|
2913
3120
|
const { headers } = await basicFetch(webServer.origin, {
|
|
2914
3121
|
method: "GET",
|
|
2915
3122
|
rejectUnauthorized: false,
|
|
@@ -2939,6 +3146,37 @@ const assertAndNormalizeWebServer = async (webServer) => {
|
|
|
2939
3146
|
}
|
|
2940
3147
|
};
|
|
2941
3148
|
|
|
3149
|
+
const ensureWebServerIsStarted = async (
|
|
3150
|
+
webServer,
|
|
3151
|
+
{ signal, teardown, logger, allocatedMs = 5_000 },
|
|
3152
|
+
) => {
|
|
3153
|
+
const aServerIsListening = await pingServer(webServer.origin);
|
|
3154
|
+
if (aServerIsListening) {
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
if (webServer.moduleUrl) {
|
|
3158
|
+
await startServerUsingModuleUrl(webServer, {
|
|
3159
|
+
signal,
|
|
3160
|
+
allocatedMs,
|
|
3161
|
+
teardown,
|
|
3162
|
+
logger,
|
|
3163
|
+
});
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
if (webServer.command) {
|
|
3167
|
+
await startServerUsingCommand(webServer, {
|
|
3168
|
+
signal,
|
|
3169
|
+
allocatedMs,
|
|
3170
|
+
teardown,
|
|
3171
|
+
logger,
|
|
3172
|
+
});
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
throw new TypeError(
|
|
3176
|
+
`webServer.moduleUrl or webServer.command is required as there is no server listening "${webServer.origin}"`,
|
|
3177
|
+
);
|
|
3178
|
+
};
|
|
3179
|
+
|
|
2942
3180
|
const executionStepsFromTestPlan = async ({
|
|
2943
3181
|
signal,
|
|
2944
3182
|
rootDirectoryUrl,
|
|
@@ -4209,7 +4447,7 @@ const executeSteps = async (
|
|
|
4209
4447
|
executionSteps,
|
|
4210
4448
|
{
|
|
4211
4449
|
signal,
|
|
4212
|
-
|
|
4450
|
+
teardown,
|
|
4213
4451
|
logger,
|
|
4214
4452
|
logRefresh,
|
|
4215
4453
|
logRuntime,
|
|
@@ -4249,23 +4487,9 @@ const executeSteps = async (
|
|
|
4249
4487
|
const executePlanReturnValue = {};
|
|
4250
4488
|
const report = {};
|
|
4251
4489
|
const callbacks = [];
|
|
4252
|
-
const stopAfterAllSignal = { notify: () => {} };
|
|
4253
4490
|
|
|
4254
4491
|
const multipleExecutionsOperation = Abort.startOperation();
|
|
4255
4492
|
multipleExecutionsOperation.addAbortSignal(signal);
|
|
4256
|
-
if (handleSIGINT) {
|
|
4257
|
-
multipleExecutionsOperation.addAbortSource((abort) => {
|
|
4258
|
-
return raceProcessTeardownEvents(
|
|
4259
|
-
{
|
|
4260
|
-
SIGINT: true,
|
|
4261
|
-
},
|
|
4262
|
-
() => {
|
|
4263
|
-
logger.debug(`SIGINT abort`);
|
|
4264
|
-
abort();
|
|
4265
|
-
},
|
|
4266
|
-
);
|
|
4267
|
-
});
|
|
4268
|
-
}
|
|
4269
4493
|
const failFastAbortController = new AbortController();
|
|
4270
4494
|
if (failFast) {
|
|
4271
4495
|
multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal);
|
|
@@ -4318,7 +4542,8 @@ const executeSteps = async (
|
|
|
4318
4542
|
coverageConfig,
|
|
4319
4543
|
coverageMethodForBrowsers,
|
|
4320
4544
|
coverageMethodForNodeJs,
|
|
4321
|
-
|
|
4545
|
+
isTestPlan: true,
|
|
4546
|
+
teardown,
|
|
4322
4547
|
};
|
|
4323
4548
|
|
|
4324
4549
|
if (logMergeForCompletedExecutions && !process.stdout.isTTY) {
|
|
@@ -4516,8 +4741,8 @@ const executeSteps = async (
|
|
|
4516
4741
|
},
|
|
4517
4742
|
});
|
|
4518
4743
|
if (!keepRunning) {
|
|
4519
|
-
logger.debug("
|
|
4520
|
-
await
|
|
4744
|
+
logger.debug("trigger test plan teardown");
|
|
4745
|
+
await teardown.trigger();
|
|
4521
4746
|
}
|
|
4522
4747
|
|
|
4523
4748
|
counters.cancelled = counters.total - counters.done;
|
|
@@ -4539,10 +4764,9 @@ const executeSteps = async (
|
|
|
4539
4764
|
executePlanReturnValue.aborted = multipleExecutionsOperation.signal.aborted;
|
|
4540
4765
|
executePlanReturnValue.planSummary = summary;
|
|
4541
4766
|
executePlanReturnValue.planReport = report;
|
|
4542
|
-
|
|
4543
|
-
await previous;
|
|
4767
|
+
for (const callback of callbacks) {
|
|
4544
4768
|
await callback();
|
|
4545
|
-
}
|
|
4769
|
+
}
|
|
4546
4770
|
return executePlanReturnValue;
|
|
4547
4771
|
} finally {
|
|
4548
4772
|
await multipleExecutionsOperation.end();
|
|
@@ -4708,6 +4932,25 @@ const executeTestPlan = async ({
|
|
|
4708
4932
|
coverageReportHtmlDirectoryUrl,
|
|
4709
4933
|
...rest
|
|
4710
4934
|
}) => {
|
|
4935
|
+
const teardown = createTeardown();
|
|
4936
|
+
|
|
4937
|
+
const operation = Abort.startOperation();
|
|
4938
|
+
operation.addAbortSignal(signal);
|
|
4939
|
+
if (handleSIGINT) {
|
|
4940
|
+
operation.addAbortSource((abort) => {
|
|
4941
|
+
return raceProcessTeardownEvents(
|
|
4942
|
+
{
|
|
4943
|
+
SIGINT: true,
|
|
4944
|
+
},
|
|
4945
|
+
() => {
|
|
4946
|
+
logger.debug(`SIGINT abort`);
|
|
4947
|
+
abort();
|
|
4948
|
+
},
|
|
4949
|
+
);
|
|
4950
|
+
});
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
let logger;
|
|
4711
4954
|
let someNeedsServer = false;
|
|
4712
4955
|
let someHasCoverageV8 = false;
|
|
4713
4956
|
let someNodeRuntime = false;
|
|
@@ -4731,6 +4974,8 @@ const executeTestPlan = async ({
|
|
|
4731
4974
|
throw new Error(`testPlan must be an object, got ${testPlan}`);
|
|
4732
4975
|
}
|
|
4733
4976
|
|
|
4977
|
+
logger = createLogger({ logLevel });
|
|
4978
|
+
|
|
4734
4979
|
Object.keys(testPlan).forEach((filePattern) => {
|
|
4735
4980
|
const filePlan = testPlan[filePattern];
|
|
4736
4981
|
if (!filePlan) return;
|
|
@@ -4753,7 +4998,11 @@ const executeTestPlan = async ({
|
|
|
4753
4998
|
});
|
|
4754
4999
|
|
|
4755
5000
|
if (someNeedsServer) {
|
|
4756
|
-
await assertAndNormalizeWebServer(webServer
|
|
5001
|
+
await assertAndNormalizeWebServer(webServer, {
|
|
5002
|
+
signal: operation.signal,
|
|
5003
|
+
teardown,
|
|
5004
|
+
logger,
|
|
5005
|
+
});
|
|
4757
5006
|
}
|
|
4758
5007
|
|
|
4759
5008
|
if (coverageEnabled) {
|
|
@@ -4838,7 +5087,6 @@ const executeTestPlan = async ({
|
|
|
4838
5087
|
}
|
|
4839
5088
|
}
|
|
4840
5089
|
|
|
4841
|
-
const logger = createLogger({ logLevel });
|
|
4842
5090
|
logger.debug(
|
|
4843
5091
|
createDetailedMessage(`Prepare executing plan`, {
|
|
4844
5092
|
runtimes: JSON.stringify(runtimes, null, " "),
|
|
@@ -4893,7 +5141,7 @@ const executeTestPlan = async ({
|
|
|
4893
5141
|
|
|
4894
5142
|
const result = await executeSteps(executionSteps, {
|
|
4895
5143
|
signal,
|
|
4896
|
-
|
|
5144
|
+
teardown,
|
|
4897
5145
|
logger,
|
|
4898
5146
|
logRefresh,
|
|
4899
5147
|
logSummary,
|
|
@@ -5217,7 +5465,8 @@ const createRuntimeUsingPlaywright = ({
|
|
|
5217
5465
|
coverageMethodForBrowsers,
|
|
5218
5466
|
coverageFileUrl,
|
|
5219
5467
|
|
|
5220
|
-
|
|
5468
|
+
teardown,
|
|
5469
|
+
isTestPlan,
|
|
5221
5470
|
stopSignal,
|
|
5222
5471
|
keepRunning,
|
|
5223
5472
|
onConsole,
|
|
@@ -5236,7 +5485,7 @@ const createRuntimeUsingPlaywright = ({
|
|
|
5236
5485
|
await cleanupCallbackList.notify({ reason });
|
|
5237
5486
|
});
|
|
5238
5487
|
|
|
5239
|
-
const isBrowserDedicatedToExecution = isolatedTab || !
|
|
5488
|
+
const isBrowserDedicatedToExecution = isolatedTab || !isTestPlan;
|
|
5240
5489
|
let browserAndContextPromise = isBrowserDedicatedToExecution
|
|
5241
5490
|
? null
|
|
5242
5491
|
: browserPromiseCache.get(label);
|
|
@@ -5485,15 +5734,11 @@ const createRuntimeUsingPlaywright = ({
|
|
|
5485
5734
|
cleanupCallbackList.add(() => {
|
|
5486
5735
|
browser.removeListener("disconnected", disconnectedCallback);
|
|
5487
5736
|
});
|
|
5488
|
-
|
|
5489
|
-
stopAfterAllSignal.notify = async () => {
|
|
5490
|
-
await notifyPrevious();
|
|
5737
|
+
teardown.addCallback(async () => {
|
|
5491
5738
|
browser.removeListener("disconnected", disconnectedCallback);
|
|
5492
|
-
logger.debug(
|
|
5493
|
-
`stopAfterAllSignal notified -> closing ${browserName}`,
|
|
5494
|
-
);
|
|
5739
|
+
logger.debug(`testPlan teardown -> closing ${browserName}`);
|
|
5495
5740
|
await closeBrowser();
|
|
5496
|
-
};
|
|
5741
|
+
});
|
|
5497
5742
|
}
|
|
5498
5743
|
},
|
|
5499
5744
|
response: async (cb) => {
|
|
@@ -6840,6 +7085,7 @@ const execute = async ({
|
|
|
6840
7085
|
rootDirectoryUrl,
|
|
6841
7086
|
"rootDirectoryUrl",
|
|
6842
7087
|
);
|
|
7088
|
+
const teardown = createTeardown();
|
|
6843
7089
|
const executeOperation = Abort.startOperation();
|
|
6844
7090
|
executeOperation.addAbortSignal(signal);
|
|
6845
7091
|
if (handleSIGINT) {
|
|
@@ -6854,7 +7100,7 @@ const execute = async ({
|
|
|
6854
7100
|
}
|
|
6855
7101
|
|
|
6856
7102
|
if (runtime.type === "browser") {
|
|
6857
|
-
await assertAndNormalizeWebServer(webServer);
|
|
7103
|
+
await assertAndNormalizeWebServer(webServer, { signal, teardown, logger });
|
|
6858
7104
|
}
|
|
6859
7105
|
|
|
6860
7106
|
let resultTransformer = (result) => result;
|
|
@@ -6863,6 +7109,7 @@ const execute = async ({
|
|
|
6863
7109
|
webServer,
|
|
6864
7110
|
fileRelativeUrl,
|
|
6865
7111
|
importMap,
|
|
7112
|
+
teardown,
|
|
6866
7113
|
...runtimeParams,
|
|
6867
7114
|
};
|
|
6868
7115
|
|
|
@@ -6902,6 +7149,7 @@ const execute = async ({
|
|
|
6902
7149
|
}
|
|
6903
7150
|
return result;
|
|
6904
7151
|
} finally {
|
|
7152
|
+
await teardown.trigger();
|
|
6905
7153
|
await executeOperation.end();
|
|
6906
7154
|
}
|
|
6907
7155
|
};
|