diff --git a/fixtures/async-exit-code.js b/fixtures/async-exit-code.js new file mode 100644 index 0000000..df82001 --- /dev/null +++ b/fixtures/async-exit-code.js @@ -0,0 +1,39 @@ +import process from 'node:process'; +import exitHook, {asyncExitHook, gracefulExit} from '../index.js'; + +exitHook(() => { + console.log('foo'); +}); + +exitHook(() => { + console.log('bar'); +}); + +const unsubscribe = exitHook(() => { + console.log('baz'); +}); + +unsubscribe(); + +asyncExitHook( + async () => { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 100); + }); + + console.log('quux'); + }, + { + wait: 200, + }, +); + +process.exitCode = 1; + +if (process.env.EXIT_HOOK_SYNC === '1') { + process.exit(); // eslint-disable-line unicorn/no-process-exit +} + +gracefulExit(); diff --git a/fixtures/empty-exit-code.js b/fixtures/empty-exit-code.js new file mode 100644 index 0000000..bdfbede --- /dev/null +++ b/fixtures/empty-exit-code.js @@ -0,0 +1,7 @@ +import process from 'node:process'; +import exitHook from '../index.js'; + +process.exitCode = 1; +exitHook(() => { + // https://github.com/sindresorhus/exit-hook/issues/23 +}); diff --git a/fixtures/signal-exit-code.js b/fixtures/signal-exit-code.js new file mode 100644 index 0000000..8f79042 --- /dev/null +++ b/fixtures/signal-exit-code.js @@ -0,0 +1,16 @@ +import process from 'node:process'; +import exitHook, {asyncExitHook} from '../index.js'; + +process.exitCode = 1; + +exitHook(signal => { + console.log(signal); +}); + +asyncExitHook(async signal => { + console.log(signal); +}, { + wait: 200, +}); + +setInterval(() => {}, 1e9); diff --git a/fixtures/sync-exit-code.js b/fixtures/sync-exit-code.js new file mode 100644 index 0000000..b1357d4 --- /dev/null +++ b/fixtures/sync-exit-code.js @@ -0,0 +1,20 @@ +import process from 'node:process'; +import exitHook from '../index.js'; + +exitHook(() => { + console.log('foo'); +}); + +exitHook(() => { + console.log('bar'); +}); + +const unsubscribe = exitHook(() => { + console.log('baz'); +}); + +unsubscribe(); + +process.exitCode = 1; + +process.exit(); // eslint-disable-line unicorn/no-process-exit diff --git a/index.js b/index.js index 9d24483..dcc471a 100644 --- a/index.js +++ b/index.js @@ -23,7 +23,15 @@ async function exit(shouldManuallyExit, isSynchronous, signal) { ].join(' ')); } - const exitCode = 128 + signal; + let exitCode = 0; + + // A non-graceful signal should be preserved over process.exitCode + if (signal > 0) { + exitCode = 128 + signal; + // Respect process.exitCode for graceful exits + } else if (typeof process.exitCode === 'number' || typeof process.exitCode === 'string') { + exitCode = process.exitCode; + } const done = (force = false) => { if (force === true || shouldManuallyExit === true) { diff --git a/test.js b/test.js index 037030b..dca31e5 100644 --- a/test.js +++ b/test.js @@ -10,12 +10,34 @@ test('main', async t => { t.is(exitCode, 0); }); +test('main with exitCode', async t => { + try { + await execa(process.execPath, ['./fixtures/sync-exit-code.js']); + t.fail(); + } catch ({stdout, stderr, exitCode}) { + t.is(stdout, 'foo\nbar'); + t.is(stderr, ''); + t.is(exitCode, 1); + } +}); + test('main-empty', async t => { const {stderr, exitCode} = await execa(process.execPath, ['./fixtures/empty.js']); t.is(stderr, ''); t.is(exitCode, 0); }); +test('main-empty with exitCode', async t => { + try { + await execa(process.execPath, ['./fixtures/empty-exit-code.js']); + t.fail(); + } catch ({stdout, stderr, exitCode}) { + t.is(stdout, ''); + t.is(stderr, ''); + t.is(exitCode, 1); + } +}); + test('main-async', async t => { const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/async.js']); t.is(stdout, 'foo\nbar\nquux'); @@ -23,6 +45,17 @@ test('main-async', async t => { t.is(exitCode, 0); }); +test('main-async with exitCode', async t => { + try { + await execa(process.execPath, ['./fixtures/async-exit-code.js']); + t.fail(); + } catch ({stdout, stderr, exitCode}) { + t.is(stdout, 'foo\nbar\nquux'); + t.is(stderr, ''); + t.is(exitCode, 1); + } +}); + test('main-async-notice', async t => { const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/async.js'], { env: { @@ -34,25 +67,42 @@ test('main-async-notice', async t => { t.is(exitCode, 0); }); +test('main-async-notice with exitCode', async t => { + try { + await execa(process.execPath, ['./fixtures/async-exit-code.js'], { + env: { + EXIT_HOOK_SYNC: '1', + }, + }); + t.fail(); + } catch ({stdout, stderr, exitCode}) { + t.is(stdout, 'foo\nbar'); + t.regex(stderr, /SYNCHRONOUS TERMINATION NOTICE/); + t.is(exitCode, 1); + } +}); + test('listener count', t => { - t.is(process.listenerCount('exit'), 0); + // This function is used as on node20+ flushSync is added internally to the exit handler of nodejs + const exitListenerCount = () => process.listeners('exit').filter(fn => fn.name !== 'flushSync').length; + t.is(exitListenerCount(), 0); const unsubscribe1 = exitHook(() => {}); const unsubscribe2 = exitHook(() => {}); - t.is(process.listenerCount('exit'), 1); + t.is(exitListenerCount(), 1); // Remove all listeners unsubscribe1(); unsubscribe2(); - t.is(process.listenerCount('exit'), 1); + t.is(exitListenerCount(), 1); // Re-add listener const unsubscribe3 = exitHook(() => {}); - t.is(process.listenerCount('exit'), 1); + t.is(exitListenerCount(), 1); // Remove again unsubscribe3(); - t.is(process.listenerCount('exit'), 1); + t.is(exitListenerCount(), 1); // Add async style listener const unsubscribe4 = asyncExitHook( @@ -61,11 +111,11 @@ test('listener count', t => { wait: 100, }, ); - t.is(process.listenerCount('exit'), 1); + t.is(exitListenerCount(), 1); // Remove again unsubscribe4(); - t.is(process.listenerCount('exit'), 1); + t.is(exitListenerCount(), 1); }); test('type enforcing', t => { @@ -115,4 +165,20 @@ for (const [signal, exitCode] of signalTests) { t.is(error.stdout, `${exitCode}\n${exitCode}`); } }); + + test(`${signal} causes process.exitCode to be ignored`, async t => { + const subprocess = execa(process.execPath, ['./fixtures/signal-exit-code.js']); + + setTimeout(() => { + subprocess.kill(signal); + }, 1000); + + try { + await subprocess; + } catch (error) { + t.is(error.exitCode, exitCode); + t.is(error.stderr, ''); + t.is(error.stdout, `${exitCode}\n${exitCode}`); + } + }); }