Skip to content

Commit bfe4633

Browse files
committed
Fix execaSync() returning undefined output when the subprocess fails to spawn
Fixes #1263
1 parent 93bddc4 commit bfe4633

2 files changed

Lines changed: 27 additions & 6 deletions

File tree

‎lib/io/output-sync.js‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@ import {getFileTarget} from '../stdio/duplicate.js';
88
import {truncateMaxBufferSync} from './max-buffer.js';
99

1010
// Apply `stdout`/`stderr` options, after spawning, in sync mode
11-
export const transformOutputSync = ({fileDescriptors, syncResult: {output}, options, verboseInfo}) => {
12-
if (output === null) {
13-
return {output: Array.from({length: fileDescriptors.length}), error: undefined};
14-
}
15-
11+
export const transformOutputSync = ({fileDescriptors, syncResult, options, verboseInfo}) => {
12+
const output = syncResult.output ?? getSpawnFailureOutput(options);
1613
const outputFiles = new Set();
1714
// Transforms, `verbose` and file writes apply to a single file descriptor each, so their errors must not affect the other ones
1815
const errors = [];
@@ -28,6 +25,10 @@ export const transformOutputSync = ({fileDescriptors, syncResult: {output}, opti
2825
return {output: transformedOutput, error: errors.find(error => error !== undefined)};
2926
};
3027

28+
// When the subprocess fails to spawn, `spawnSync()` returns no output. Like with asynchronous methods, each piped output is empty instead.
29+
// This is the output `spawnSync()` returns when the subprocess succeeds without printing anything: `stdin` and non-piped file descriptors are `null`. Input pipes on other file descriptors cannot be used with synchronous methods.
30+
const getSpawnFailureOutput = ({stdio}) => stdio.map((stdioOption, fdNumber) => fdNumber !== 0 && stdioOption === 'pipe' ? new Uint8Array() : null);
31+
3132
const transformOutputResultSync = (
3233
{result, fileDescriptors, fdNumber, errors, outputFiles, verboseInfo},
3334
{buffer, encoding, lines, stripFinalNewline, maxBuffer},
@@ -79,7 +80,8 @@ const transformOutputResultSync = (
7980
// Applies transform generators to `stdout`/`stderr`
8081
const runOutputGeneratorsSync = (chunks, stdioItems, encoding) => {
8182
try {
82-
return {chunks: runGeneratorsSync(chunks, stdioItems, encoding, false)};
83+
// `error` is set, so that a polluted `Object.prototype.error` is not read when destructuring
84+
return {chunks: runGeneratorsSync(chunks, stdioItems, encoding, false), error: undefined};
8385
} catch (error) {
8486
return {chunks, error};
8587
}

‎test/return/early-error.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';
1616
import {foobarString, foobarUint8Array} from '../helpers/input.js';
1717
import {fullStdio} from '../helpers/stdio.js';
18+
import {getOutputGenerator, convertTransformToFinal} from '../helpers/generator.js';
1819
import {
1920
earlyErrorOptions,
2021
getEarlyErrorSubprocess,
@@ -43,6 +44,24 @@ test('execaSync() throws error if ENOENT', () => {
4344
}, {message: ENOENT_REGEXP});
4445
});
4546

47+
// The subprocess never started, so its output is empty, but it must have the same shape as with the asynchronous methods
48+
const testSpawnFailureOutput = async options => {
49+
const {stdio, all} = await execa('nonexistent-command-for-execa', {...options, reject: false});
50+
const {stdio: stdioSync, all: allSync} = execaSync('nonexistent-command-for-execa', {...options, reject: false});
51+
assert.deepEqual(stdioSync, stdio);
52+
assert.deepEqual(allSync, all);
53+
};
54+
55+
test('execaSync() output is empty when the command does not exist', () => testSpawnFailureOutput({}));
56+
test('execaSync() output is empty when the command does not exist, with encoding: buffer', () => testSpawnFailureOutput({encoding: 'buffer'}));
57+
test('execaSync() output is empty when the command does not exist, with lines: true', () => testSpawnFailureOutput({lines: true}));
58+
test('execaSync() output is empty when the command does not exist, with all: true', () => testSpawnFailureOutput({all: true}));
59+
test('execaSync() output is empty when the command does not exist, with an additional file descriptor', () => testSpawnFailureOutput(fullStdio));
60+
test('execaSync() output is undefined when the command does not exist, with stdout: ignore', () => testSpawnFailureOutput({stdout: 'ignore'}));
61+
test('execaSync() output is undefined when the command does not exist, with buffer: false', () => testSpawnFailureOutput({buffer: false}));
62+
test('execaSync() output is empty when the cwd does not exist', () => testSpawnFailureOutput({cwd: '/nonexistent-directory-for-execa'}));
63+
test('execaSync() output runs the final of transforms when the command does not exist', () => testSpawnFailureOutput({stdout: convertTransformToFinal(getOutputGenerator(foobarString)(), true)}));
64+
4665
const testEarlyErrorShape = async reject => {
4766
const subprocess = getEarlyErrorSubprocess({reject});
4867
assert.doesNotThrow(() => {

0 commit comments

Comments
 (0)