@jsenv/core 27.0.3 → 27.2.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/controllable_child_process.mjs +139 -0
- package/dist/controllable_worker_thread.mjs +103 -0
- package/dist/js/execute_using_dynamic_import.js +169 -0
- package/dist/js/v8_coverage.js +539 -0
- package/dist/main.js +683 -818
- package/package.json +9 -8
- package/src/build/build.js +9 -12
- package/src/build/build_urls_generator.js +1 -1
- package/src/build/inject_global_version_mappings.js +3 -2
- package/src/build/inject_service_worker_urls.js +1 -2
- package/src/execute/run.js +50 -68
- package/src/execute/runtimes/browsers/chromium.js +1 -1
- package/src/execute/runtimes/browsers/firefox.js +1 -1
- package/src/execute/runtimes/browsers/from_playwright.js +13 -8
- package/src/execute/runtimes/browsers/webkit.js +1 -1
- package/src/execute/runtimes/node/{controllable_file.mjs → controllable_child_process.mjs} +18 -50
- package/src/execute/runtimes/node/controllable_worker_thread.mjs +103 -0
- package/src/execute/runtimes/node/execute_using_dynamic_import.js +49 -0
- package/src/execute/runtimes/node/exit_codes.js +9 -0
- package/src/execute/runtimes/node/{node_process.js → node_child_process.js} +56 -50
- package/src/execute/runtimes/node/node_worker_thread.js +268 -25
- package/src/execute/runtimes/node/profiler_v8_coverage.js +56 -0
- package/src/main.js +3 -1
- package/src/omega/kitchen.js +19 -6
- package/src/omega/server/file_service.js +2 -2
- package/src/omega/url_graph/url_graph_load.js +0 -1
- package/src/omega/url_graph.js +1 -0
- package/src/plugins/bundling/js_module/bundle_js_module.js +2 -5
- package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic.js +18 -15
- package/src/plugins/url_resolution/jsenv_plugin_url_resolution.js +2 -1
- package/src/test/coverage/report_to_coverage.js +16 -19
- package/src/test/coverage/v8_coverage.js +26 -0
- package/src/test/coverage/{v8_coverage_from_directory.js → v8_coverage_node_directory.js} +22 -26
- package/src/test/execute_plan.js +98 -91
- package/src/test/execute_test_plan.js +19 -13
- package/src/test/logs_file_execution.js +90 -13
- package/dist/js/controllable_file.mjs +0 -227
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { startJsCoverage } from "./profiler_v8_coverage.js"
|
|
2
|
+
import { startObservingPerformances } from "./node_execution_performance.js"
|
|
3
|
+
|
|
4
|
+
export const executeUsingDynamicImport = async ({
|
|
5
|
+
rootDirectoryUrl,
|
|
6
|
+
fileUrl,
|
|
7
|
+
collectPerformance,
|
|
8
|
+
coverageEnabled,
|
|
9
|
+
coverageConfig,
|
|
10
|
+
coverageMethodForNodeJs,
|
|
11
|
+
}) => {
|
|
12
|
+
let result = {}
|
|
13
|
+
const afterImportCallbacks = []
|
|
14
|
+
if (coverageEnabled && coverageMethodForNodeJs === "Profiler") {
|
|
15
|
+
const { filterV8Coverage } = await import(
|
|
16
|
+
"@jsenv/core/src/test/coverage/v8_coverage.js"
|
|
17
|
+
)
|
|
18
|
+
const { stopJsCoverage } = await startJsCoverage()
|
|
19
|
+
afterImportCallbacks.push(async () => {
|
|
20
|
+
const coverage = await stopJsCoverage()
|
|
21
|
+
const coverageLight = await filterV8Coverage(coverage, {
|
|
22
|
+
rootDirectoryUrl,
|
|
23
|
+
coverageConfig,
|
|
24
|
+
})
|
|
25
|
+
result.coverage = coverageLight
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
if (collectPerformance) {
|
|
29
|
+
const getPerformance = startObservingPerformances()
|
|
30
|
+
afterImportCallbacks.push(async () => {
|
|
31
|
+
const performance = await getPerformance()
|
|
32
|
+
result.performance = performance
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
const namespace = await import(fileUrl)
|
|
36
|
+
const namespaceResolved = {}
|
|
37
|
+
await Promise.all(
|
|
38
|
+
Object.keys(namespace).map(async (key) => {
|
|
39
|
+
const value = await namespace[key]
|
|
40
|
+
namespaceResolved[key] = value
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
result.namespace = namespaceResolved
|
|
44
|
+
await afterImportCallbacks.reduce(async (previous, afterImportCallback) => {
|
|
45
|
+
await previous
|
|
46
|
+
await afterImportCallback()
|
|
47
|
+
}, Promise.resolve())
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// https://nodejs.org/api/process.html#process_signal_events
|
|
2
|
+
const SIGINT_SIGNAL_NUMBER = 2
|
|
3
|
+
const SIGABORT_SIGNAL_NUMBER = 6
|
|
4
|
+
const SIGTERM_SIGNAL_NUMBER = 15
|
|
5
|
+
export const EXIT_CODES = {
|
|
6
|
+
SIGINT: 128 + SIGINT_SIGNAL_NUMBER,
|
|
7
|
+
SIGABORT: 128 + SIGABORT_SIGNAL_NUMBER,
|
|
8
|
+
SIGTERM: 128 + SIGTERM_SIGNAL_NUMBER,
|
|
9
|
+
}
|
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import { fork } from "node:child_process"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
2
3
|
import {
|
|
3
4
|
Abort,
|
|
4
5
|
raceCallbacks,
|
|
5
6
|
createCallbackListNotifiedOnce,
|
|
6
7
|
} from "@jsenv/abort"
|
|
7
|
-
import { uneval } from "@jsenv/uneval"
|
|
8
|
-
import { urlToFileSystemPath } from "@jsenv/urls"
|
|
9
8
|
import { createDetailedMessage } from "@jsenv/log"
|
|
10
9
|
import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
|
|
11
10
|
|
|
12
11
|
import { createChildExecOptions } from "./child_exec_options.js"
|
|
13
12
|
import { ExecOptions } from "./exec_options.js"
|
|
14
13
|
import { killProcessTree } from "./kill_process_tree.js"
|
|
14
|
+
import { EXIT_CODES } from "./exit_codes.js"
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
"./
|
|
16
|
+
const CONTROLLABLE_CHILD_PROCESS_URL = new URL(
|
|
17
|
+
"./controllable_child_process.mjs?entry_point",
|
|
18
18
|
import.meta.url,
|
|
19
19
|
).href
|
|
20
20
|
|
|
21
|
-
export const
|
|
22
|
-
|
|
21
|
+
export const nodeChildProcess = {
|
|
22
|
+
type: "node",
|
|
23
|
+
name: "node_child_process",
|
|
23
24
|
version: process.version.slice(1),
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
nodeChildProcess.run = async ({
|
|
27
28
|
signal = new AbortController().signal,
|
|
28
29
|
logger,
|
|
29
30
|
logProcessCommand = false,
|
|
@@ -35,14 +36,15 @@ nodeProcess.run = async ({
|
|
|
35
36
|
stopSignal,
|
|
36
37
|
onConsole,
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
coverageEnabled = false,
|
|
40
|
+
coverageConfig,
|
|
41
|
+
coverageMethodForNodeJs,
|
|
40
42
|
collectPerformance,
|
|
41
43
|
|
|
44
|
+
env,
|
|
42
45
|
debugPort,
|
|
43
46
|
debugMode,
|
|
44
47
|
debugModeInheritBreak,
|
|
45
|
-
env,
|
|
46
48
|
inheritProcessEnv = true,
|
|
47
49
|
commandLineOptions = [],
|
|
48
50
|
stdin = "pipe",
|
|
@@ -54,12 +56,9 @@ nodeProcess.run = async ({
|
|
|
54
56
|
}
|
|
55
57
|
env = {
|
|
56
58
|
...env,
|
|
57
|
-
COVERAGE_ENABLED: collectCoverage,
|
|
58
59
|
JSENV: true,
|
|
59
60
|
}
|
|
60
|
-
if (
|
|
61
|
-
// if we want to force istanbul, we will set process.env.NODE_V8_COVERAGE = ''
|
|
62
|
-
// into the child_process
|
|
61
|
+
if (coverageMethodForNodeJs !== "NODE_V8_COVERAGE") {
|
|
63
62
|
env.NODE_V8_COVERAGE = ""
|
|
64
63
|
}
|
|
65
64
|
commandLineOptions = [
|
|
@@ -87,11 +86,11 @@ nodeProcess.run = async ({
|
|
|
87
86
|
...env,
|
|
88
87
|
}
|
|
89
88
|
logger[logProcessCommand ? "info" : "debug"](
|
|
90
|
-
`${process.argv[0]} ${execArgv.join(" ")} ${
|
|
91
|
-
|
|
89
|
+
`${process.argv[0]} ${execArgv.join(" ")} ${fileURLToPath(
|
|
90
|
+
CONTROLLABLE_CHILD_PROCESS_URL,
|
|
92
91
|
)}`,
|
|
93
92
|
)
|
|
94
|
-
const childProcess = fork(
|
|
93
|
+
const childProcess = fork(fileURLToPath(CONTROLLABLE_CHILD_PROCESS_URL), {
|
|
95
94
|
execArgv,
|
|
96
95
|
// silent: true
|
|
97
96
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
@@ -114,9 +113,9 @@ nodeProcess.run = async ({
|
|
|
114
113
|
childProcess.stderr.pipe(stderr)
|
|
115
114
|
}
|
|
116
115
|
const childProcessReadyPromise = new Promise((resolve) => {
|
|
117
|
-
|
|
116
|
+
onceChildProcessMessage(childProcess, "ready", resolve)
|
|
118
117
|
})
|
|
119
|
-
const removeOutputListener =
|
|
118
|
+
const removeOutputListener = installChildProcessOutputListener(
|
|
120
119
|
childProcess,
|
|
121
120
|
({ type, text }) => {
|
|
122
121
|
onConsole({ type, text })
|
|
@@ -171,15 +170,15 @@ nodeProcess.run = async ({
|
|
|
171
170
|
// },
|
|
172
171
|
// https://nodejs.org/api/child_process.html#child_process_event_error
|
|
173
172
|
error: (cb) => {
|
|
174
|
-
return
|
|
173
|
+
return onceChildProcessEvent(childProcess, "error", cb)
|
|
175
174
|
},
|
|
176
175
|
exit: (cb) => {
|
|
177
|
-
return
|
|
176
|
+
return onceChildProcessEvent(childProcess, "exit", (code, signal) => {
|
|
178
177
|
cb({ code, signal })
|
|
179
178
|
})
|
|
180
179
|
},
|
|
181
180
|
response: (cb) => {
|
|
182
|
-
|
|
181
|
+
return onceChildProcessMessage(childProcess, "action-result", cb)
|
|
183
182
|
},
|
|
184
183
|
},
|
|
185
184
|
resolve,
|
|
@@ -189,11 +188,19 @@ nodeProcess.run = async ({
|
|
|
189
188
|
actionOperation.throwIfAborted()
|
|
190
189
|
await childProcessReadyPromise
|
|
191
190
|
actionOperation.throwIfAborted()
|
|
192
|
-
await
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
await sendToChildProcess(childProcess, {
|
|
192
|
+
type: "action",
|
|
193
|
+
data: {
|
|
194
|
+
actionType: "execute-using-dynamic-import",
|
|
195
|
+
actionParams: {
|
|
196
|
+
rootDirectoryUrl,
|
|
197
|
+
fileUrl: new URL(fileRelativeUrl, rootDirectoryUrl).href,
|
|
198
|
+
collectPerformance,
|
|
199
|
+
coverageEnabled,
|
|
200
|
+
coverageConfig,
|
|
201
|
+
coverageMethodForNodeJs,
|
|
202
|
+
exitAfterAction: true,
|
|
203
|
+
},
|
|
197
204
|
},
|
|
198
205
|
})
|
|
199
206
|
const winner = await winnerPromise
|
|
@@ -224,9 +231,9 @@ nodeProcess.run = async ({
|
|
|
224
231
|
if (
|
|
225
232
|
code === null ||
|
|
226
233
|
code === 0 ||
|
|
227
|
-
code ===
|
|
228
|
-
code ===
|
|
229
|
-
code ===
|
|
234
|
+
code === EXIT_CODES.SIGINT ||
|
|
235
|
+
code === EXIT_CODES.SIGTERM ||
|
|
236
|
+
code === EXIT_CODES.SIGABORT
|
|
230
237
|
) {
|
|
231
238
|
return {
|
|
232
239
|
status: "errored",
|
|
@@ -275,13 +282,6 @@ nodeProcess.run = async ({
|
|
|
275
282
|
return result
|
|
276
283
|
}
|
|
277
284
|
|
|
278
|
-
// https://nodejs.org/api/process.html#process_signal_events
|
|
279
|
-
const SIGINT_SIGNAL_NUMBER = 2
|
|
280
|
-
const SIGABORT_SIGNAL_NUMBER = 6
|
|
281
|
-
const SIGTERM_SIGNAL_NUMBER = 15
|
|
282
|
-
const SIGINT_EXIT_CODE = 128 + SIGINT_SIGNAL_NUMBER
|
|
283
|
-
const SIGABORT_EXIT_CODE = 128 + SIGABORT_SIGNAL_NUMBER
|
|
284
|
-
const SIGTERM_EXIT_CODE = 128 + SIGTERM_SIGNAL_NUMBER
|
|
285
285
|
// http://man7.org/linux/man-pages/man7/signal.7.html
|
|
286
286
|
// https:// github.com/nodejs/node/blob/1d9511127c419ec116b3ddf5fc7a59e8f0f1c1e4/lib/internal/child_process.js#L472
|
|
287
287
|
const GRACEFUL_STOP_SIGNAL = "SIGTERM"
|
|
@@ -290,20 +290,26 @@ const STOP_SIGNAL = "SIGKILL"
|
|
|
290
290
|
// but I'm not sure and it changes nothing so just use SIGKILL
|
|
291
291
|
const GRACEFUL_STOP_FAILED_SIGNAL = "SIGKILL"
|
|
292
292
|
|
|
293
|
-
const
|
|
294
|
-
const source = uneval(data, { functionAllowed: true })
|
|
293
|
+
const sendToChildProcess = async (childProcess, { type, data }) => {
|
|
295
294
|
return new Promise((resolve, reject) => {
|
|
296
|
-
childProcess.send(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
295
|
+
childProcess.send(
|
|
296
|
+
{
|
|
297
|
+
jsenv: true,
|
|
298
|
+
type,
|
|
299
|
+
data,
|
|
300
|
+
},
|
|
301
|
+
(error) => {
|
|
302
|
+
if (error) {
|
|
303
|
+
reject(error)
|
|
304
|
+
} else {
|
|
305
|
+
resolve()
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
)
|
|
303
309
|
})
|
|
304
310
|
}
|
|
305
311
|
|
|
306
|
-
const
|
|
312
|
+
const installChildProcessOutputListener = (childProcess, callback) => {
|
|
307
313
|
// beware that we may receive ansi output here, should not be a problem but keep that in mind
|
|
308
314
|
const stdoutDataCallback = (chunk) => {
|
|
309
315
|
callback({ type: "log", text: String(chunk) })
|
|
@@ -319,9 +325,9 @@ const installProcessOutputListener = (childProcess, callback) => {
|
|
|
319
325
|
}
|
|
320
326
|
}
|
|
321
327
|
|
|
322
|
-
const
|
|
328
|
+
const onceChildProcessMessage = (childProcess, type, callback) => {
|
|
323
329
|
const onmessage = (message) => {
|
|
324
|
-
if (message.type === type) {
|
|
330
|
+
if (message && message.jsenv && message.type === type) {
|
|
325
331
|
childProcess.removeListener("message", onmessage)
|
|
326
332
|
// eslint-disable-next-line no-eval
|
|
327
333
|
callback(message.data ? eval(`(${message.data})`) : "")
|
|
@@ -333,7 +339,7 @@ const onceProcessMessage = (childProcess, type, callback) => {
|
|
|
333
339
|
}
|
|
334
340
|
}
|
|
335
341
|
|
|
336
|
-
const
|
|
342
|
+
const onceChildProcessEvent = (childProcess, type, callback) => {
|
|
337
343
|
childProcess.once(type, callback)
|
|
338
344
|
return () => {
|
|
339
345
|
childProcess.removeListener(type, callback)
|
|
@@ -1,28 +1,271 @@
|
|
|
1
1
|
// https://github.com/avajs/ava/blob/576f534b345259055c95fa0c2b33bef10847a2af/lib/fork.js#L23
|
|
2
2
|
// https://nodejs.org/api/worker_threads.html
|
|
3
3
|
// https://github.com/avajs/ava/blob/576f534b345259055c95fa0c2b33bef10847a2af/lib/worker/base.js
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
4
|
+
import { Worker } from "node:worker_threads"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
import {
|
|
7
|
+
Abort,
|
|
8
|
+
createCallbackListNotifiedOnce,
|
|
9
|
+
raceCallbacks,
|
|
10
|
+
} from "@jsenv/abort"
|
|
11
|
+
import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
|
|
12
|
+
|
|
13
|
+
import { createChildExecOptions } from "./child_exec_options.js"
|
|
14
|
+
import { ExecOptions } from "./exec_options.js"
|
|
15
|
+
import { EXIT_CODES } from "./exit_codes.js"
|
|
16
|
+
|
|
17
|
+
const CONTROLLABLE_WORKER_THREAD_URL = new URL(
|
|
18
|
+
"./controllable_worker_thread.mjs?entry_point",
|
|
19
|
+
import.meta.url,
|
|
20
|
+
).href
|
|
21
|
+
|
|
22
|
+
export const nodeWorkerThread = {
|
|
23
|
+
type: "node",
|
|
24
|
+
name: "node_worker_thread",
|
|
25
|
+
version: process.version.slice(1),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
nodeWorkerThread.run = async ({
|
|
29
|
+
signal = new AbortController().signal,
|
|
30
|
+
// logger,
|
|
31
|
+
rootDirectoryUrl,
|
|
32
|
+
fileRelativeUrl,
|
|
33
|
+
|
|
34
|
+
keepRunning,
|
|
35
|
+
stopSignal,
|
|
36
|
+
onConsole,
|
|
37
|
+
|
|
38
|
+
coverageConfig,
|
|
39
|
+
coverageMethodForNodeJs,
|
|
40
|
+
coverageEnabled = false,
|
|
41
|
+
collectPerformance,
|
|
42
|
+
|
|
43
|
+
env,
|
|
44
|
+
debugPort,
|
|
45
|
+
debugMode,
|
|
46
|
+
debugModeInheritBreak,
|
|
47
|
+
inheritProcessEnv = true,
|
|
48
|
+
commandLineOptions = [],
|
|
49
|
+
}) => {
|
|
50
|
+
if (env !== undefined && typeof env !== "object") {
|
|
51
|
+
throw new TypeError(`env must be an object, got ${env}`)
|
|
52
|
+
}
|
|
53
|
+
env = {
|
|
54
|
+
...env,
|
|
55
|
+
JSENV: true,
|
|
56
|
+
}
|
|
57
|
+
if (coverageMethodForNodeJs !== "NODE_V8_COVERAGE") {
|
|
58
|
+
env.NODE_V8_COVERAGE = ""
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const workerThreadExecOptions = await createChildExecOptions({
|
|
62
|
+
signal,
|
|
63
|
+
debugPort,
|
|
64
|
+
debugMode,
|
|
65
|
+
debugModeInheritBreak,
|
|
66
|
+
})
|
|
67
|
+
const execArgvForWorkerThread = ExecOptions.toExecArgv({
|
|
68
|
+
...workerThreadExecOptions,
|
|
69
|
+
...ExecOptions.fromExecArgv(commandLineOptions),
|
|
70
|
+
})
|
|
71
|
+
const envForWorkerThread = {
|
|
72
|
+
...(inheritProcessEnv ? process.env : {}),
|
|
73
|
+
...env,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cleanupCallbackList = createCallbackListNotifiedOnce()
|
|
77
|
+
const cleanup = async (reason) => {
|
|
78
|
+
await cleanupCallbackList.notify({ reason })
|
|
79
|
+
}
|
|
80
|
+
const actionOperation = Abort.startOperation()
|
|
81
|
+
actionOperation.addAbortSignal(signal)
|
|
82
|
+
// https://nodejs.org/api/worker_threads.html#new-workerfilename-options
|
|
83
|
+
const workerThread = new Worker(
|
|
84
|
+
fileURLToPath(CONTROLLABLE_WORKER_THREAD_URL),
|
|
85
|
+
{
|
|
86
|
+
env: envForWorkerThread,
|
|
87
|
+
execArgv: execArgvForWorkerThread,
|
|
88
|
+
// workerData: { options },
|
|
89
|
+
// trackUnmanagedFds: true,
|
|
90
|
+
stdin: true,
|
|
91
|
+
stdout: true,
|
|
92
|
+
stderr: true,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
const removeOutputListener = installWorkerThreadOutputListener(
|
|
96
|
+
workerThread,
|
|
97
|
+
({ type, text }) => {
|
|
98
|
+
onConsole({ type, text })
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
const workerThreadReadyPromise = new Promise((resolve) => {
|
|
102
|
+
onceWorkerThreadMessage(workerThread, "ready", resolve)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const stop = memoize(async () => {
|
|
106
|
+
await workerThreadReadyPromise
|
|
107
|
+
await workerThread.terminate()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const winnerPromise = new Promise((resolve) => {
|
|
111
|
+
raceCallbacks(
|
|
112
|
+
{
|
|
113
|
+
aborted: (cb) => {
|
|
114
|
+
return actionOperation.addAbortCallback(cb)
|
|
115
|
+
},
|
|
116
|
+
error: (cb) => {
|
|
117
|
+
return onceWorkerThreadEvent(workerThread, "error", cb)
|
|
118
|
+
},
|
|
119
|
+
exit: (cb) => {
|
|
120
|
+
return onceWorkerThreadEvent(workerThread, "exit", (code, signal) => {
|
|
121
|
+
cb({ code, signal })
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
response: (cb) => {
|
|
125
|
+
return onceWorkerThreadMessage(workerThread, "action-result", cb)
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
resolve,
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const getResult = async () => {
|
|
133
|
+
actionOperation.throwIfAborted()
|
|
134
|
+
await workerThreadReadyPromise
|
|
135
|
+
actionOperation.throwIfAborted()
|
|
136
|
+
await sendToWorkerThread(workerThread, {
|
|
137
|
+
type: "action",
|
|
138
|
+
data: {
|
|
139
|
+
actionType: "execute-using-dynamic-import",
|
|
140
|
+
actionParams: {
|
|
141
|
+
rootDirectoryUrl,
|
|
142
|
+
fileUrl: new URL(fileRelativeUrl, rootDirectoryUrl).href,
|
|
143
|
+
collectPerformance,
|
|
144
|
+
coverageEnabled,
|
|
145
|
+
coverageConfig,
|
|
146
|
+
coverageMethodForNodeJs,
|
|
147
|
+
exitAfterAction: true,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
const winner = await winnerPromise
|
|
152
|
+
if (winner.name === "aborted") {
|
|
153
|
+
return {
|
|
154
|
+
status: "aborted",
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (winner.name === "error") {
|
|
158
|
+
const error = winner.data
|
|
159
|
+
removeOutputListener()
|
|
160
|
+
return {
|
|
161
|
+
status: "errored",
|
|
162
|
+
error,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (winner.name === "exit") {
|
|
166
|
+
const { code } = winner.data
|
|
167
|
+
await cleanup("process exit")
|
|
168
|
+
if (code === 12) {
|
|
169
|
+
return {
|
|
170
|
+
status: "errored",
|
|
171
|
+
error: new Error(
|
|
172
|
+
`node process exited with 12 (the forked child process wanted to use a non-available port for debug)`,
|
|
173
|
+
),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (
|
|
177
|
+
code === null ||
|
|
178
|
+
code === 0 ||
|
|
179
|
+
code === EXIT_CODES.SIGINT ||
|
|
180
|
+
code === EXIT_CODES.SIGTERM ||
|
|
181
|
+
code === EXIT_CODES.SIGABORT
|
|
182
|
+
) {
|
|
183
|
+
return {
|
|
184
|
+
status: "errored",
|
|
185
|
+
error: new Error(`node worker thread exited during execution`),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// process.exit(1) in child process or process.exitCode = 1 + process.exit()
|
|
189
|
+
// means there was an error even if we don't know exactly what.
|
|
190
|
+
return {
|
|
191
|
+
status: "errored",
|
|
192
|
+
error: new Error(
|
|
193
|
+
`node worker thread exited with code ${code} during execution`,
|
|
194
|
+
),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const { status, value } = winner.data
|
|
198
|
+
if (status === "action-failed") {
|
|
199
|
+
return {
|
|
200
|
+
status: "errored",
|
|
201
|
+
error: value,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const { namespace, performance, coverage } = value
|
|
205
|
+
return {
|
|
206
|
+
status: "completed",
|
|
207
|
+
namespace,
|
|
208
|
+
performance,
|
|
209
|
+
coverage,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let result
|
|
214
|
+
try {
|
|
215
|
+
result = await getResult()
|
|
216
|
+
} catch (e) {
|
|
217
|
+
result = {
|
|
218
|
+
status: "errored",
|
|
219
|
+
error: e,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (keepRunning) {
|
|
224
|
+
stopSignal.notify = stop
|
|
225
|
+
} else {
|
|
226
|
+
await stop()
|
|
227
|
+
}
|
|
228
|
+
await actionOperation.end()
|
|
229
|
+
return result
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const installWorkerThreadOutputListener = (workerThread, callback) => {
|
|
233
|
+
// beware that we may receive ansi output here, should not be a problem but keep that in mind
|
|
234
|
+
const stdoutDataCallback = (chunk) => {
|
|
235
|
+
callback({ type: "log", text: String(chunk) })
|
|
236
|
+
}
|
|
237
|
+
workerThread.stdout.on("data", stdoutDataCallback)
|
|
238
|
+
const stdErrorDataCallback = (chunk) => {
|
|
239
|
+
callback({ type: "error", text: String(chunk) })
|
|
240
|
+
}
|
|
241
|
+
workerThread.stderr.on("data", stdErrorDataCallback)
|
|
242
|
+
return () => {
|
|
243
|
+
workerThread.stdout.removeListener("data", stdoutDataCallback)
|
|
244
|
+
workerThread.stderr.removeListener("data", stdoutDataCallback)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const sendToWorkerThread = (worker, { type, data }) => {
|
|
249
|
+
worker.postMessage({ jsenv: true, type, data })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const onceWorkerThreadMessage = (workerThread, type, callback) => {
|
|
253
|
+
const onmessage = (message) => {
|
|
254
|
+
if (message && message.jsenv && message.type === type) {
|
|
255
|
+
workerThread.removeListener("message", onmessage)
|
|
256
|
+
// eslint-disable-next-line no-eval
|
|
257
|
+
callback(message.data ? eval(`(${message.data})`) : undefined)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
workerThread.on("message", onmessage)
|
|
261
|
+
return () => {
|
|
262
|
+
workerThread.removeListener("message", onmessage)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const onceWorkerThreadEvent = (worker, type, callback) => {
|
|
267
|
+
worker.once(type, callback)
|
|
268
|
+
return () => {
|
|
269
|
+
worker.removeListener(type, callback)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Calling Profiler.startPreciseCoverage DO NOT propagate to
|
|
3
|
+
* subprocesses (new Worker or child_process.fork())
|
|
4
|
+
* So the best solution remains NODE_V8_COVERAGE
|
|
5
|
+
* This profiler strategy remains useful when:
|
|
6
|
+
* - As fallback when NODE_V8_COVERAGE is not configured
|
|
7
|
+
* - If explicitely enabled with coverageMethodForNodeJs: 'Profiler'
|
|
8
|
+
* - Used by jsenv during automated tests about coverage
|
|
9
|
+
* - Anyone prefering this approach over NODE_V8_COVERAGE and assuming
|
|
10
|
+
* it will not fork subprocess or don't care if coverage is missed for this code
|
|
11
|
+
* - https://v8.dev/blog/javascript-code-coverage#for-embedders
|
|
12
|
+
* - https://github.com/nodejs/node/issues/28283
|
|
13
|
+
* - https://vanilla.aslushnikov.com/?Profiler.startPreciseCoverage
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Session } from "node:inspector"
|
|
17
|
+
|
|
18
|
+
export const startJsCoverage = async ({
|
|
19
|
+
callCount = true,
|
|
20
|
+
detailed = true,
|
|
21
|
+
} = {}) => {
|
|
22
|
+
const session = new Session()
|
|
23
|
+
session.connect()
|
|
24
|
+
const postSession = (action, options) => {
|
|
25
|
+
const promise = new Promise((resolve, reject) => {
|
|
26
|
+
session.post(action, options, (error, data) => {
|
|
27
|
+
if (error) {
|
|
28
|
+
reject(error)
|
|
29
|
+
} else {
|
|
30
|
+
resolve(data)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
return promise
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await postSession("Profiler.enable")
|
|
38
|
+
await postSession("Profiler.startPreciseCoverage", { callCount, detailed })
|
|
39
|
+
|
|
40
|
+
const takeJsCoverage = async () => {
|
|
41
|
+
const coverage = await postSession("Profiler.takePreciseCoverage")
|
|
42
|
+
return coverage
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stopJsCoverage = async () => {
|
|
46
|
+
const coverage = await takeJsCoverage()
|
|
47
|
+
await postSession("Profiler.stopPreciseCoverage")
|
|
48
|
+
await postSession("Profiler.disable")
|
|
49
|
+
return coverage
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
takeJsCoverage,
|
|
54
|
+
stopJsCoverage,
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { startDevServer } from "./dev/start_dev_server.js"
|
|
3
3
|
// test
|
|
4
4
|
export { executeTestPlan } from "./test/execute_test_plan.js"
|
|
5
|
+
// runtimes (used to execute tests)
|
|
5
6
|
export {
|
|
6
7
|
chromium,
|
|
7
8
|
chromiumIsolatedTab,
|
|
@@ -14,7 +15,8 @@ export {
|
|
|
14
15
|
webkit,
|
|
15
16
|
webkitIsolatedTab,
|
|
16
17
|
} from "./execute/runtimes/browsers/webkit.js"
|
|
17
|
-
export {
|
|
18
|
+
export { nodeChildProcess } from "./execute/runtimes/node/node_child_process.js"
|
|
19
|
+
export { nodeWorkerThread } from "./execute/runtimes/node/node_worker_thread.js"
|
|
18
20
|
// build
|
|
19
21
|
export { build } from "./build/build.js"
|
|
20
22
|
export { startBuildServer } from "./build/start_build_server.js"
|