diff --git a/index.js b/index.js index 34bf68b89f..97690ba851 100644 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ const mergeStream = require('merge-stream'); const pFinally = require('p-finally'); const onExit = require('signal-exit'); const stdio = require('./lib/stdio'); +const mergePrototypes = require('./lib/merge'); const TEN_MEGABYTES = 1000 * 1000 * 10; @@ -247,13 +248,16 @@ const execa = (file, args, options) => { try { spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); } catch (error) { - return Promise.reject(makeError({error, stdout: '', stderr: '', all: ''}, { + const promise = Promise.reject(makeError({error, stdout: '', stderr: '', all: ''}, { joinedCommand, parsed, timedOut: false, isCanceled: false, killed: false })); + // We make sure `child_process` properties are present even though no child process was created. + // This is to ensure the return value always has the same shape. + return mergePrototypes(promise, new childProcess.ChildProcess()); } const kill = spawned.kill.bind(spawned); diff --git a/lib/merge.js b/lib/merge.js new file mode 100644 index 0000000000..132d12500c --- /dev/null +++ b/lib/merge.js @@ -0,0 +1,24 @@ +'use strict'; + +// Merge two objects, including their prototypes +function mergePrototypes(to, from) { + const prototypes = [...getPrototypes(to), ...getPrototypes(from)]; + const newPrototype = prototypes.reduce(reducePrototype, {}); + return Object.assign(Object.setPrototypeOf(to, newPrototype), to, from); +} + +function getPrototypes(object, prototypes = []) { + const prototype = Object.getPrototypeOf(object); + if (prototype !== null) { + return getPrototypes(prototype, [...prototypes, prototype]); + } + + return prototypes; +} + +function reducePrototype(prototype, constructor) { + return Object.defineProperties(prototype, Object.getOwnPropertyDescriptors(constructor)); +} + +module.exports = mergePrototypes; + diff --git a/test.js b/test.js index 5249888a11..88319904a6 100644 --- a/test.js +++ b/test.js @@ -288,6 +288,15 @@ test('execa() returns a promise with kill() and pid', t => { t.is(typeof pid, 'number'); }); +test('child_process.spawn() propagated errors have correct shape', t => { + const cp = execa('noop', {uid: -1}); + t.notThrows(() => { + cp.catch(() => {}); + cp.unref(); + cp.on('error', () => {}); + }); +}); + test('child_process.spawn() errors are propagated', async t => { const {exitCodeName} = await t.throwsAsync(execa('noop', {uid: -1})); t.is(exitCodeName, process.platform === 'win32' ? 'ENOTSUP' : 'EINVAL');