diff --git a/lib/internal/modules/esm/handle_process_exit.js b/lib/internal/modules/esm/handle_process_exit.js new file mode 100644 index 00000000000000..db830900bd3154 --- /dev/null +++ b/lib/internal/modules/esm/handle_process_exit.js @@ -0,0 +1,12 @@ +'use strict'; + +// Handle a Promise from running code that potentially does Top-Level Await. +// In that case, it makes sense to set the exit code to a specific non-zero +// value if the main code never finishes running. +function handleProcessExit() { + process.exitCode ??= 13; +} + +module.exports = { + handleProcessExit, +}; diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 9a0263024144fb..924c4836bb2aa2 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -8,6 +8,9 @@ const CJSLoader = require('internal/modules/cjs/loader'); const { Module, toRealPath, readPackageScope } = CJSLoader; const { getOptionValue } = require('internal/options'); const path = require('path'); +const { + handleProcessExit, +} = require('internal/modules/esm/handle_process_exit'); function resolveMainPath(main) { // Note extension resolution for the main entry point can be deprecated in a @@ -53,18 +56,11 @@ function runMainESM(mainPath) { } async function handleMainPromise(promise) { - // Handle a Promise from running code that potentially does Top-Level Await. - // In that case, it makes sense to set the exit code to a specific non-zero - // value if the main code never finishes running. - function handler() { - if (process.exitCode === undefined) - process.exitCode = 13; - } - process.on('exit', handler); + process.on('exit', handleProcessExit); try { return await promise; } finally { - process.off('exit', handler); + process.off('exit', handleProcessExit); } } diff --git a/lib/internal/process/per_thread.js b/lib/internal/process/per_thread.js index a63217d9b3955a..709bcb7b137639 100644 --- a/lib/internal/process/per_thread.js +++ b/lib/internal/process/per_thread.js @@ -48,6 +48,10 @@ const { } = require('internal/validators'); const constants = internalBinding('constants').os.signals; +const { + handleProcessExit, +} = require('internal/modules/esm/handle_process_exit'); + const kInternal = Symbol('internal properties'); function assert(x, msg) { @@ -175,6 +179,8 @@ function wrapProcessMethods(binding) { memoryUsage.rss = rss; function exit(code) { + process.off('exit', handleProcessExit); + if (code || code === 0) process.exitCode = code; diff --git a/test/es-module/test-esm-tla-unfinished.mjs b/test/es-module/test-esm-tla-unfinished.mjs index 7d35c86285ac81..d7658c19e98e1c 100644 --- a/test/es-module/test-esm-tla-unfinished.mjs +++ b/test/es-module/test-esm-tla-unfinished.mjs @@ -80,3 +80,21 @@ import fixtures from '../common/fixtures.js'; assert.deepStrictEqual([status, stdout], [1, '']); assert.match(stderr, /Error: Xyz/); } + +{ + // Calling process.exit() in .mjs should return status 0 + const { status, stdout, stderr } = child_process.spawnSync( + process.execPath, + [fixtures.path('es-modules/tla/process-exit.mjs')], + { encoding: 'utf8' }); + assert.deepStrictEqual([status, stdout, stderr], [0, '', '']); +} + +{ + // Calling process.exit() in worker thread shouldn't influence main thread + const { status, stdout, stderr } = child_process.spawnSync( + process.execPath, + [fixtures.path('es-modules/tla/unresolved-with-worker-process-exit.mjs')], + { encoding: 'utf8' }); + assert.deepStrictEqual([status, stdout, stderr], [13, '', '']); +} diff --git a/test/fixtures/es-modules/tla/process-exit.mjs b/test/fixtures/es-modules/tla/process-exit.mjs new file mode 100644 index 00000000000000..78e86b018e7fbb --- /dev/null +++ b/test/fixtures/es-modules/tla/process-exit.mjs @@ -0,0 +1 @@ +process.exit(); diff --git a/test/fixtures/es-modules/tla/unresolved-with-worker-process-exit.mjs b/test/fixtures/es-modules/tla/unresolved-with-worker-process-exit.mjs new file mode 100644 index 00000000000000..8d65baa2b59cce --- /dev/null +++ b/test/fixtures/es-modules/tla/unresolved-with-worker-process-exit.mjs @@ -0,0 +1,8 @@ +import { Worker, isMainThread } from 'worker_threads'; + +if (isMainThread) { + new Worker(new URL(import.meta.url)); + await new Promise(() => {}); +} else { + process.exit(); +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 9a687eede28ca9..cd1a503837191e 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -79,6 +79,7 @@ const expectedModules = new Set([ 'NativeModule internal/modules/esm/resolve', 'NativeModule internal/modules/esm/initialize_import_meta', 'NativeModule internal/modules/esm/translators', + 'NativeModule internal/modules/esm/handle_process_exit', 'NativeModule internal/process/esm_loader', 'NativeModule internal/options', 'NativeModule internal/perf/event_loop_delay',