@jsenv/test 1.5.1 → 1.6.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/dist/jsenv_test.js +1238 -981
- package/dist/worker_importing_module_starting_web_server.mjs +5 -0
- package/package.json +3 -3
- 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 +88 -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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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;
|
|
904
|
+
|
|
905
|
+
if (others.read) binaryFlags |= S_IROTH;
|
|
906
|
+
if (others.write) binaryFlags |= S_IWOTH;
|
|
907
|
+
if (others.execute) binaryFlags |= S_IXOTH;
|
|
862
908
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
applyRemoveEffects();
|
|
866
|
-
return callbackNoop;
|
|
867
|
-
}
|
|
909
|
+
return binaryFlags;
|
|
910
|
+
};
|
|
868
911
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
applyAbortEffects();
|
|
872
|
-
applyRemoveEffects();
|
|
873
|
-
return callbackNoop;
|
|
874
|
-
}
|
|
912
|
+
const writeEntryPermissions = async (source, permissions) => {
|
|
913
|
+
const sourceUrl = assertAndNormalizeFileUrl(source);
|
|
875
914
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
},
|
|
884
|
-
child_abort: (cb) => {
|
|
885
|
-
return addEventListener(signal, "abort", cb);
|
|
886
|
-
},
|
|
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);
|
|
1047
1007
|
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
};
|
|
1008
|
+
const sourcePath = urlToFileSystemPath(sourceUrl);
|
|
1009
|
+
|
|
1010
|
+
const handleNotFoundOption = nullIfNotFound
|
|
1011
|
+
? {
|
|
1012
|
+
handleNotFoundError: () => null,
|
|
1013
|
+
}
|
|
1014
|
+
: {};
|
|
1056
1015
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
+
);
|
|
1065
1026
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
+
});
|
|
1073
1052
|
};
|
|
1074
1053
|
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
},
|
|
1054
|
+
const readStat = (
|
|
1055
|
+
sourcePath,
|
|
1056
|
+
{
|
|
1057
|
+
followLink,
|
|
1058
|
+
handleNotFoundError = null,
|
|
1059
|
+
handlePermissionDeniedError = null,
|
|
1060
|
+
} = {},
|
|
1061
|
+
) => {
|
|
1062
|
+
const nodeMethod = followLink ? stat : lstat;
|
|
1063
|
+
|
|
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,217 @@ 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(
|
|
2841
|
+
new URL(
|
|
2842
|
+
"./worker_importing_module_starting_web_server.mjs",
|
|
2843
|
+
import.meta.url,
|
|
2844
|
+
),
|
|
2845
|
+
{
|
|
2846
|
+
workerData: {
|
|
2847
|
+
url: String(webServer.moduleUrl),
|
|
2848
|
+
},
|
|
2849
|
+
env: {
|
|
2850
|
+
IMPORTED_BY_TEST_PLAN: "1",
|
|
2851
|
+
},
|
|
2852
|
+
stdin: true,
|
|
2853
|
+
stdout: true,
|
|
2854
|
+
},
|
|
2855
|
+
);
|
|
2856
|
+
let errorReceived = false;
|
|
2857
|
+
const errorPromise = new Promise((resolve, reject) => {
|
|
2858
|
+
worker.on("error", (e) => {
|
|
2859
|
+
errorReceived = true;
|
|
2860
|
+
reject(e);
|
|
2861
|
+
});
|
|
2862
|
+
});
|
|
2863
|
+
|
|
2864
|
+
const killWorker = async () => {
|
|
2865
|
+
await worker.terminate();
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
const startOperation = Abort.startOperation();
|
|
2869
|
+
startOperation.addAbortSignal(signal);
|
|
2870
|
+
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
2871
|
+
startOperation.addAbortCallback(killWorker);
|
|
2872
|
+
teardown.addCallback(killWorker);
|
|
2873
|
+
|
|
2874
|
+
const startedPromise = (async () => {
|
|
2875
|
+
const logScale = [100, 250, 500];
|
|
2876
|
+
// eslint-disable-next-line no-constant-condition
|
|
2877
|
+
while (true) {
|
|
2878
|
+
if (errorReceived) {
|
|
2879
|
+
break;
|
|
2880
|
+
}
|
|
2881
|
+
const connected = await pingServer(webServer.origin);
|
|
2882
|
+
if (connected) {
|
|
2883
|
+
break;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
startOperation.throwIfAborted();
|
|
2887
|
+
const delay = logScale.shift() || 1000;
|
|
2888
|
+
logger.debug(`Waiting ${delay}ms`);
|
|
2889
|
+
await new Promise((x) => setTimeout(x, delay));
|
|
2890
|
+
}
|
|
2891
|
+
})();
|
|
2892
|
+
|
|
2893
|
+
try {
|
|
2894
|
+
await Promise.race([errorPromise, startedPromise]);
|
|
2895
|
+
} catch (e) {
|
|
2896
|
+
if (Abort.isAbortError(e)) {
|
|
2897
|
+
if (timeoutAbortSource.signal.aborted) {
|
|
2898
|
+
// aborted by timeout
|
|
2899
|
+
throw new Error(
|
|
2900
|
+
`"${webServer.moduleUrl}" did not start a server in less than ${allocatedMs}ms (webServer.moduleUrl)`,
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
if (signal.aborted) {
|
|
2904
|
+
// aborted from outside
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
throw e;
|
|
2909
|
+
} finally {
|
|
2910
|
+
await startOperation.end();
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
|
|
2914
|
+
const startServerUsingCommand = async (
|
|
2915
|
+
webServer,
|
|
2916
|
+
{ signal, allocatedMs, logger, teardown },
|
|
2917
|
+
) => {
|
|
2918
|
+
const spawnedProcess = spawn(webServer.command, [], {
|
|
2919
|
+
// On non-windows platforms, `detached: true` makes child process a leader of a new
|
|
2920
|
+
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
|
|
2921
|
+
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
|
|
2922
|
+
detached: process.platform !== "win32",
|
|
2923
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2924
|
+
shell: true,
|
|
2925
|
+
cwd: webServer.cwd,
|
|
2926
|
+
});
|
|
2927
|
+
if (!spawnedProcess.pid) {
|
|
2928
|
+
await new Promise((resolve, reject) => {
|
|
2929
|
+
spawnedProcess.once("error", (error) => {
|
|
2930
|
+
reject(new Error(`Failed to launch: ${error}`));
|
|
2931
|
+
});
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
let errorReceived = false;
|
|
2936
|
+
const errorPromise = new Promise((resolve, reject) => {
|
|
2937
|
+
spawnedProcess.on("error", (e) => {
|
|
2938
|
+
errorReceived = true;
|
|
2939
|
+
reject(e);
|
|
2940
|
+
});
|
|
2941
|
+
});
|
|
2942
|
+
|
|
2943
|
+
// const stdout = readline.createInterface({ input: spawnedProcess.stdout });
|
|
2944
|
+
// stdout.on("line", () => {
|
|
2945
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][out] ${data}`);
|
|
2946
|
+
// });
|
|
2947
|
+
// const stderr = readline.createInterface({ input: spawnedProcess.stderr });
|
|
2948
|
+
// stderr.on("line", (data) => {
|
|
2949
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][err] ${data}`);
|
|
2950
|
+
// });
|
|
2951
|
+
let processClosed = false;
|
|
2952
|
+
const closedPromise = new Promise((resolve) => {
|
|
2953
|
+
spawnedProcess.once("exit", (exitCode, signal) => {
|
|
2954
|
+
logger.info(
|
|
2955
|
+
`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`,
|
|
2956
|
+
);
|
|
2957
|
+
processClosed = true;
|
|
2958
|
+
resolve();
|
|
2959
|
+
});
|
|
2960
|
+
});
|
|
2961
|
+
const killProcess = async () => {
|
|
2962
|
+
logger.info(`[pid=${spawnedProcess.pid}] <kill>`);
|
|
2963
|
+
if (!spawnedProcess.pid || spawnedProcess.killed || processClosed) {
|
|
2964
|
+
logger.info(
|
|
2965
|
+
`[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`,
|
|
2966
|
+
);
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
logger.info(`[pid=${spawnedProcess.pid}] <will force kill>`);
|
|
2970
|
+
// Force kill the browser.
|
|
2971
|
+
try {
|
|
2972
|
+
if (process.platform === "win32") {
|
|
2973
|
+
const taskkillProcess = spawnSync(
|
|
2974
|
+
`taskkill /pid ${spawnedProcess.pid} /T /F`,
|
|
2975
|
+
{ shell: true },
|
|
2976
|
+
);
|
|
2977
|
+
const [stdout, stderr] = [
|
|
2978
|
+
taskkillProcess.stdout.toString(),
|
|
2979
|
+
taskkillProcess.stderr.toString(),
|
|
2980
|
+
];
|
|
2981
|
+
if (stdout)
|
|
2982
|
+
logger.info(`[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`);
|
|
2983
|
+
if (stderr)
|
|
2984
|
+
logger.info(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
|
|
2985
|
+
} else {
|
|
2986
|
+
process.kill(-spawnedProcess.pid, "SIGKILL");
|
|
2987
|
+
}
|
|
2988
|
+
} catch (e) {
|
|
2989
|
+
logger.info(
|
|
2990
|
+
`[pid=${spawnedProcess.pid}] exception while trying to kill process: ${e}`,
|
|
2991
|
+
);
|
|
2992
|
+
// the process might have already stopped
|
|
2993
|
+
}
|
|
2994
|
+
await closedPromise;
|
|
2995
|
+
};
|
|
2996
|
+
|
|
2997
|
+
const startOperation = Abort.startOperation();
|
|
2998
|
+
startOperation.addAbortSignal(signal);
|
|
2999
|
+
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
3000
|
+
startOperation.addAbortCallback(killProcess);
|
|
3001
|
+
teardown.addCallback(killProcess);
|
|
3002
|
+
|
|
3003
|
+
const startedPromise = (async () => {
|
|
3004
|
+
const logScale = [100, 250, 500];
|
|
3005
|
+
// eslint-disable-next-line no-constant-condition
|
|
3006
|
+
while (true) {
|
|
3007
|
+
if (errorReceived) {
|
|
3008
|
+
break;
|
|
3009
|
+
}
|
|
3010
|
+
const connected = await pingServer(webServer.origin);
|
|
3011
|
+
if (connected) {
|
|
3012
|
+
break;
|
|
3013
|
+
}
|
|
3014
|
+
startOperation.throwIfAborted();
|
|
3015
|
+
const delay = logScale.shift() || 1000;
|
|
3016
|
+
logger.debug(`Waiting ${delay}ms`);
|
|
3017
|
+
await new Promise((x) => setTimeout(x, delay));
|
|
3018
|
+
}
|
|
3019
|
+
})();
|
|
3020
|
+
|
|
3021
|
+
try {
|
|
3022
|
+
await Promise.race([errorPromise, startedPromise]);
|
|
3023
|
+
} catch (e) {
|
|
3024
|
+
if (Abort.isAbortError(e)) {
|
|
3025
|
+
if (timeoutAbortSource.signal.aborted) {
|
|
3026
|
+
// aborted by timeout
|
|
3027
|
+
throw new Error(
|
|
3028
|
+
`"${webServer.command}" command did not start a server in less than ${allocatedMs}ms (webServer.command)`,
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
if (signal.aborted) {
|
|
3032
|
+
// aborted from outside
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
throw e;
|
|
3037
|
+
} finally {
|
|
3038
|
+
await startOperation.end();
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
|
|
2815
3042
|
const basicFetch = async (
|
|
2816
3043
|
url,
|
|
2817
3044
|
{ rejectUnauthorized = true, method = "GET", headers = {} } = {},
|
|
@@ -2866,50 +3093,39 @@ const basicFetch = async (
|
|
|
2866
3093
|
});
|
|
2867
3094
|
};
|
|
2868
3095
|
|
|
2869
|
-
const assertAndNormalizeWebServer = async (
|
|
3096
|
+
const assertAndNormalizeWebServer = async (
|
|
3097
|
+
webServer,
|
|
3098
|
+
{ signal, logger, teardown },
|
|
3099
|
+
) => {
|
|
2870
3100
|
if (!webServer) {
|
|
2871
3101
|
throw new TypeError(
|
|
2872
3102
|
`webServer is required when running tests on browser(s)`,
|
|
2873
3103
|
);
|
|
2874
3104
|
}
|
|
2875
3105
|
const unexpectedParamNames = Object.keys(webServer).filter((key) => {
|
|
2876
|
-
return ![
|
|
3106
|
+
return ![
|
|
3107
|
+
"origin",
|
|
3108
|
+
"moduleUrl",
|
|
3109
|
+
"command",
|
|
3110
|
+
"cwd",
|
|
3111
|
+
"rootDirectoryUrl",
|
|
3112
|
+
].includes(key);
|
|
2877
3113
|
});
|
|
2878
3114
|
if (unexpectedParamNames.length > 0) {
|
|
2879
3115
|
throw new TypeError(
|
|
2880
3116
|
`${unexpectedParamNames.join(",")}: there is no such param to webServer`,
|
|
2881
3117
|
);
|
|
2882
3118
|
}
|
|
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
|
-
}
|
|
3119
|
+
if (typeof webServer.origin !== "string") {
|
|
3120
|
+
throw new TypeError(
|
|
3121
|
+
`webServer.origin must be a string, got ${webServer.origin}`,
|
|
3122
|
+
);
|
|
2912
3123
|
}
|
|
3124
|
+
await ensureWebServerIsStarted(webServer, {
|
|
3125
|
+
signal,
|
|
3126
|
+
teardown,
|
|
3127
|
+
logger,
|
|
3128
|
+
});
|
|
2913
3129
|
const { headers } = await basicFetch(webServer.origin, {
|
|
2914
3130
|
method: "GET",
|
|
2915
3131
|
rejectUnauthorized: false,
|
|
@@ -2939,6 +3155,37 @@ const assertAndNormalizeWebServer = async (webServer) => {
|
|
|
2939
3155
|
}
|
|
2940
3156
|
};
|
|
2941
3157
|
|
|
3158
|
+
const ensureWebServerIsStarted = async (
|
|
3159
|
+
webServer,
|
|
3160
|
+
{ signal, teardown, logger, allocatedMs = 5_000 },
|
|
3161
|
+
) => {
|
|
3162
|
+
const aServerIsListening = await pingServer(webServer.origin);
|
|
3163
|
+
if (aServerIsListening) {
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
if (webServer.moduleUrl) {
|
|
3167
|
+
await startServerUsingModuleUrl(webServer, {
|
|
3168
|
+
signal,
|
|
3169
|
+
allocatedMs,
|
|
3170
|
+
teardown,
|
|
3171
|
+
logger,
|
|
3172
|
+
});
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
if (webServer.command) {
|
|
3176
|
+
await startServerUsingCommand(webServer, {
|
|
3177
|
+
signal,
|
|
3178
|
+
allocatedMs,
|
|
3179
|
+
teardown,
|
|
3180
|
+
logger,
|
|
3181
|
+
});
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
throw new TypeError(
|
|
3185
|
+
`webServer.moduleUrl or webServer.command is required as there is no server listening "${webServer.origin}"`,
|
|
3186
|
+
);
|
|
3187
|
+
};
|
|
3188
|
+
|
|
2942
3189
|
const executionStepsFromTestPlan = async ({
|
|
2943
3190
|
signal,
|
|
2944
3191
|
rootDirectoryUrl,
|
|
@@ -4209,7 +4456,7 @@ const executeSteps = async (
|
|
|
4209
4456
|
executionSteps,
|
|
4210
4457
|
{
|
|
4211
4458
|
signal,
|
|
4212
|
-
|
|
4459
|
+
teardown,
|
|
4213
4460
|
logger,
|
|
4214
4461
|
logRefresh,
|
|
4215
4462
|
logRuntime,
|
|
@@ -4249,23 +4496,9 @@ const executeSteps = async (
|
|
|
4249
4496
|
const executePlanReturnValue = {};
|
|
4250
4497
|
const report = {};
|
|
4251
4498
|
const callbacks = [];
|
|
4252
|
-
const stopAfterAllSignal = { notify: () => {} };
|
|
4253
4499
|
|
|
4254
4500
|
const multipleExecutionsOperation = Abort.startOperation();
|
|
4255
4501
|
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
4502
|
const failFastAbortController = new AbortController();
|
|
4270
4503
|
if (failFast) {
|
|
4271
4504
|
multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal);
|
|
@@ -4318,7 +4551,8 @@ const executeSteps = async (
|
|
|
4318
4551
|
coverageConfig,
|
|
4319
4552
|
coverageMethodForBrowsers,
|
|
4320
4553
|
coverageMethodForNodeJs,
|
|
4321
|
-
|
|
4554
|
+
isTestPlan: true,
|
|
4555
|
+
teardown,
|
|
4322
4556
|
};
|
|
4323
4557
|
|
|
4324
4558
|
if (logMergeForCompletedExecutions && !process.stdout.isTTY) {
|
|
@@ -4516,8 +4750,8 @@ const executeSteps = async (
|
|
|
4516
4750
|
},
|
|
4517
4751
|
});
|
|
4518
4752
|
if (!keepRunning) {
|
|
4519
|
-
logger.debug("
|
|
4520
|
-
await
|
|
4753
|
+
logger.debug("trigger test plan teardown");
|
|
4754
|
+
await teardown.trigger();
|
|
4521
4755
|
}
|
|
4522
4756
|
|
|
4523
4757
|
counters.cancelled = counters.total - counters.done;
|
|
@@ -4539,10 +4773,9 @@ const executeSteps = async (
|
|
|
4539
4773
|
executePlanReturnValue.aborted = multipleExecutionsOperation.signal.aborted;
|
|
4540
4774
|
executePlanReturnValue.planSummary = summary;
|
|
4541
4775
|
executePlanReturnValue.planReport = report;
|
|
4542
|
-
|
|
4543
|
-
await previous;
|
|
4776
|
+
for (const callback of callbacks) {
|
|
4544
4777
|
await callback();
|
|
4545
|
-
}
|
|
4778
|
+
}
|
|
4546
4779
|
return executePlanReturnValue;
|
|
4547
4780
|
} finally {
|
|
4548
4781
|
await multipleExecutionsOperation.end();
|
|
@@ -4708,6 +4941,25 @@ const executeTestPlan = async ({
|
|
|
4708
4941
|
coverageReportHtmlDirectoryUrl,
|
|
4709
4942
|
...rest
|
|
4710
4943
|
}) => {
|
|
4944
|
+
const teardown = createTeardown();
|
|
4945
|
+
|
|
4946
|
+
const operation = Abort.startOperation();
|
|
4947
|
+
operation.addAbortSignal(signal);
|
|
4948
|
+
if (handleSIGINT) {
|
|
4949
|
+
operation.addAbortSource((abort) => {
|
|
4950
|
+
return raceProcessTeardownEvents(
|
|
4951
|
+
{
|
|
4952
|
+
SIGINT: true,
|
|
4953
|
+
},
|
|
4954
|
+
() => {
|
|
4955
|
+
logger.debug(`SIGINT abort`);
|
|
4956
|
+
abort();
|
|
4957
|
+
},
|
|
4958
|
+
);
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4962
|
+
let logger;
|
|
4711
4963
|
let someNeedsServer = false;
|
|
4712
4964
|
let someHasCoverageV8 = false;
|
|
4713
4965
|
let someNodeRuntime = false;
|
|
@@ -4731,6 +4983,8 @@ const executeTestPlan = async ({
|
|
|
4731
4983
|
throw new Error(`testPlan must be an object, got ${testPlan}`);
|
|
4732
4984
|
}
|
|
4733
4985
|
|
|
4986
|
+
logger = createLogger({ logLevel });
|
|
4987
|
+
|
|
4734
4988
|
Object.keys(testPlan).forEach((filePattern) => {
|
|
4735
4989
|
const filePlan = testPlan[filePattern];
|
|
4736
4990
|
if (!filePlan) return;
|
|
@@ -4753,7 +5007,11 @@ const executeTestPlan = async ({
|
|
|
4753
5007
|
});
|
|
4754
5008
|
|
|
4755
5009
|
if (someNeedsServer) {
|
|
4756
|
-
await assertAndNormalizeWebServer(webServer
|
|
5010
|
+
await assertAndNormalizeWebServer(webServer, {
|
|
5011
|
+
signal: operation.signal,
|
|
5012
|
+
teardown,
|
|
5013
|
+
logger,
|
|
5014
|
+
});
|
|
4757
5015
|
}
|
|
4758
5016
|
|
|
4759
5017
|
if (coverageEnabled) {
|
|
@@ -4838,7 +5096,6 @@ const executeTestPlan = async ({
|
|
|
4838
5096
|
}
|
|
4839
5097
|
}
|
|
4840
5098
|
|
|
4841
|
-
const logger = createLogger({ logLevel });
|
|
4842
5099
|
logger.debug(
|
|
4843
5100
|
createDetailedMessage(`Prepare executing plan`, {
|
|
4844
5101
|
runtimes: JSON.stringify(runtimes, null, " "),
|
|
@@ -4893,7 +5150,7 @@ const executeTestPlan = async ({
|
|
|
4893
5150
|
|
|
4894
5151
|
const result = await executeSteps(executionSteps, {
|
|
4895
5152
|
signal,
|
|
4896
|
-
|
|
5153
|
+
teardown,
|
|
4897
5154
|
logger,
|
|
4898
5155
|
logRefresh,
|
|
4899
5156
|
logSummary,
|
|
@@ -5217,7 +5474,8 @@ const createRuntimeUsingPlaywright = ({
|
|
|
5217
5474
|
coverageMethodForBrowsers,
|
|
5218
5475
|
coverageFileUrl,
|
|
5219
5476
|
|
|
5220
|
-
|
|
5477
|
+
teardown,
|
|
5478
|
+
isTestPlan,
|
|
5221
5479
|
stopSignal,
|
|
5222
5480
|
keepRunning,
|
|
5223
5481
|
onConsole,
|
|
@@ -5236,7 +5494,7 @@ const createRuntimeUsingPlaywright = ({
|
|
|
5236
5494
|
await cleanupCallbackList.notify({ reason });
|
|
5237
5495
|
});
|
|
5238
5496
|
|
|
5239
|
-
const isBrowserDedicatedToExecution = isolatedTab || !
|
|
5497
|
+
const isBrowserDedicatedToExecution = isolatedTab || !isTestPlan;
|
|
5240
5498
|
let browserAndContextPromise = isBrowserDedicatedToExecution
|
|
5241
5499
|
? null
|
|
5242
5500
|
: browserPromiseCache.get(label);
|
|
@@ -5485,15 +5743,11 @@ const createRuntimeUsingPlaywright = ({
|
|
|
5485
5743
|
cleanupCallbackList.add(() => {
|
|
5486
5744
|
browser.removeListener("disconnected", disconnectedCallback);
|
|
5487
5745
|
});
|
|
5488
|
-
|
|
5489
|
-
stopAfterAllSignal.notify = async () => {
|
|
5490
|
-
await notifyPrevious();
|
|
5746
|
+
teardown.addCallback(async () => {
|
|
5491
5747
|
browser.removeListener("disconnected", disconnectedCallback);
|
|
5492
|
-
logger.debug(
|
|
5493
|
-
`stopAfterAllSignal notified -> closing ${browserName}`,
|
|
5494
|
-
);
|
|
5748
|
+
logger.debug(`testPlan teardown -> closing ${browserName}`);
|
|
5495
5749
|
await closeBrowser();
|
|
5496
|
-
};
|
|
5750
|
+
});
|
|
5497
5751
|
}
|
|
5498
5752
|
},
|
|
5499
5753
|
response: async (cb) => {
|
|
@@ -6840,6 +7094,7 @@ const execute = async ({
|
|
|
6840
7094
|
rootDirectoryUrl,
|
|
6841
7095
|
"rootDirectoryUrl",
|
|
6842
7096
|
);
|
|
7097
|
+
const teardown = createTeardown();
|
|
6843
7098
|
const executeOperation = Abort.startOperation();
|
|
6844
7099
|
executeOperation.addAbortSignal(signal);
|
|
6845
7100
|
if (handleSIGINT) {
|
|
@@ -6854,7 +7109,7 @@ const execute = async ({
|
|
|
6854
7109
|
}
|
|
6855
7110
|
|
|
6856
7111
|
if (runtime.type === "browser") {
|
|
6857
|
-
await assertAndNormalizeWebServer(webServer);
|
|
7112
|
+
await assertAndNormalizeWebServer(webServer, { signal, teardown, logger });
|
|
6858
7113
|
}
|
|
6859
7114
|
|
|
6860
7115
|
let resultTransformer = (result) => result;
|
|
@@ -6863,6 +7118,7 @@ const execute = async ({
|
|
|
6863
7118
|
webServer,
|
|
6864
7119
|
fileRelativeUrl,
|
|
6865
7120
|
importMap,
|
|
7121
|
+
teardown,
|
|
6866
7122
|
...runtimeParams,
|
|
6867
7123
|
};
|
|
6868
7124
|
|
|
@@ -6902,6 +7158,7 @@ const execute = async ({
|
|
|
6902
7158
|
}
|
|
6903
7159
|
return result;
|
|
6904
7160
|
} finally {
|
|
7161
|
+
await teardown.trigger();
|
|
6905
7162
|
await executeOperation.end();
|
|
6906
7163
|
}
|
|
6907
7164
|
};
|