Skip to content

Commit 862f49c

Browse files
Fix it to respect process.exitCode (#41)
1 parent 783baaf commit 862f49c

File tree

6 files changed

+164
-8
lines changed

6 files changed

+164
-8
lines changed

fixtures/async-exit-code.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import process from 'node:process';
2+
import exitHook, {asyncExitHook, gracefulExit} from '../index.js';
3+
4+
exitHook(() => {
5+
console.log('foo');
6+
});
7+
8+
exitHook(() => {
9+
console.log('bar');
10+
});
11+
12+
const unsubscribe = exitHook(() => {
13+
console.log('baz');
14+
});
15+
16+
unsubscribe();
17+
18+
asyncExitHook(
19+
async () => {
20+
await new Promise(resolve => {
21+
setTimeout(() => {
22+
resolve();
23+
}, 100);
24+
});
25+
26+
console.log('quux');
27+
},
28+
{
29+
wait: 200,
30+
},
31+
);
32+
33+
process.exitCode = 1;
34+
35+
if (process.env.EXIT_HOOK_SYNC === '1') {
36+
process.exit(); // eslint-disable-line unicorn/no-process-exit
37+
}
38+
39+
gracefulExit();

fixtures/empty-exit-code.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import process from 'node:process';
2+
import exitHook from '../index.js';
3+
4+
process.exitCode = 1;
5+
exitHook(() => {
6+
// https://github.com/sindresorhus/exit-hook/issues/23
7+
});

fixtures/signal-exit-code.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import process from 'node:process';
2+
import exitHook, {asyncExitHook} from '../index.js';
3+
4+
process.exitCode = 1;
5+
6+
exitHook(signal => {
7+
console.log(signal);
8+
});
9+
10+
asyncExitHook(async signal => {
11+
console.log(signal);
12+
}, {
13+
wait: 200,
14+
});
15+
16+
setInterval(() => {}, 1e9);

fixtures/sync-exit-code.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import process from 'node:process';
2+
import exitHook from '../index.js';
3+
4+
exitHook(() => {
5+
console.log('foo');
6+
});
7+
8+
exitHook(() => {
9+
console.log('bar');
10+
});
11+
12+
const unsubscribe = exitHook(() => {
13+
console.log('baz');
14+
});
15+
16+
unsubscribe();
17+
18+
process.exitCode = 1;
19+
20+
process.exit(); // eslint-disable-line unicorn/no-process-exit

index.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ async function exit(shouldManuallyExit, isSynchronous, signal) {
2323
].join(' '));
2424
}
2525

26-
const exitCode = 128 + signal;
26+
let exitCode = 0;
27+
28+
// A non-graceful signal should be preserved over process.exitCode
29+
if (signal > 0) {
30+
exitCode = 128 + signal;
31+
// Respect process.exitCode for graceful exits
32+
} else if (typeof process.exitCode === 'number' || typeof process.exitCode === 'string') {
33+
exitCode = process.exitCode;
34+
}
2735

