@jsenv/core 23.0.9 → 23.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/jsenv_browser_system.js +14 -2
  2. package/dist/jsenv_browser_system.js.map +4 -4
  3. package/dist/jsenv_toolbar.js +4 -0
  4. package/dist/jsenv_toolbar.js.map +2 -2
  5. package/dist/jsenv_toolbar_injector.js +129 -107
  6. package/dist/jsenv_toolbar_injector.js.map +4 -2
  7. package/package.json +4 -3
  8. package/src/execute.js +12 -0
  9. package/src/internal/compiling/compile-directory/updateMeta.js +5 -1
  10. package/src/internal/compiling/createCompiledFileService.js +6 -6
  11. package/src/internal/compiling/jsenvCompilerForHtml.js +21 -7
  12. package/src/internal/compiling/jsenvCompilerForJavaScript.js +2 -0
  13. package/src/internal/compiling/startCompileServer.js +32 -27
  14. package/src/internal/executing/coverage/v8CoverageFromNodeV8Directory.js +35 -18
  15. package/src/internal/executing/executionLogs.js +12 -3
  16. package/src/internal/executing/launchAndExecute.js +24 -15
  17. package/src/internal/logs/log_style.js +13 -1
  18. package/src/internal/node-launcher/createControllableNodeProcess.js +97 -45
  19. package/src/internal/node-launcher/nodeControllableFile.mjs +1 -1
  20. package/src/internal/runtime/createBrowserRuntime/createBrowserSystem.js +18 -3
  21. package/src/internal/toolbar/execution/toolbar.execution.js +4 -0
  22. package/src/internal/toolbar/toolbar.html +157 -61
  23. package/src/internal/toolbar/toolbar.injector.js +8 -0
  24. package/src/launchBrowser.js +67 -49
  25. package/src/launchNode.js +3 -7
  26. package/src/startExploring.js +4 -0
  27. package/src/internal/toolbar/animation/toolbar-movie-icon.svg +0 -15
  28. package/src/internal/toolbar/compilation/flask.svg +0 -7
  29. package/src/internal/toolbar/compilation/info.svg +0 -9
  30. package/src/internal/toolbar/compilation/loupe.svg +0 -11
  31. package/src/internal/toolbar/compilation/toolbar_compilation.svg +0 -11
  32. package/src/internal/toolbar/eventsource/toolbar-power-icon.svg +0 -10
  33. package/src/internal/toolbar/eventsource/toolbar-power-off-icon.svg +0 -10
  34. package/src/internal/toolbar/responsive/toolbar-dots-icon.svg +0 -10
  35. package/src/internal/toolbar/settings/toolbar-settings-icon.svg +0 -9
  36. package/src/internal/toolbar/theme/toolbar-palette-icon.svg +0 -10
  37. package/src/internal/toolbar/toolbar-cross-icon.svg +0 -10
  38. package/src/internal/toolbar/toolbar-loading-icon.svg +0 -102
  39. package/src/internal/toolbar/toolbar-notif-icon.svg +0 -9
