diff --git a/README.md b/README.md index 2b42149..92d7b1a 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The options object can have the following properties: - `persist` - if `true`, the process will continue after the host exits - `stdin` - another `Result` can be used as the input to this process - `nodeOptions` - any valid options to node's underlying `spawn` function +- `throwOnError` - if true, non-zero exit codes will throw an error ### Piping to another process diff --git a/src/main.ts b/src/main.ts index 5863591..685db51 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,9 @@ import {computeEnv} from './env.js'; import {combineStreams} from './stream.js'; import readline from 'node:readline'; import {_parse} from 'cross-spawn'; +import {NonZeroExitError} from './non-zero-exit-error.js'; + +export {NonZeroExitError}; export interface Output { stderr: string; @@ -40,6 +43,7 @@ export interface Options { timeout: number; persist: boolean; stdin: ExecProcess; + throwOnError: boolean; } export interface TinyExec { @@ -188,6 +192,14 @@ export class ExecProcess implements Result { if (this._thrownError) { throw this._thrownError; } + + if ( + this._options?.throwOnError && + this.exitCode !== 0 && + this.exitCode !== undefined + ) { + throw new NonZeroExitError(this); + } } protected async _waitForOutput(): Promise { @@ -229,6 +241,14 @@ export class ExecProcess implements Result { stdout }; + if ( + this._options.throwOnError && + this.exitCode !== 0 && + this.exitCode !== undefined + ) { + throw new NonZeroExitError(this, result); + } + return result; } diff --git a/src/non-zero-exit-error.ts b/src/non-zero-exit-error.ts new file mode 100644 index 0000000..a338aee --- /dev/null +++ b/src/non-zero-exit-error.ts @@ -0,0 +1,20 @@ +import type {Result, Output} from './main.js'; + +export class NonZeroExitError extends Error { + public readonly result: Result; + public readonly output?: Output; + + public get exitCode(): number | undefined { + if (this.result.exitCode !== null) { + return this.result.exitCode; + } + return undefined; + } + + public constructor(result: Result, output?: Output) { + super(`Process exited with non-zero status (${result.exitCode})`); + + this.result = result; + this.output = output; + } +} diff --git a/src/test/main_test.ts b/src/test/main_test.ts index 06ea12f..9f4fc96 100644 --- a/src/test/main_test.ts +++ b/src/test/main_test.ts @@ -1,4 +1,4 @@ -import {x} from '../main.js'; +import {x, NonZeroExitError} from '../main.js'; import * as assert from 'node:assert/strict'; import {test} from 'node:test'; import os from 'node:os'; @@ -19,6 +19,14 @@ test('exec', async (t) => { assert.equal(proc.exitCode, 0); }); + await t.test('non-zero exitCode throws when throwOnError=true', async () => { + const proc = x('node', ['-e', 'process.exit(1);'], {throwOnError: true}); + await assert.rejects(async () => { + await proc; + }, NonZeroExitError); + assert.equal(proc.exitCode, 1); + }); + await t.test('async iterator gets correct output', async () => { const proc = x('node', ['-e', "console.log('foo'); console.log('bar');"]); const lines = []; @@ -64,6 +72,25 @@ if (isWindows) { assert.equal(result.stdout, ''); }); + await t.test('throws spawn errors when throwOnError=true', async () => { + const proc = x('definitelyNonExistent', [], {throwOnError: true}); + await assert.rejects( + async () => { + await proc; + }, + (err) => { + assert.ok(err instanceof NonZeroExitError); + assert.equal( + err.output?.stderr, + "'definitelyNonExistent' is not recognized as an internal" + + ' or external command,\r\noperable program or batch file.\r\n' + ); + assert.equal(err.output?.stdout, ''); + return true; + } + ); + }); + await t.test('kill terminates the process', async () => { // Somewhat filthy way of waiting for 2 seconds across cmd/ps const proc = x('ping', ['127.0.0.1', '-n', '2']); @@ -100,7 +127,7 @@ if (isWindows) { await t.test('async iterator receives errors as lines', async () => { const proc = x('nonexistentforsure'); - const lines = []; + const lines: string[] = []; for await (const line of proc) { lines.push(line); }