2836
const done = (force = false) => {
2937
if (force === true || shouldManuallyExit === true) {

test.js

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,52 @@ test('main', async t => {
1010
t.is(exitCode, 0);
1111
});
1212

13+
test('main with exitCode', async t => {
14+
try {
15+
await execa(process.execPath, ['./fixtures/sync-exit-code.js']);
16+
t.fail();
17+
} catch ({stdout, stderr, exitCode}) {
18+
t.is(stdout, 'foo\nbar');
19+
t.is(stderr, '');
20+
t.is(exitCode, 1);
21+
}
22+
});
23+
1324
test('main-empty', async t => {
1425
const {stderr, exitCode} = await execa(process.execPath, ['./fixtures/empty.js']);
1526
t.is(stderr, '');
1627
t.is(exitCode, 0);
1728
});
1829

30+
test('main-empty with exitCode', async t => {
31+
try {
32+
await execa(process.execPath, ['./fixtures/empty-exit-code.js']);
33+
t.fail();
34+
} catch ({stdout, stderr, exitCode}) {
35+
t.is(stdout, '');
36+
t.is(stderr, '');
37+
t.is(exitCode, 1);
38+
}
39+
});
40+
1941
test('main-async', async t => {
2042
const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/async.js']);
2143
t.is(stdout, 'foo\nbar\nquux');
2244
t.is(stderr, '');
2345
t.is(exitCode, 0);
2446
});
2547

48+
test('main-async with exitCode', async t => {
49+
try {
50+
await execa(process.execPath, ['./fixtures/async-exit-code.js']);
51+
t.fail();
52+
} catch ({stdout, stderr, exitCode}) {
53+
t.is(stdout, 'foo\nbar\nquux');
54+
t.is(stderr, '');
55+
t.is(exitCode, 1);
56+
}
57+
});
58+
2659
test('main-async-notice', async t => {
2760
const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/async.js'], {
2861
env: {
@@ -34,25 +67,42 @@ test('main-async-notice', async t => {
3467
t.is(exitCode, 0);
3568
});
3669

70+
test('main-async-notice with exitCode', async t => {
71+
try {
72+
await execa(process.execPath, ['./fixtures/async-exit-code.js'], {
73+
env: {
74+
EXIT_HOOK_SYNC: '1',
75+
},
76+
});
77+
t.fail();
78+
} catch ({stdout, stderr, exitCode}) {
79+
t.is(stdout, 'foo\nbar');
80+
t.regex(stderr, /SYNCHRONOUS TERMINATION NOTICE/);
81+
t.is(exitCode, 1);
82+
}
83+
});
84+
3785
test('listener count', t => {
38-
t.is(process.listenerCount('exit'), 0);
86+
// This function is used as on node20+ flushSync is added internally to the exit handler of nodejs
87+
const exitListenerCount = () => process.listeners('exit').filter(fn => fn.name !== 'flushSync').length;
88+
t.is(exitListenerCount(), 0);
3989

4090
const unsubscribe1 = exitHook(() => {});
4191
const unsubscribe2 = exitHook(() => {});
42-
t.is(process.listenerCount('exit'), 1);
92+
t.is(exitListenerCount(), 1);
4393

4494
// Remove all listeners
4595
unsubscribe1();
4696
unsubscribe2();
47-
t.is(process.listenerCount('exit'), 1);
97+
t.is(exitListenerCount(), 1);
4898

4999
// Re-add listener
50100
const unsubscribe3 = exitHook(() => {});
51-
t.is(process.listenerCount('exit'), 1);
101+
t.is(exitListenerCount(), 1);
52102

53103
// Remove again
54104
unsubscribe3();
55-
t.is(process.listenerCount('exit'), 1);
105+
t.is(exitListenerCount(), 1);
56106

57107
// Add async style listener
58108
const unsubscribe4 = asyncExitHook(
@@ -61,11 +111,11 @@ test('listener count', t => {
61111
wait: 100,
62112
},
63113
);
64-
t.is(process.listenerCount('exit'), 1);
114+
t.is(exitListenerCount(), 1);
65115

66116
// Remove again
67117
unsubscribe4();
68-
t.is(process.listenerCount('exit'), 1);
118+
t.is(exitListenerCount(), 1);
69119
});
70120

71121
test('type enforcing', t => {
@@ -115,4 +165,20 @@ for (const [signal, exitCode] of signalTests) {
115165
t.is(error.stdout, `${exitCode}\n${exitCode}`);
116166
}
117167
});
168+
169+
test(`${signal} causes process.exitCode to be ignored`, async t => {
170+
const subprocess = execa(process.execPath, ['./fixtures/signal-exit-code.js']);
171+
172+
setTimeout(() => {
173+
subprocess.kill(signal);
174+
}, 1000);
175+
176+
try {
177+
await subprocess;
178+
} catch (error) {
179+
t.is(error.exitCode, exitCode);
180+
t.is(error.stderr, '');
181+
t.is(error.stdout, `${exitCode}\n${exitCode}`);
182+
}
183+
});
118184
}

0 commit comments

Comments
 (0)