@@ -1,4 +1,4 @@
1
- import { serveFile } from "@jsenv/server"
1
+ import { serveFile, nextService } from "@jsenv/server"
2
2
  import {
3
3
  resolveUrl,
4
4
  resolveDirectoryUrl,
@@ -31,7 +31,6 @@ const jsenvCompilers = {
31
31
  }
32
32
 
33
33
  export const createCompiledFileService = ({
34
- sourceFileService,
35
34
  cancellationToken,
36
35
  logger,
37
36
 
@@ -51,6 +50,7 @@ export const createCompiledFileService = ({
51
50
  projectFileRequestedCallback,
52
51
  useFilesystemAsCache,
53
52
  compileCacheStrategy,
53
+ sourcemapMethod,
54
54
  sourcemapExcludeSources,
55
55
  }) => {
56
56
  Object.keys(customCompilers).forEach((key) => {
@@ -80,7 +80,7 @@ export const createCompiledFileService = ({
80
80
 
81
81
  // not inside compile directory -> nothing to compile
82
82
  if (!requestCompileInfo.insideCompileDirectory) {
83
- return sourceFileService(request)
83
+ return null
84
84
  }
85
85
 
86
86
  const { compileId, afterCompileId } = requestCompileInfo
@@ -110,7 +110,7 @@ export const createCompiledFileService = ({
110
110
 
111
111
  // nothing after compileId, we don't know what to compile (not supposed to happen)
112
112
  if (afterCompileId === "") {
113
- return sourceFileService(request)
113
+ return null
114
114
  }
115
115
 
116
116
  const originalFileRelativeUrl = afterCompileId
@@ -132,7 +132,7 @@ export const createCompiledFileService = ({
132
132
  // we don't redirect otherwise it complexify ressource tracking
133
133
  // and url resolution
134
134
  if (!compiler) {
135
- return sourceFileService({
135
+ return nextService({
136
136
  ...request,
137
137
  ressource: `/${originalFileRelativeUrl}`,
138
138
  })
@@ -175,7 +175,7 @@ export const createCompiledFileService = ({
175
175
  groupMap,
176
176
  }),
177
177
 
178
- sourcemapMethod: "comment", // "inline" is also possible
178
+ sourcemapMethod,
179
179
  sourcemapExcludeSources,
180
180
  jsenvToolbarInjection,
181
181
  })
@@ -6,7 +6,10 @@ import {
6
6
  jsenvToolbarInjectorFileInfo,
7
7
  } from "@jsenv/core/src/internal/jsenvInternalFiles.js"
8
8
  import { getDefaultImportMap } from "@jsenv/core/src/internal/import-resolution/importmap-default.js"
9
- import { setJavaScriptSourceMappingUrl } from "../sourceMappingURLUtils.js"
9
+ import {
10
+ setJavaScriptSourceMappingUrl,
11
+ sourcemapToBase64Url,
12
+ } from "../sourceMappingURLUtils.js"
10
13
  import { transformJs } from "./js-compilation-service/transformJs.js"
11
14
  import {
12
15
  parseHtmlString,
@@ -41,6 +44,7 @@ export const compileHtml = async ({
41
44
  babelPluginMap,
42
45
 
43
46
  jsenvToolbarInjection,
47
+ sourcemapMethod,
44
48
  }) => {
45
49
  const jsenvBrowserBuildUrlRelativeToProject = urlToRelativeUrl(
46
50
  jsenvBrowserSystemFileInfo.jsenvBuildUrl,
@@ -203,12 +207,22 @@ export const compileHtml = async ({
203
207
  sourcemapFileUrl,
204
208
  compiledUrl,
205
209
  )
206
- code = setJavaScriptSourceMappingUrl(
207
- code,
208
- sourcemapFileRelativePathForModule,
209
- )
210
- assets = [...assets, scriptAssetUrl, sourcemapFileUrl]
211
- assetsContent = [...assetsContent, code, JSON.stringify(map, null, " ")]
210
+
211
+ if (sourcemapMethod === "inline") {
212
+ code = setJavaScriptSourceMappingUrl(code, sourcemapToBase64Url(map))
213
+ } else {
214
+ // TODO: respect "sourcemapMethod" parameter
215
+ code = setJavaScriptSourceMappingUrl(
216
+ code,
217
+ sourcemapFileRelativePathForModule,
218
+ )
219
+ assets = [...assets, scriptAssetUrl, sourcemapFileUrl]
220
+ assetsContent = [
221
+ ...assetsContent,
222
+ code,
223
+ JSON.stringify(map, null, " "),
224
+ ]
225
+ }
212
226
  }),
213
227
  )
214
228
 
@@ -14,6 +14,7 @@ export const compileJavascript = async ({
14
14
  importMetaFormat,
15
15
 
16
16
  sourcemapExcludeSources,
17
+ sourcemapMethod,
17
18
  }) => {
18
19
  const transformResult = await transformJs({
19
20
  code,
@@ -42,6 +43,7 @@ export const compileJavascript = async ({
42
43
  compiledFileUrl: compiledUrl,
43
44
  sourcemapFileUrl: `${compiledUrl}.map`,
44
45
  sourcemapExcludeSources,
46
+ sourcemapMethod,
45
47
  },
46
48
  )
47
49
  }
@@ -59,6 +59,7 @@ export const startCompileServer = async ({
59
59
  jsenvDirectoryClean = false,
60
60
  outDirectoryName = "out",
61
61
 
62
+ sourcemapMethod = "comment", // "inline" is also possible
62
63
  sourcemapExcludeSources = false, // this should increase perf (no need to download source for browser)
63
64
  compileServerCanReadFromFilesystem = true,
64
65
  compileServerCanWriteOnFilesystem = true,
@@ -269,6 +270,8 @@ export const startCompileServer = async ({
269
270
  babelPluginMap,
270
271
  customCompilers,
271
272
  jsenvToolbarInjection,
273
+ sourcemapMethod,
274
+ sourcemapExcludeSources,
272
275
  })
273
276
  if (compileServerCanWriteOnFilesystem) {
274
277
  await setupOutDirectory({
@@ -280,31 +283,6 @@ export const startCompileServer = async ({
280
283
  })
281
284
  }
282
285
 
283
- const sourceFileService = composeServicesWithTiming({
284
- ...(transformHtmlSourceFiles
285
- ? {
286
- "service:transform html source file":
287
- createTransformHtmlSourceFileService({
288
- logger,
289
- projectDirectoryUrl,
290
- inlineImportMapIntoHTML,
291
- jsenvScriptInjection,
292
- jsenvToolbarInjection,
293
- }),
294
- }
295
- : {}),
296
- "service:source file": createSourceFileService({
297
- logger,
298
- projectDirectoryUrl,
299
- projectFileRequestedCallback,
300
- projectFileEtagEnabled,
301
- transformHtmlSourceFiles,
302
- inlineImportMapIntoHTML,
303
- jsenvScriptInjection,
304
- jsenvToolbarInjection,
305
- }),
306
- })
307
-
308
286
  const jsenvServices = {
309
287
  "service:compilation asset": createCompilationAssetFileService({
310
288
  projectDirectoryUrl,
@@ -324,7 +302,6 @@ export const startCompileServer = async ({
324
302
  projectDirectoryUrl,
325
303
  }),
326
304
  "service:compiled file": createCompiledFileService({
327
- sourceFileService,
328
305
  cancellationToken,
329
306
  logger,
330
307
 
@@ -345,9 +322,32 @@ export const startCompileServer = async ({
345
322
  projectFileRequestedCallback,
346
323
  useFilesystemAsCache: compileServerCanReadFromFilesystem,
347
324
  writeOnFilesystem: compileServerCanWriteOnFilesystem,
325
+ sourcemapMethod,
348
326
  sourcemapExcludeSources,
349
327
  compileCacheStrategy,
350
328
  }),
329
+ ...(transformHtmlSourceFiles
330
+ ? {
331
+ "service:transform html source file":
332
+ createTransformHtmlSourceFileService({
333
+ logger,
334
+ projectDirectoryUrl,
335
+ inlineImportMapIntoHTML,
336
+ jsenvScriptInjection,
337
+ jsenvToolbarInjection,
338
+ }),
339
+ }
340
+ : {}),
341
+ "service:source file": createSourceFileService({
342
+ logger,
343
+ projectDirectoryUrl,
344
+ projectFileRequestedCallback,
345
+ projectFileEtagEnabled,
346
+ transformHtmlSourceFiles,
347
+ inlineImportMapIntoHTML,
348
+ jsenvScriptInjection,
349
+ jsenvToolbarInjection,
350
+ }),
351
351
  }
352
352
 
353
353
  const compileServer = await startServer({
@@ -361,7 +361,8 @@ export const startCompileServer = async ({
361
361
  ip: compileServerIp,
362
362
  port: compileServerPort,
363
363
  sendServerTiming: true,
364
- nagle: false,
364
+ // nagle: false,
365
+ requestWaitingMs: 60 * 1000,
365
366
  sendServerInternalErrorDetails: true,
366
367
  requestToResponse: composeServicesWithTiming({
367
368
  ...customServices,
@@ -945,6 +946,8 @@ const createOutJSONFiles = ({
945
946
  inlineImportMapIntoHTML,
946
947
  customCompilers,
947
948
  jsenvToolbarInjection,
949
+ sourcemapMethod,
950
+ sourcemapExcludeSources,
948
951
  }) => {
949
952
  const outJSONFiles = {}
950
953
  const outDirectoryUrl = resolveUrl(
@@ -968,6 +971,8 @@ const createOutJSONFiles = ({
968
971
  processEnvNodeEnv,
969
972
  customCompilerPatterns,
970
973
  jsenvToolbarInjection,
974
+ sourcemapMethod,
975
+ sourcemapExcludeSources,
971
976
  }
972
977
  outJSONFiles.meta = {
973
978
  url: metaOutFileUrl,
@@ -4,6 +4,7 @@ import {
4
4
  readFile,
5
5
  resolveUrl,
6
6
  } from "@jsenv/filesystem"
7
+ import { createDetailedMessage } from "@jsenv/logger"
7
8
 
8
9
  import { v8CoverageFromAllV8Coverages } from "./v8CoverageFromAllV8Coverages.js"
9
10
 
@@ -12,7 +13,9 @@ export const v8CoverageFromNodeV8Directory = async ({
12
13
  NODE_V8_COVERAGE,
13
14
  coverageConfig,
14
15
  }) => {
15
- const allV8Coverages = await readV8CoverageReportsFromDirectory(NODE_V8_COVERAGE)
16
+ const allV8Coverages = await readV8CoverageReportsFromDirectory(
17
+ NODE_V8_COVERAGE,
18
+ )
16
19
 
17
20
  const v8Coverage = v8CoverageFromAllV8Coverages(allV8Coverages, {
18
21
  coverageRootUrl: projectDirectoryUrl,
@@ -28,33 +31,47 @@ const readV8CoverageReportsFromDirectory = async (coverageDirectory) => {
28
31
  if (dirContent.length > 0) {
29
32
  return dirContent
30
33
  }
31
- if (timeSpentTrying > 1500) {
32
- console.warn(`v8 coverage directory is empty at ${coverageDirectory}`)
33
- return dirContent
34
+ if (timeSpentTrying < 800) {
35
+ await new Promise((resolve) => setTimeout(resolve, 100))
36
+ return tryReadDirectory(timeSpentTrying + 100)
34
37
  }
35
- await new Promise((resolve) => setTimeout(resolve, 100))
36
- return tryReadDirectory(timeSpentTrying + 100)
38
+ console.warn(`v8 coverage directory is empty at ${coverageDirectory}`)
39
+ return dirContent
37
40
  }
38
41
  const dirContent = await tryReadDirectory()
39
42
 
40
43
  const coverageReports = []
41
-
42
44
  const coverageDirectoryUrl = assertAndNormalizeDirectoryUrl(coverageDirectory)
43
45
  await Promise.all(
44
46
  dirContent.map(async (dirEntry) => {
45
47
  const dirEntryUrl = resolveUrl(dirEntry, coverageDirectoryUrl)
46
- try {
47
- const fileContent = await readFile(dirEntryUrl, { as: "json" })
48
- if (fileContent) {
49
- coverageReports.push(fileContent)
48
+ const tryReadJsonFile = async (timeSpentTrying = 0) => {
49
+ try {
50
+ const fileContent = await readFile(dirEntryUrl, { as: "json" })
51
+ return fileContent
52
+ } catch (e) {
53
+ if (e.name === "SyntaxError") {
54
+ // If there is a syntax error it's likely because Node.js
55
+ // is not done writing the json file content
56
+ // -> let's retry to read file after 100ms
57
+ if (timeSpentTrying < 100) {
58
+ await new Promise((resolve) => setTimeout(resolve, 100))
59
+ return tryReadJsonFile(timeSpentTrying + 100)
60
+ }
61
+ }
62
+ console.warn(
63
+ createDetailedMessage(`Error while reading coverage file`, {
64
+ "error stack": e.stack,
65
+ "file": dirEntryUrl,
66
+ }),
67
+ )
68
+ return null
50
69
  }
51
- } catch (e) {
52
- console.warn(`Error while reading coverage file
53
- --- error stack ---
54
- ${e.stack}
55
- --- file ---
56
- ${dirEntryUrl}`)
57
- return
70
+ }
71
+
72
+ const fileContent = await tryReadJsonFile()
73
+ if (fileContent) {
74
+ coverageReports.push(fileContent)
58
75
  }
59
76
  }),
60
77
  )
@@ -54,7 +54,7 @@ file: ${fileRelativeUrl}
54
54
  runtime: ${runtime}${appendDuration({
55
55
  startMs,
56
56
  endMs,
57
- })}${appendConsole(consoleCalls)}${appendError(error)}`
57
+ })}${appendConsole(consoleCalls)}${appendError(error, runtimeName)}`
58
58
  }
59
59
 
60
60
  const descriptionFormatters = {
@@ -107,8 +107,17 @@ ${consoleOutputTrimmed}
107
107
  ${setANSIColor(`-------------------------`, ANSI_GREY)}`
108
108
  }
109
109
 
110
- const appendError = (error) => {
111
- if (!error) return ``
110
+ const appendError = (error, runtimeName) => {
111
+ if (!error) {
112
+ return ``
113
+ }
114
+
115
+ if (runtimeName === "webkit") {
116
+ return `
117
+ error: ${error.message}
118
+ at ${error.stack}`
119
+ }
120
+
112
121
  return `
113
122
  error: ${error.stack}`
114
123
  }
@@ -47,7 +47,10 @@ export const launchAndExecute = async ({
47
47
  runtimeConsoleCallback = () => {},
48
48
  runtimeStartedCallback = () => {},
49
49
  runtimeStoppedCallback = () => {},
50
- runtimeErrorCallback = () => {},
50
+ runtimeErrorAfterExecutionCallback = (error) => {
51
+ // by default throw on error after execution
52
+ throw error
53
+ },
51
54
  runtimeDisconnectCallback = () => {},
52
55
 
53
56
  coverageV8MergeConflictIsExpected,
@@ -205,7 +208,7 @@ export const launchAndExecute = async ({
205
208
  stopAfterExecuteReason,
206
209
  gracefulStopAllocatedMs,
207
210
  runtimeConsoleCallback,
208
- runtimeErrorCallback,
211
+ runtimeErrorAfterExecutionCallback,
209
212
  runtimeDisconnectCallback,
210
213
  runtimeStartedCallback,
211
214
  runtimeStoppedCallback,
@@ -280,7 +283,7 @@ const computeExecutionResult = async ({
280
283
  runtimeStartedCallback,
281
284
  runtimeStoppedCallback,
282
285
  runtimeConsoleCallback,
283
- runtimeErrorCallback,
286
+ runtimeErrorAfterExecutionCallback,
284
287
  runtimeDisconnectCallback,
285
288
  }) => {
286
289
  const runtimeLabel = `${runtime.name}/${runtime.version}`
@@ -387,17 +390,6 @@ const computeExecutionResult = async ({
387
390
 
388
391
  timing = TIMING_DURING_EXECUTION
389
392
 
390
- registerErrorCallback((error) => {
391
- logger.error(
392
- createDetailedMessage(`error ${timing}.`, {
393
- ["error stack"]: error.stack,
394
- ["execute params"]: JSON.stringify(executeParams, null, " "),
395
- ["runtime"]: runtimeLabel,
396
- }),
397
- )
398
- runtimeErrorCallback({ error, timing })
399
- })
400
-
401
393
  const raceResult = await promiseTrackRace([disconnected, executed])
402
394
  timing = TIMING_AFTER_EXECUTION
403
395
 
@@ -406,7 +398,24 @@ const computeExecutionResult = async ({
406
398
  }
407
399
 
408
400
  if (stopAfterExecute) {
409
- launchOperation.stop(stopAfterExecuteReason)
401
+ // if there is an error while stopping the runtine
402
+ // the execution is considered as failed
403
+ try {
404
+ await launchOperation.stop(stopAfterExecuteReason)
405
+ } catch (e) {
406
+ return finalizeExecutionResult(
407
+ createErroredExecutionResult({
408
+ error: e,
409
+ }),
410
+ )
411
+ }
412
+ } else {
413
+ // when the process is still alive
414
+ // we want to catch error to notify runtimeErrorAfterExecutionCallback
415
+ // and throw that error by default
416
+ registerErrorCallback((error) => {
417
+ runtimeErrorAfterExecutionCallback(error)
418
+ })
410
419
  }
411
420
 
412
421
  const executionResult = raceResult.value
@@ -3,6 +3,18 @@ import { createSupportsColor } from "supports-color"
3
3
 
4
4
  const canUseUnicode = isUnicodeSupported()
5
5
  const processSupportsBasicColor = createSupportsColor(process.stdout).hasBasic
6
+ let canUseColors = processSupportsBasicColor
7
+
8
+ // GitHub workflow does support ANSI but "supports-color" returns false
9
+ // because stream.isTTY returns false, see https://github.com/actions/runner/issues/241
10
+ if (process.env.GITHUB_WORKFLOW) {
11
+ // Check on FORCE_COLOR is to ensure it is prio over GitHub workflow check
12
+ if (process.env.FORCE_COLOR !== "false") {
13
+ // in unit test we use process.env.FORCE_COLOR = 'false' to fake
14
+ // that colors are not supported. Let it have priority
15
+ canUseColors = true
16
+ }
17
+ }
6
18
 
7
19
  const ansiStyles = {
8
20
  red: { open: 31, close: 39 },
@@ -22,7 +34,7 @@ export const ANSI_GREY = ansiStyles.grey
22
34
 
23
35
  export const ANSI_RESET = "\x1b[0m"
24
36
 
25
- export const setANSIColor = processSupportsBasicColor
37
+ export const setANSIColor = canUseColors
26
38
  ? (text, ansiColor) =>
27
39
  `\x1b[${ansiColor.open}m${text}\x1b[${ansiColor.close}m`
28
40
  : (text) => text
@@ -77,16 +77,17 @@ export const createControllableNodeProcess = async ({
77
77
  }
78
78
 
79
79
  await assertFilePresence(nodeControllableFileUrl)
80
+ const envForChildProcess = {
81
+ ...(inheritProcessEnv ? process.env : {}),
82
+ ...env,
83
+ }
80
84
  const childProcess = forkChildProcess(
81
85
  urlToFileSystemPath(nodeControllableFileUrl),
82
86
  {
83
87
  execArgv,
84
88
  // silent: true
85
89
  stdio: ["pipe", "pipe", "pipe", "ipc"],
86
- env: {
87
- ...(inheritProcessEnv ? process.env : {}),
88
- ...env,
89
- },
90
+ env: envForChildProcess,
90
91
  },
91
92
  )
92
93
 
@@ -123,14 +124,17 @@ ${JSON.stringify(env, null, " ")}`)
123
124
  const registerConsoleCallback = (callback) => {
124
125
  consoleCallbackArray.push(callback)
125
126
  }
126
- installProcessOutputListener(childProcess, ({ type, text }) => {
127
- consoleCallbackArray.forEach((callback) => {
128
- callback({
129
- type,
130
- text,
127
+ const removeOutputListener = installProcessOutputListener(
128
+ childProcess,
129
+ ({ type, text }) => {
130
+ consoleCallbackArray.forEach((callback) => {
131
+ callback({
132
+ type,
133
+ text,
134
+ })
131
135
  })
132
- })
133
- })
136
+ },
137
+ )
134
138
  // keep listening process outputs while child process is killed to catch
135
139
  // outputs until it's actually disconnected
136
140
  // registerCleanupCallback(removeProcessOutputListener)
@@ -138,38 +142,68 @@ ${JSON.stringify(env, null, " ")}`)
138
142
  const errorCallbackArray = []
139
143
  const registerErrorCallback = (callback) => {
140
144
  errorCallbackArray.push(callback)
145
+ return () => {
146
+ const index = errorCallbackArray.indexOf(callback)
147
+ if (index > -1) {
148
+ errorCallbackArray.splice(index, 1)
149
+ }
150
+ }
141
151
  }
142
152
  let killing = false
143
- installProcessErrorListener(childProcess, (error) => {
144
- if (!childProcess.connected && error.code === "ERR_IPC_DISCONNECTED") {
145
- return
146
- }
147
- // on windows killProcessTree uses taskkill which seems to kill the process
148
- // with an exitCode of 1
149
- if (process.platform === "win32" && killing && error.exitCode === 1) {
150
- return
151
- }
152
- errorCallbackArray.forEach((callback) => {
153
- callback(error)
154
- })
155
- })
153
+ const removeErrorListener = installProcessErrorListener(
154
+ childProcess,
155
+ (error) => {
156
+ removeOutputListener()
157
+ if (!childProcess.connected && error.code === "ERR_IPC_DISCONNECTED") {
158
+ return
159
+ }
160
+ // on windows killProcessTree uses taskkill which seems to kill the process
161
+ // with an exitCode of 1
162
+ if (process.platform === "win32" && killing && error.exitCode === 1) {
163
+ return
164
+ }
165
+ errorCallbackArray.forEach((callback) => {
166
+ callback(error)
167
+ })
168
+ },
169
+ )
156
170
  // keep listening process errors while child process is killed to catch
157
171
  // errors until it's actually disconnected
158
172
  // registerCleanupCallback(removeProcessErrorListener)
159
173
 
160
174
  // https://nodejs.org/api/child_process.html#child_process_event_disconnect
161
175
  let resolveDisconnect
176
+ let hasExitedOrDisconnected = false
162
177
  const disconnected = new Promise((resolve) => {
163
- resolveDisconnect = resolve
164
- onceProcessMessage(childProcess, "disconnect", () => {
165
- resolve()
166
- })
167
- onceProcessEvent(childProcess, "disconnect", () => {
178
+ resolveDisconnect = () => {
179
+ hasExitedOrDisconnected = true
180
+ removeExitListener()
181
+ removeDisconnectListener()
182
+ }
183
+
184
+ const removeDisconnectListener = onceProcessEvent(
185
+ childProcess,
186
+ "disconnect",
187
+ () => {
188
+ hasExitedOrDisconnected = true
189
+ removeErrorListener()
190
+ removeExitListener()
191
+ resolve()
192
+ },
193
+ )
194
+
195
+ const removeExitListener = onceProcessEvent(childProcess, "exit", () => {
196
+ hasExitedOrDisconnected = true
197
+ removeErrorListener()
198
+ removeDisconnectListener()
168
199
  resolve()
169
200
  })
170
201
  })
171
202
 
172
- const disconnectChildProcess = () => {
203
+ const disconnectChildProcess = async () => {
204
+ if (hasExitedOrDisconnected) {
205
+ return
206
+ }
173
207
  try {
174
208
  childProcess.disconnect()
175
209
  } catch (e) {
@@ -179,14 +213,20 @@ ${JSON.stringify(env, null, " ")}`)
179
213
  throw e
180
214
  }
181
215
  }
182
- return disconnected
216
+ await disconnected
183
217
  }
184
218
 
185
219
  const killChildProcess = async ({ signal }) => {
220
+ if (hasExitedOrDisconnected) {
221
+ await disconnectChildProcess()
222
+ return
223
+ }
224
+
186
225
  killing = true
187
226
  logger.debug(`send ${signal} to child process with pid ${childProcess.pid}`)
188
227
 
189
228
  await new Promise((resolve) => {
229
+ // see also https://github.com/sindresorhus/execa/issues/96
190
230
  const killProcessTree = require("tree-kill")
191
231
  killProcessTree(childProcess.pid, signal, (error) => {
192
232
  if (error) {
@@ -214,7 +254,7 @@ ${JSON.stringify(env, null, " ")}`)
214
254
  return
215
255
  }
216
256
 
217
- logger.error(
257
+ logger.warn(
218
258
  createDetailedMessage(
219
259
  `error while killing process tree with ${signal}`,
220
260
  {
@@ -237,17 +277,31 @@ ${JSON.stringify(env, null, " ")}`)
237
277
  // in case the child process did not disconnect by itself at this point
238
278
  // something is keeping it alive and it cannot be propely killed.
239
279
  // wait for the child process to disconnect by itself
240
- return disconnectChildProcess()
280
+ await disconnectChildProcess()
241
281
  }
242
282
 
243
- const stop = ({ gracefulFailed } = {}) => {
244
- return killChildProcess({
245
- signal: gracefulFailed ? GRACEFUL_STOP_FAILED_SIGNAL : STOP_SIGNAL,
246
- })
283
+ const stop = async ({ gracefulFailed } = {}) => {
284
+ let unregisterErrorCallback
285
+ await Promise.race([
286
+ new Promise((resolve, reject) => {
287
+ unregisterErrorCallback = registerErrorCallback(reject)
288
+ }),
289
+ killChildProcess({
290
+ signal: gracefulFailed ? GRACEFUL_STOP_FAILED_SIGNAL : STOP_SIGNAL,
291
+ }),
292
+ ])
293
+ unregisterErrorCallback()
247
294
  }
248
295
 
249
- const gracefulStop = () => {
250
- return killChildProcess({ signal: GRACEFUL_STOP_SIGNAL })
296
+ const gracefulStop = async () => {
297
+ let unregisterErrorCallback
298
+ await Promise.race([
299
+ new Promise((resolve, reject) => {
300
+ unregisterErrorCallback = registerErrorCallback(reject)
301
+ }),
302
+ killChildProcess({ signal: GRACEFUL_STOP_SIGNAL }),
303
+ ])
304
+ unregisterErrorCallback()
251
305
  }
252
306
 
253
307
  const requestActionOnChildProcess = ({ actionType, actionParams }) => {
@@ -338,15 +392,13 @@ const installProcessOutputListener = (childProcess, callback) => {
338
392
 
339
393
  const installProcessErrorListener = (childProcess, callback) => {
340
394
  // https://nodejs.org/api/child_process.html#child_process_event_error
341
- const errorListener = (error) => {
342
- removeExitListener() // if an error occured we ignore the child process exitCode
343
- callback(error)
344
- onceProcessMessage(childProcess, "error", errorListener)
345
- }
346
395
  const removeErrorListener = onceProcessMessage(
347
396
  childProcess,
348
397
  "error",
349
- errorListener,
398
+ (error) => {
399
+ removeExitListener() // if an error occured we ignore the child process exitCode
400
+ callback(error)
401
+ },
350
402
  )
351
403
  // process.exit(1) in child process or process.exitCode = 1 + process.exit()
352
404
  // means there was an error even if we don't know exactly what.
@@ -150,7 +150,7 @@ const removeActionRequestListener = onceProcessMessage(
150
150
  // removeActionRequestListener()
151
151
  if (actionParams.exitAfterAction) {
152
152
  // for some reason this fixes v8 coverage directory somtimes empty
153
- setTimeout(() => process.exit())
153
+ process.exit()
154
154
  }
155
155
  },
156
156
  )