Repository: sindresorhus/execa
Pull request #248 (2019-05-14 15:41:38)

Make `error.killed` more consistent
`error.killed` is always `true` or `false` with `execa()`.

But with `execa.sync()`, it can also be `undefined` sometimes, e.g. on `ENOENT`. This PR ensures `error.killed` is always a boolean. 

Details:
  - `spawn()` returns a `childProcess` with [`childProcess.killed`](https://github.com/nodejs/node/blob/master/lib/internal/child_process.js#L446)
  - `spawnSync()` does not return this, but we can emulate most of the cases by checking the `signal` property.

Tests are added for this as well.

sindresorhus: Unrelated to this PR, but what do you think about renaming `.killed` to `.isKilled` for naming consistency?
sindresorhus: > spawnSync() does not return this, but we can emulate most of the cases by checking the signal property.

It's weird that the synchronous version has this inconsistency. Should we open a Node.js issue about it?

Patch:
diff --git a/index.js b/index.js
index 303537d..1ca516a 100644
--- a/index.js
+++ b/index.js
@@ -432,7 +432,8 @@ module.exports.sync = (command, args, options) => {
 			joinedCommand,
 			parsed,
 			timedOut: false,
-			isCanceled: false
+			isCanceled: false,
+			killed: result.signal !== null
 		});
 
 		if (!parsed.options.reject) {
diff --git a/readme.md b/readme.md
index d735f01..1b3ca69 100644
--- a/readme.md
+++ b/readme.md
@@ -113,7 +113,8 @@ try {
 		stderr: null,
 		failed: true,
 		timedOut: false,
-		isCanceled: false
+		isCanceled: false,
+		killed: false
 	}
 	*/
 }


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index a6c95f6..72fcc8c 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -15,8 +15,10 @@ try {
 	const unicornsResult = await execaPromise;
 	expectType<string>(unicornsResult.command);
 	expectType<string | number>(unicornsResult.code);
-	expectType<string>(unicornsResult.stderr);
+	expectType<number>(unicornsResult.exitCode);
+	expectType<string>(unicornsResult.exitCodeName);
 	expectType<string>(unicornsResult.stdout);
+	expectType<string>(unicornsResult.stderr);
 	expectType<string>(unicornsResult.all);
 	expectType<boolean>(unicornsResult.failed);
 	expectType<boolean>(unicornsResult.timedOut);
@@ -28,16 +30,26 @@ try {
 
 	expectType<string>(execaError.message);
 	expectType<number | string>(execaError.code);
+	expectType<number>(execaError.exitCode);
+	expectType<string>(execaError.exitCodeName);
+	expectType<string>(execaError.stdout);
+	expectType<string>(execaError.stderr);
 	expectType<string>(execaError.all);
+	expectType<boolean>(execaError.failed);
+	expectType<boolean>(execaError.timedOut);
 	expectType<boolean>(execaError.isCanceled);
+	expectType<boolean>(execaError.killed);
+	expectType<string | undefined>(execaError.signal);
 }
 
 try {
 	const unicornsResult = execa.sync('unicorns');
 	expectType<string>(unicornsResult.command);
 	expectType<string | number>(unicornsResult.code);
-	expectType<string>(unicornsResult.stderr);
+	expectType<number>(unicornsResult.exitCode);
+	expectType<string>(unicornsResult.exitCodeName);
 	expectType<string>(unicornsResult.stdout);
+	expectType<string>(unicornsResult.stderr);
 	expectError(unicornsResult.all);
 	expectType<boolean>(unicornsResult.failed);
 	expectType<boolean>(unicornsResult.timedOut);
@@ -49,8 +61,16 @@ try {
 
 	expectType<string>(execaError.message);
 	expectType<number | string>(execaError.code);
+	expectType<number>(execaError.exitCode);
+	expectType<string>(execaError.exitCodeName);
+	expectType<string>(execaError.stdout);
+	expectType<string>(execaError.stderr);
 	expectError(execaError.all);
+	expectType<boolean>(execaError.failed);
+	expectType<boolean>(execaError.timedOut);
 	expectError(execaError.isCanceled);
+	expectType<boolean>(execaError.killed);
+	expectType<string | undefined>(execaError.signal);
 }
 
 execa('unicorns', {cwd: '.'});
@@ -85,13 +105,6 @@ execa('unicorns', {stdin: 'inherit'});
 execa('unicorns', {stdin: process.stdin});
 execa('unicorns', {stdin: 1});
 execa('unicorns', {stdin: undefined});
-execa('unicorns', {stderr: 'pipe'});
-execa('unicorns', {stderr: 'ipc'});
-execa('unicorns', {stderr: 'ignore'});
-execa('unicorns', {stderr: 'inherit'});
-execa('unicorns', {stderr: process.stderr});
-execa('unicorns', {stderr: 1});
-execa('unicorns', {stderr: undefined});
 execa('unicorns', {stdout: 'pipe'});
 execa('unicorns', {stdout: 'ipc'});
 execa('unicorns', {stdout: 'ignore'});
@@ -99,7 +112,17 @@ execa('unicorns', {stdout: 'inherit'});
 execa('unicorns', {stdout: process.stdout});
 execa('unicorns', {stdout: 1});
 execa('unicorns', {stdout: undefined});
+execa('unicorns', {stderr: 'pipe'});
+execa('unicorns', {stderr: 'ipc'});
+execa('unicorns', {stderr: 'ignore'});
+execa('unicorns', {stderr: 'inherit'});
+execa('unicorns', {stderr: process.stderr});
+execa('unicorns', {stderr: 1});
+execa('unicorns', {stderr: undefined});
 execa('unicorns', {windowsVerbatimArguments: true});
+execa('unicorns', {input: ''});
+execa('unicorns', {input: Buffer.from('')});
+execa('unicorns', {input: process.stdin});
 
 expectType<ExecaChildProcess<string>>(execa('unicorns'));
 expectType<ExecaReturnValue<string>>(await execa('unicorns'));
diff --git a/test.js b/test.js
index 9579764..bcc850a 100644
--- a/test.js
+++ b/test.js
@@ -141,7 +141,9 @@ test('stripFinalNewline in sync mode', t => {
 });
 
 test('stripFinalNewline in sync mode on failure', t => {
-	const {stderr} = t.throws(() => execa.sync('noop-throw', ['foo'], {stripFinalNewline: true}));
+	const {stderr} = t.throws(() => {
+		execa.sync('noop-throw', ['foo'], {stripFinalNewline: true});
+	});
 	t.is(stderr, 'foo');
 });
 
@@ -318,6 +320,18 @@ test('result.killed is false if not killed, in sync mode', t => {
 	t.false(result.killed);
 });
 
+test('result.killed is false on process error', async t => {
+	const {killed} = await t.throwsAsync(execa('wrong command'));
+	t.false(killed);
+});
+
+test('result.killed is false on process error, in sync mode', t => {
+	const {killed} = t.throws(() => {
+		execa.sync('wrong command');
+	});
+	t.false(killed);
+});
+
 if (process.platform === 'darwin') {
 	test.cb('sanity check: child_process.exec also has killed.false if killed indirectly', t => {
 		const cp = childProcess.exec('forever', error => {
@@ -613,7 +627,9 @@ test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mod
 });
 
 test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (failure)', t => {
-	const error = t.throws(() => execa.sync('fail'));
+	const error = t.throws(() => {
+		execa.sync('fail');
+	});
 	t.false(error.isCanceled);
 });