diff --git a/README.md b/README.md index e635528..7e9b017 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ but more thorough, because that package doesn't seem very actively maintained. `promisify-child-process` provides a **drop-in replacement** for the original `child_process` functions, not just duplicate methods that return a `Promise`. So when you call `exec(...)` we still return a -`ChildProcess` instance, just with `.then()` and `.catch()` added to +`ChildProcess` instance, just with `.then()`, `.catch()`, and `.finally()` added to make it promise-friendly. ## Install and Set-up diff --git a/package.json b/package.json index 691d39d..b81eb1b 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,8 @@ "prettier-eslint": "^8.8.2", "rimraf": "^2.6.0", "semantic-release": "^15.13.31", - "typescript": "^3.7.3" + "typescript": "^3.7.3", + "waait": "^1.0.5" }, "dependencies": {}, "renovate": { diff --git a/src/index.js b/src/index.js index 26d3efc..c9b3740 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,21 @@ type PromisifyChildProcessBaseOpts = { export type SpawnOpts = child_process$spawnOpts & PromisifyChildProcessBaseOpts export type ForkOpts = child_process$forkOpts & PromisifyChildProcessBaseOpts +const bindFinally = (promise: Promise) => ( + handler: () => mixed +): Promise => + // don't assume we're running in an environment with Promise.finally + promise.then( + async (value: any): any => { + await handler() + return value + }, + async (reason: any): any => { + await handler() + throw reason + } + ) + function joinChunks( chunks: $ReadOnlyArray, encoding: ?string @@ -139,6 +154,9 @@ export function promisifyChildProcess( return (Object.create(child, { then: { value: _promise.then.bind(_promise) }, catch: { value: _promise.catch.bind(_promise) }, + finally: { + value: bindFinally(_promise), + }, }): any) } @@ -196,6 +214,7 @@ function promisifyExecMethod(method: any): any { return (Object.create((child: any), { then: { value: _promise.then.bind(_promise) }, catch: { value: _promise.catch.bind(_promise) }, + finally: { value: bindFinally(_promise) }, }): any) } } diff --git a/test/index.js b/test/index.js index 581748f..4fd06e4 100644 --- a/test/index.js +++ b/test/index.js @@ -5,6 +5,7 @@ import { describe, it, before, after } from 'mocha' import { expect } from 'chai' import path from 'path' import fs from 'fs-extra' +import delay from 'waait' before(() => Promise.all([ @@ -50,11 +51,16 @@ describe('spawn', function() { this.timeout(30000) it('resolves with process output', async () => { + let finallyDone = false const { stdout, stderr } = await spawn( process.execPath, [require.resolve('./resolvesWithProcessOutput')], { maxBuffer: 200 * 1024 } - ) + ).finally(async () => { + await delay(50) + finallyDone = true + }) + expect(finallyDone, 'finally handler finished').to.be.true if (stdout == null || stderr == null) throw new Error('missing output') if (!(stdout instanceof Buffer)) throw new Error('expected stdout to be a buffer') @@ -94,10 +100,17 @@ describe('spawn', function() { expect(stderr).to.equal('world') }) it('rejects with exit code', async () => { + let finallyDone = false let error await spawn(process.execPath, [require.resolve('./rejectsWithExitCode')], { maxBuffer: 200 * 1024, - }).catch(err => (error = err)) + }) + .finally(async () => { + await delay(50) + finallyDone = true + }) + .catch(err => (error = err)) + expect(finallyDone, 'finally handler finished').to.be.true if (error == null) throw new Error('missing error') const { code, message, stdout, stderr } = error expect(message).to.equal('Process exited with code 2') @@ -157,10 +170,15 @@ describe('fork', function() { this.timeout(30000) it('resolves with process output', async () => { + let finallyDone = false const { stdout, stderr } = await fork( require.resolve('./resolvesWithProcessOutput'), { silent: true, maxBuffer: 200 * 1024 } - ) + ).finally(async () => { + await delay(50) + finallyDone = true + }) + expect(finallyDone, 'finally handler finished').to.be.true if (stdout == null || stderr == null) throw new Error('missing output') if (!(stdout instanceof Buffer)) throw new Error('expected stdout to be a buffer') @@ -198,11 +216,18 @@ describe('fork', function() { expect(stderr).to.equal('world') }) it('rejects with exit code', async () => { + let finallyDone = false let error await fork(require.resolve('./rejectsWithExitCode'), { silent: true, maxBuffer: 200 * 1024, - }).catch(err => (error = err)) + }) + .finally(async () => { + await delay(50) + finallyDone = true + }) + .catch(err => (error = err)) + expect(finallyDone, 'finally handler finished').to.be.true if (error == null) throw new Error('missing error') const { code, message, stdout, stderr } = error expect(message).to.equal('Process exited with code 2') diff --git a/yarn.lock b/yarn.lock index 15e1c44..3c365a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8313,6 +8313,11 @@ vue-eslint-parser@^2.0.2: esquery "^1.0.0" lodash "^4.17.4" +waait@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/waait/-/waait-1.0.5.tgz#6a3c7aaa88bd0a1a545e9d47890b9595bebf9aa7" + integrity sha512-wp+unA4CpqxvBUKHHv8D86fK4jWByHAWyhEXXVHfVUZfK+16ylpj7hjQ58Z8j9ntu8XNukRQT8Fi5qbyJ8rkyw== + wcwidth@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"