Skip to content

Commit

Permalink
Fix shape of exceptions rejected by execa() (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored and sindresorhus committed Jun 15, 2019
1 parent bad2140 commit 49c74ac
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 16 deletions.
50 changes: 34 additions & 16 deletions index.js
Expand Up @@ -228,6 +228,30 @@ function joinCommand(file, args = []) {
return [file, ...args].join(' ');
}

function mergePromiseProperty(spawned, getPromise, property) {
Object.defineProperty(spawned, property, {
value(...args) {
return getPromise()[property](...args);
},
writable: true,
enumerable: false,
configurable: true
});
}

// The return value is a mixin of `childProcess` and `Promise`
function mergePromise(spawned, getPromise) {
mergePromiseProperty(spawned, getPromise, 'then');
mergePromiseProperty(spawned, getPromise, 'catch');

// TODO: Remove the `if`-guard when targeting Node.js 10
if (Promise.prototype.finally) {
mergePromiseProperty(spawned, getPromise, 'finally');
}

return spawned;
}

function spawnedKill(kill, signal = 'SIGTERM', options = {}) {
const killResult = kill(signal);
setKillTimeout(kill, signal, options, killResult);
Expand Down Expand Up @@ -275,13 +299,15 @@ 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: ''}, {
command,
parsed,
timedOut: false,
isCanceled: false,
killed: false
}));
return mergePromise(new childProcess.ChildProcess(), () =>
Promise.reject(makeError({error, stdout: '', stderr: '', all: ''}, {
command,
parsed,
timedOut: false,
isCanceled: false,
killed: false
}))
);
}

const kill = spawned.kill.bind(spawned);
Expand Down Expand Up @@ -403,21 +429,13 @@ const execa = (file, args, options) => {

spawned.all = makeAllStream(spawned);

// eslint-disable-next-line promise/prefer-await-to-then
spawned.then = (onFulfilled, onRejected) => handlePromise().then(onFulfilled, onRejected);
spawned.catch = onRejected => handlePromise().catch(onRejected);
spawned.cancel = () => {
if (spawned.kill()) {
isCanceled = true;
}
};

// TODO: Remove the `if`-guard when targeting Node.js 10
if (Promise.prototype.finally) {
spawned.finally = onFinally => handlePromise().finally(onFinally);
}

return spawned;
return mergePromise(spawned, handlePromise);
};

module.exports = execa;
Expand Down
20 changes: 20 additions & 0 deletions test.js
Expand Up @@ -301,6 +301,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 {failed} = await t.throwsAsync(execa('noop', {uid: -1}));
t.true(failed);
Expand Down Expand Up @@ -664,6 +673,17 @@ test('removes exit handler on exit', async t => {
t.false(included);
});

test('promise methods are not enumerable', t => {
const descriptors = Object.getOwnPropertyDescriptors(execa('noop'));
// eslint-disable-next-line promise/prefer-await-to-then
t.false(descriptors.then.enumerable);
t.false(descriptors.catch.enumerable);
// TOOD: Remove the `if`-guard when targeting Node.js 10
if (Promise.prototype.finally) {
t.false(descriptors.finally.enumerable);
}
});

// TOOD: Remove the `if`-guard when targeting Node.js 10
if (Promise.prototype.finally) {
test('finally function is executed on success', async t => {
Expand Down

0 comments on commit 49c74ac

Please sign in to comment.