From 05e15548a88c75617fcf40469c39f1aa5f557f99 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 26 Feb 2023 21:36:05 +0000 Subject: [PATCH 1/9] Add `pipeStdout()` method --- index.js | 8 ++++-- lib/pipe.js | 38 +++++++++++++++++++++++++++ lib/promise.js | 2 -- readme.md | 8 ++---- test/pipe.js | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 lib/pipe.js create mode 100644 test/pipe.js diff --git a/index.js b/index.js index 5c6ac50c71..e5525b9d4c 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,7 @@ import onetime from 'onetime'; import {makeError} from './lib/error.js'; import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; +import {addPipeMethods} from './lib/pipe.js'; import {handleInput, getSpawnedResult, makeAllStream, validateInputSync} from './lib/stream.js'; import {mergePromise, getSpawnedPromise} from './lib/promise.js'; import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js'; @@ -100,7 +101,8 @@ export function execa(file, args, options) { isCanceled: false, killed: false, })); - return mergePromise(dummySpawned, errorPromise); + mergePromise(dummySpawned, errorPromise); + return dummySpawned; } const spawnedPromise = getSpawnedPromise(spawned); @@ -161,7 +163,9 @@ export function execa(file, args, options) { spawned.all = makeAllStream(spawned, parsed.options); - return mergePromise(spawned, handlePromiseOnce); + addPipeMethods(spawned); + mergePromise(spawned, handlePromiseOnce); + return spawned; } export function execaSync(file, args, options) { diff --git a/lib/pipe.js b/lib/pipe.js new file mode 100644 index 0000000000..6e39e0046e --- /dev/null +++ b/lib/pipe.js @@ -0,0 +1,38 @@ +import {createWriteStream} from 'node:fs'; +import {ChildProcess} from 'node:child_process'; +import {isWritableStream} from 'is-stream'; + +const isExecaChildProcess = target => target instanceof ChildProcess && isWritableStream(target.stdin) && typeof target.then === 'function'; + +const pipeToTarget = (spawned, streamName, target) => { + if (typeof target === 'string') { + spawned[streamName].pipe(createWriteStream(target)); + return spawned; + } + + if (isWritableStream(target)) { + spawned[streamName].pipe(target); + return spawned; + } + + if (isExecaChildProcess(target)) { + spawned[streamName].pipe(target.stdin); + return target; + } + + throw new TypeError('The second argument must be a string, a stream or an Execa child process.'); +}; + +export const addPipeMethods = spawned => { + if (spawned.stdout !== null) { + spawned.pipeStdout = pipeToTarget.bind(null, spawned, 'stdout'); + } + + if (spawned.stderr !== null) { + spawned.pipeStderr = pipeToTarget.bind(null, spawned, 'stderr'); + } + + if (spawned.all !== undefined) { + spawned.pipeAll = pipeToTarget.bind(null, spawned, 'all'); + } +}; diff --git a/lib/promise.js b/lib/promise.js index 975aee0cd6..a4773f30b0 100644 --- a/lib/promise.js +++ b/lib/promise.js @@ -16,8 +16,6 @@ export const mergePromise = (spawned, promise) => { Reflect.defineProperty(spawned, property, {...descriptor, value}); } - - return spawned; }; // Use promises instead of `child_process` events diff --git a/readme.md b/readme.md index 69aa9eaed8..0ecc4d0839 100644 --- a/readme.md +++ b/readme.md @@ -713,10 +713,7 @@ Let's say you want to show the output of a child process in real-time while also ```js import {execa} from 'execa'; -const subprocess = execa('echo', ['foo']); -subprocess.stdout.pipe(process.stdout); - -const {stdout} = await subprocess; +const {stdout} = await execa('echo', ['foo']).pipeStdout(process.stdout); console.log('child output:', stdout); ``` @@ -725,8 +722,7 @@ console.log('child output:', stdout); ```js import {execa} from 'execa'; -const subprocess = execa('echo', ['foo']) -subprocess.stdout.pipe(fs.createWriteStream('stdout.txt')) +await execa('echo', ['foo']).pipeStdout('stdout.txt') ``` ### Redirect input from a file diff --git a/test/pipe.js b/test/pipe.js new file mode 100644 index 0000000000..c7a7681d09 --- /dev/null +++ b/test/pipe.js @@ -0,0 +1,71 @@ +import {PassThrough, Readable} from 'node:stream'; +import {spawn} from 'node:child_process'; +import {readFile} from 'node:fs/promises'; +import tempfile from 'tempfile'; +import test from 'ava'; +import getStream from 'get-stream'; +import {execa} from '../index.js'; +import {setFixtureDir} from './helpers/fixtures-dir.js'; + +setFixtureDir(); + +const pipeToProcess = async (t, fixtureName, funcName) => { + const {stdout} = await execa(fixtureName, ['test'], {all: true})[funcName](execa('stdin.js')); + t.is(stdout, 'test'); +}; + +test('pipeStdout() can pipe to Execa child processes', pipeToProcess, 'noop.js', 'pipeStdout'); +test('pipeStderr() can pipe to Execa child processes', pipeToProcess, 'noop-err.js', 'pipeStderr'); +test('pipeAll() can pipe stdout to Execa child processes', pipeToProcess, 'noop.js', 'pipeAll'); +test('pipeAll() can pipe stderr to Execa child processes', pipeToProcess, 'noop-err.js', 'pipeAll'); + +const pipeToStream = async (t, fixtureName, funcName, streamName) => { + const stream = new PassThrough(); + const result = await execa(fixtureName, ['test'], {all: true})[funcName](stream); + t.is(result[streamName], 'test'); + t.is(await getStream(stream), 'test\n'); +}; + +test('pipeStdout() can pipe to streams', pipeToStream, 'noop.js', 'pipeStdout', 'stdout'); +test('pipeStderr() can pipe to streams', pipeToStream, 'noop-err.js', 'pipeStderr', 'stderr'); +test('pipeAll() can pipe stdout to streams', pipeToStream, 'noop.js', 'pipeAll', 'stdout'); +test('pipeAll() can pipe stderr to streams', pipeToStream, 'noop-err.js', 'pipeAll', 'stderr'); + +const pipeToFile = async (t, fixtureName, funcName, streamName) => { + const file = tempfile('.txt'); + const result = await execa(fixtureName, ['test'], {all: true})[funcName](file); + t.is(result[streamName], 'test'); + t.is(await readFile(file, 'utf8'), 'test\n'); +}; + +test('pipeStdout() can pipe to files', pipeToFile, 'noop.js', 'pipeStdout', 'stdout'); +test('pipeStderr() can pipe to files', pipeToFile, 'noop-err.js', 'pipeStderr', 'stderr'); +test('pipeAll() can pipe stdout to files', pipeToFile, 'noop.js', 'pipeAll', 'stdout'); +test('pipeAll() can pipe stderr to files', pipeToFile, 'noop-err.js', 'pipeAll', 'stderr'); + +const invalidTargetMacro = (t, funcName, getTarget) => { + t.throws(() => execa('noop.js', {all: true})[funcName](getTarget()), { + message: /a stream or an Execa child process/, + }); +}; + +test('pipeStdout() can only pipe to writable streams', invalidTargetMacro, 'pipeStdout', () => new Readable()); +test('pipeStderr() can only pipe to writable streams', invalidTargetMacro, 'pipeStderr', () => new Readable()); +test('pipeAll() can only pipe to writable streams', invalidTargetMacro, 'pipeAll', () => new Readable()); +test('pipeStdout() cannot pipe to processes with inherited stdin', invalidTargetMacro, 'pipeStdout', () => execa('stdin.js', {stdin: 'inherit'})); +test('pipeStderr() cannot pipe to processes with inherited stdin', invalidTargetMacro, 'pipeStderr', () => execa('stdin.js', {stdin: 'inherit'})); +test('pipeAll() cannot pipe to processes with inherited stdin', invalidTargetMacro, 'pipeStderr', () => execa('stdin.js', {stdin: 'inherit'})); +test('pipeStdout() cannot pipe to non-processes', invalidTargetMacro, 'pipeStdout', () => ({stdin: new PassThrough()})); +test('pipeStderr() cannot pipe to non-processes', invalidTargetMacro, 'pipeStderr', () => ({stdin: new PassThrough()})); +test('pipeAll() cannot pipe to non-processes', invalidTargetMacro, 'pipeStderr', () => ({stdin: new PassThrough()})); +test('pipeStdout() cannot pipe to non-Execa processes', invalidTargetMacro, 'pipeStdout', () => spawn('node', ['--version'])); +test('pipeStderr() cannot pipe to non-Execa processes', invalidTargetMacro, 'pipeStderr', () => spawn('node', ['--version'])); +test('pipeAll() cannot pipe to non-Execa processes', invalidTargetMacro, 'pipeStderr', () => spawn('node', ['--version'])); + +const invalidSourceMacro = (t, funcName) => { + t.false(funcName in execa('noop.js', {stdout: 'ignore', stderr: 'ignore'})); +}; + +test('Must set "stdout" option to "pipe" use pipeStdout()', invalidSourceMacro, 'pipeStdout'); +test('Must set "stderr" option to "pipe" use pipeStderr()', invalidSourceMacro, 'pipeStderr'); +test('Must set "stdout" or "stderr" option to "pipe" use pipeAll()', invalidSourceMacro, 'pipeAll'); From d595fbb29f3238f1aed4151d6e23a3f3ae7d0e88 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Mon, 6 Mar 2023 16:23:22 +0100 Subject: [PATCH 2/9] Update lib/pipe.js Co-authored-by: Sindre Sorhus --- lib/pipe.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pipe.js b/lib/pipe.js index 6e39e0046e..119ce1331e 100644 --- a/lib/pipe.js +++ b/lib/pipe.js @@ -25,7 +25,7 @@ const pipeToTarget = (spawned, streamName, target) => { export const addPipeMethods = spawned => { if (spawned.stdout !== null) { - spawned.pipeStdout = pipeToTarget.bind(null, spawned, 'stdout'); + spawned.pipeStdout = pipeToTarget.bind(undefined, spawned, 'stdout'); } if (spawned.stderr !== null) { From 6ca09f1394aa0b2adf56b41af021d624bb4b6f07 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 5 Mar 2023 23:16:59 +0000 Subject: [PATCH 3/9] Use `undefined` with `.bind()` --- lib/pipe.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pipe.js b/lib/pipe.js index 119ce1331e..4785597543 100644 --- a/lib/pipe.js +++ b/lib/pipe.js @@ -29,10 +29,10 @@ export const addPipeMethods = spawned => { } if (spawned.stderr !== null) { - spawned.pipeStderr = pipeToTarget.bind(null, spawned, 'stderr'); + spawned.pipeStderr = pipeToTarget.bind(undefined, spawned, 'stderr'); } if (spawned.all !== undefined) { - spawned.pipeAll = pipeToTarget.bind(null, spawned, 'all'); + spawned.pipeAll = pipeToTarget.bind(undefined, spawned, 'all'); } }; From 3e62680d0746a78761da9ae40afc5bcb75b164aa Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 5 Mar 2023 23:19:55 +0000 Subject: [PATCH 4/9] Add types and type tests --- index.d.ts | 38 +++++++++++++++++++++++++++++--------- index.test-d.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/index.d.ts b/index.d.ts index 83c172724c..71cf6fc62f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ import {type Buffer} from 'node:buffer'; import {type ChildProcess} from 'node:child_process'; -import {type Stream, type Readable as ReadableStream} from 'node:stream'; +import {type Stream, type Readable as ReadableStream, type Writable as WritableStream} from 'node:stream'; export type StdioOption = | 'pipe' @@ -285,7 +285,9 @@ export type NodeOptions = { readonly nodeOptions?: string[]; } & Options; -export type ExecaReturnBase = { +type StdoutStderrAll = string | Buffer | undefined; + +export type ExecaReturnBase = { /** The file and arguments that were run, for logging purposes. @@ -346,7 +348,7 @@ export type ExecaReturnBase = { signalDescription?: string; }; -export type ExecaSyncReturnValue = { +export type ExecaSyncReturnValue = { } & ExecaReturnBase; /** @@ -359,7 +361,7 @@ The child process fails when: - being canceled - there's not enough memory or there are already too many child processes */ -export type ExecaReturnValue = { +export type ExecaReturnValue = { /** The output of the process with `stdout` and `stderr` interleaved. @@ -377,7 +379,7 @@ export type ExecaReturnValue = { isCanceled: boolean; } & ExecaSyncReturnValue; -export type ExecaSyncError = { +export type ExecaSyncError = { /** Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored. @@ -398,7 +400,7 @@ export type ExecaSyncError = { originalMessage?: string; } & Error & ExecaReturnBase; -export type ExecaError = { +export type ExecaError = { /** The output of the process with `stdout` and `stderr` interleaved. @@ -425,7 +427,7 @@ export type KillOptions = { forceKillAfterTimeout?: number | false; }; -export type ExecaChildPromise = { +export type ExecaChildPromise = { /** Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr). @@ -448,9 +450,27 @@ export type ExecaChildPromise = { Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This used to be preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. But now this is deprecated and you should either use `.kill()` or the `signal` option when creating the child process. */ cancel(): void; + + /** + + */ + pipeStdout?>(target: Target): Target; + pipeStdout?(target: WritableStream | string): ExecaChildProcess; + + /** + + */ + pipeStderr?>(target: Target): Target; + pipeStderr?(target: WritableStream | string): ExecaChildProcess; + + /** + + */ + pipeAll?>(target: Target): Target; + pipeAll?(target: WritableStream | string): ExecaChildProcess; }; -export type ExecaChildProcess = ChildProcess & +export type ExecaChildProcess = ChildProcess & ExecaChildPromise & Promise>; @@ -557,7 +577,7 @@ type TemplateExpression = | ExecaSyncReturnValue | Array | ExecaSyncReturnValue>; -type Execa$ = { +type Execa$ = { /** Same as `execa()` except both file and arguments are specified in a single tagged template string. For example, `` $`echo unicorns` `` is the same as `execa('echo', ['unicorns'])`. diff --git a/index.test-d.ts b/index.test-d.ts index 17b383f9ee..c735237680 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -4,7 +4,8 @@ import {Buffer} from 'node:buffer'; // to get treated as `any` by `@typescript-eslint/no-unsafe-assignment`. import * as process from 'node:process'; import {type Readable as ReadableStream} from 'node:stream'; -import {expectType, expectError} from 'tsd'; +import {createWriteStream} from 'node:fs'; +import {expectType, expectError, expectAssignable} from 'tsd'; import { $, execa, @@ -24,6 +25,39 @@ try { execaPromise.cancel(); expectType(execaPromise.all); + const execaBufferPromise = execa('unicorns', {encoding: null}); + const writeStream = createWriteStream('output.txt'); + + expectAssignable(execaPromise.pipeStdout); + expectType(execaPromise.pipeStdout!('file.txt')); + expectType>(execaBufferPromise.pipeStdout!('file.txt')); + expectType(execaPromise.pipeStdout!(writeStream)); + expectType>(execaBufferPromise.pipeStdout!(writeStream)); + expectType(execaPromise.pipeStdout!(execaPromise)); + expectType>(execaPromise.pipeStdout!(execaBufferPromise)); + expectType(execaBufferPromise.pipeStdout!(execaPromise)); + expectType>(execaBufferPromise.pipeStdout!(execaBufferPromise)); + + expectAssignable(execaPromise.pipeStderr); + expectType(execaPromise.pipeStderr!('file.txt')); + expectType>(execaBufferPromise.pipeStderr!('file.txt')); + expectType(execaPromise.pipeStderr!(writeStream)); + expectType>(execaBufferPromise.pipeStderr!(writeStream)); + expectType(execaPromise.pipeStderr!(execaPromise)); + expectType>(execaPromise.pipeStderr!(execaBufferPromise)); + expectType(execaBufferPromise.pipeStderr!(execaPromise)); + expectType>(execaBufferPromise.pipeStderr!(execaBufferPromise)); + + expectAssignable(execaPromise.pipeAll); + expectType(execaPromise.pipeAll!('file.txt')); + expectType>(execaBufferPromise.pipeAll!('file.txt')); + expectType(execaPromise.pipeAll!(writeStream)); + expectType>(execaBufferPromise.pipeAll!(writeStream)); + expectType(execaPromise.pipeAll!(execaPromise)); + expectType>(execaPromise.pipeAll!(execaBufferPromise)); + expectType(execaBufferPromise.pipeAll!(execaPromise)); + expectType>(execaBufferPromise.pipeAll!(execaBufferPromise)); + const unicornsResult = await execaPromise; expectType(unicornsResult.command); expectType(unicornsResult.escapedCommand); From b661de5e9d2113f2d6e8fd543ce0cad45c0c518b Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 5 Mar 2023 23:22:05 +0000 Subject: [PATCH 5/9] Document new pipe methods --- index.test-d.ts | 3 ++ readme.md | 76 +++++++++++++++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index c735237680..15208999d6 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -97,6 +97,9 @@ try { expectType(unicornsResult.stdout); expectType(unicornsResult.stderr); expectError(unicornsResult.all); + expectError(unicornsResult.pipeStdout); + expectError(unicornsResult.pipeStderr); + expectError(unicornsResult.pipeAll); expectType(unicornsResult.failed); expectType(unicornsResult.timedOut); expectError(unicornsResult.isCanceled); diff --git a/readme.md b/readme.md index 0ecc4d0839..0e8f99260f 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,7 @@ This package improves [`child_process`](https://nodejs.org/api/child_process.htm - [Executes locally installed binaries by name.](#preferlocal) - [Cleans up spawned processes when the parent process dies.](#cleanup) - [Get interleaved output](#all) from `stdout` and `stderr` similar to what is printed on the terminal. [*(Async only)*](#execasyncfile-arguments-options) +- Convenience methods to [pipe processes' output](#redirect-output-to-a-file) - [Can specify file and arguments as a single string without a shell](#execacommandcommand-options) - More descriptive errors. @@ -115,12 +116,41 @@ unicorns rainbows ``` -### Pipe the child process stdout to the parent +### Redirect output to a file + +```js +import {execa} from 'execa'; + +// Similar to `echo unicorns > stdout.txt` in Bash +await execa('echo', ['unicorns']).pipeStdout('stdout.txt') + +// Similar to `echo unicorns 2> stdout.txt` in Bash +await execa('echo', ['unicorns']).pipeStderr('stderr.txt') + +// Similar to `echo unicorns &> stdout.txt` in Bash +await execa('echo', ['unicorns'], {all:true}).pipeAll('all.txt') +``` + +### Save and pipe output from a child process + +```js +import {execa} from 'execa'; + +const {stdout} = await execa('echo', ['unicorns']).pipeStdout(process.stdout); +// Prints `unicorns` +console.log(stdout); +// Also returns 'unicorns' +``` + +### Pipe multiple processes ```js import {execa} from 'execa'; -execa('echo', ['unicorns']).stdout.pipe(process.stdout); +// Similar to `echo unicorns | cat` in Bash +const {stdout} = await execa('echo', ['unicorns']).pipeStdout(execa('cat')); +console.log(stdout); +//=> 'unicorns' ``` ### Handling Errors @@ -261,6 +291,29 @@ This is `undefined` if either: - the [`all` option](#all-2) is `false` (the default value) - both [`stdout`](#stdout-1) and [`stderr`](#stderr-1) options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio) +#### pipeStdout(target) + +[Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be: + - Another [`execa()` return value](#pipe-multiple-processes) + - A [writable stream](#save-and-pipe-output-from-a-child-process) + - A [file path string](#redirect-output-to-a-file) + +If the `target` is another [`execa()` return value](#execacommandcommand-options), it is returned. Otherwise, the original `execa()` return value is returned. + +This requires the [`stdout` option](#stdout-1) to be kept as `pipe`, its default value. + +#### pipeStderr(target) + +Like [`pipeStdout()`](#pipestdouttarget) but piping the child process's `stderr` instead. + +This requires the [`stderr` option](#stderr-1) to be kept as `pipe`, its default value. + +#### pipeAll(target) + +Combines both [`pipeStdout()`](#pipestdouttarget) and [`pipeStderr()`](#pipestderrtarget). + +This requires either the [`stdout` option](#stdout-1) or the [`stderr` option](#stderr-1) to be kept as `pipe`, their default value. Also, the [`all` option](#all-2) must be set to `true`. + ### execaSync(file, arguments?, options?) Execute a file synchronously. @@ -706,25 +759,6 @@ const run = async () => { console.log(await pRetry(run, {retries: 5})); ``` -### Save and pipe output from a child process - -Let's say you want to show the output of a child process in real-time while also saving it to a variable. - -```js -import {execa} from 'execa'; - -const {stdout} = await execa('echo', ['foo']).pipeStdout(process.stdout); -console.log('child output:', stdout); -``` - -### Redirect output to a file - -```js -import {execa} from 'execa'; - -await execa('echo', ['foo']).pipeStdout('stdout.txt') -``` - ### Redirect input from a file ```js From fb063abf0a8dea65ac98c84ab1ab561b36aa7988 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 5 Mar 2023 23:22:26 +0000 Subject: [PATCH 6/9] Improve documentation --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 0e8f99260f..c16583e6d5 100644 --- a/readme.md +++ b/readme.md @@ -298,21 +298,21 @@ This is `undefined` if either: - A [writable stream](#save-and-pipe-output-from-a-child-process) - A [file path string](#redirect-output-to-a-file) -If the `target` is another [`execa()` return value](#execacommandcommand-options), it is returned. Otherwise, the original `execa()` return value is returned. +If the `target` is another [`execa()` return value](#execacommandcommand-options), it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the [final result](#childprocessresult). -This requires the [`stdout` option](#stdout-1) to be kept as `pipe`, its default value. +The [`stdout` option](#stdout-1) must be kept as `pipe`, its default value. #### pipeStderr(target) Like [`pipeStdout()`](#pipestdouttarget) but piping the child process's `stderr` instead. -This requires the [`stderr` option](#stderr-1) to be kept as `pipe`, its default value. +The [`stderr` option](#stderr-1) must be kept as `pipe`, its default value. #### pipeAll(target) Combines both [`pipeStdout()`](#pipestdouttarget) and [`pipeStderr()`](#pipestderrtarget). -This requires either the [`stdout` option](#stdout-1) or the [`stderr` option](#stderr-1) to be kept as `pipe`, their default value. Also, the [`all` option](#all-2) must be set to `true`. +Either the [`stdout` option](#stdout-1) or the [`stderr` option](#stderr-1) must be kept as `pipe`, their default value. Also, the [`all` option](#all-2) must be set to `true`. ### execaSync(file, arguments?, options?) From 516a6dd652f84af8307ac6e2c73b8bcece3de839 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 5 Mar 2023 23:22:39 +0000 Subject: [PATCH 7/9] Add documentation to types --- index.d.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 71cf6fc62f..a332f1da6f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -452,20 +452,31 @@ export type ExecaChildPromise = { cancel(): void; /** + [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be: + - Another `execa()` return value + - A writable stream + - A file path string - */ + If the `target` is another `execa()` return value, it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the final result. + + The `stdout` option] must be kept as `pipe`, its default value. + */ pipeStdout?>(target: Target): Target; pipeStdout?(target: WritableStream | string): ExecaChildProcess; /** + Like `pipeStdout()` but piping the child process's `stderr` instead. - */ + The `stderr` option must be kept as `pipe`, its default value. + */ pipeStderr?>(target: Target): Target; pipeStderr?(target: WritableStream | string): ExecaChildProcess; /** + Combines both `pipeStdout()` and `pipeStderr()`. - */ + Either the `stdout` option or the `stderr` option must be kept as `pipe`, their default value. Also, the `all` option must be set to `true`. + */ pipeAll?>(target: Target): Target; pipeAll?(target: WritableStream | string): ExecaChildProcess; }; From 18be1cb6144e582272e6b40f5fd1699f1561ae3f Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 5 Mar 2023 23:29:54 +0000 Subject: [PATCH 8/9] Improve validation --- lib/pipe.js | 14 +++++++++----- test/pipe.js | 41 ++++++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/pipe.js b/lib/pipe.js index 4785597543..e73ffcc989 100644 --- a/lib/pipe.js +++ b/lib/pipe.js @@ -2,7 +2,7 @@ import {createWriteStream} from 'node:fs'; import {ChildProcess} from 'node:child_process'; import {isWritableStream} from 'is-stream'; -const isExecaChildProcess = target => target instanceof ChildProcess && isWritableStream(target.stdin) && typeof target.then === 'function'; +const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function'; const pipeToTarget = (spawned, streamName, target) => { if (typeof target === 'string') { @@ -15,12 +15,16 @@ const pipeToTarget = (spawned, streamName, target) => { return spawned; } - if (isExecaChildProcess(target)) { - spawned[streamName].pipe(target.stdin); - return target; + if (!isExecaChildProcess(target)) { + throw new TypeError('The second argument must be a string, a stream or an Execa child process.'); } - throw new TypeError('The second argument must be a string, a stream or an Execa child process.'); + if (!isWritableStream(target.stdin)) { + throw new TypeError('The target child process\'s stdin must be available.'); + } + + spawned[streamName].pipe(target.stdin); + return target; }; export const addPipeMethods = spawned => { diff --git a/test/pipe.js b/test/pipe.js index c7a7681d09..b465440e10 100644 --- a/test/pipe.js +++ b/test/pipe.js @@ -43,29 +43,36 @@ test('pipeStderr() can pipe to files', pipeToFile, 'noop-err.js', 'pipeStderr', test('pipeAll() can pipe stdout to files', pipeToFile, 'noop.js', 'pipeAll', 'stdout'); test('pipeAll() can pipe stderr to files', pipeToFile, 'noop-err.js', 'pipeAll', 'stderr'); -const invalidTargetMacro = (t, funcName, getTarget) => { +const invalidTarget = (t, funcName, getTarget) => { t.throws(() => execa('noop.js', {all: true})[funcName](getTarget()), { message: /a stream or an Execa child process/, }); }; -test('pipeStdout() can only pipe to writable streams', invalidTargetMacro, 'pipeStdout', () => new Readable()); -test('pipeStderr() can only pipe to writable streams', invalidTargetMacro, 'pipeStderr', () => new Readable()); -test('pipeAll() can only pipe to writable streams', invalidTargetMacro, 'pipeAll', () => new Readable()); -test('pipeStdout() cannot pipe to processes with inherited stdin', invalidTargetMacro, 'pipeStdout', () => execa('stdin.js', {stdin: 'inherit'})); -test('pipeStderr() cannot pipe to processes with inherited stdin', invalidTargetMacro, 'pipeStderr', () => execa('stdin.js', {stdin: 'inherit'})); -test('pipeAll() cannot pipe to processes with inherited stdin', invalidTargetMacro, 'pipeStderr', () => execa('stdin.js', {stdin: 'inherit'})); -test('pipeStdout() cannot pipe to non-processes', invalidTargetMacro, 'pipeStdout', () => ({stdin: new PassThrough()})); -test('pipeStderr() cannot pipe to non-processes', invalidTargetMacro, 'pipeStderr', () => ({stdin: new PassThrough()})); -test('pipeAll() cannot pipe to non-processes', invalidTargetMacro, 'pipeStderr', () => ({stdin: new PassThrough()})); -test('pipeStdout() cannot pipe to non-Execa processes', invalidTargetMacro, 'pipeStdout', () => spawn('node', ['--version'])); -test('pipeStderr() cannot pipe to non-Execa processes', invalidTargetMacro, 'pipeStderr', () => spawn('node', ['--version'])); -test('pipeAll() cannot pipe to non-Execa processes', invalidTargetMacro, 'pipeStderr', () => spawn('node', ['--version'])); +test('pipeStdout() can only pipe to writable streams', invalidTarget, 'pipeStdout', () => new Readable()); +test('pipeStderr() can only pipe to writable streams', invalidTarget, 'pipeStderr', () => new Readable()); +test('pipeAll() can only pipe to writable streams', invalidTarget, 'pipeAll', () => new Readable()); +test('pipeStdout() cannot pipe to non-processes', invalidTarget, 'pipeStdout', () => ({stdin: new PassThrough()})); +test('pipeStderr() cannot pipe to non-processes', invalidTarget, 'pipeStderr', () => ({stdin: new PassThrough()})); +test('pipeAll() cannot pipe to non-processes', invalidTarget, 'pipeStderr', () => ({stdin: new PassThrough()})); +test('pipeStdout() cannot pipe to non-Execa processes', invalidTarget, 'pipeStdout', () => spawn('node', ['--version'])); +test('pipeStderr() cannot pipe to non-Execa processes', invalidTarget, 'pipeStderr', () => spawn('node', ['--version'])); +test('pipeAll() cannot pipe to non-Execa processes', invalidTarget, 'pipeStderr', () => spawn('node', ['--version'])); -const invalidSourceMacro = (t, funcName) => { +const invalidSource = (t, funcName) => { t.false(funcName in execa('noop.js', {stdout: 'ignore', stderr: 'ignore'})); }; -test('Must set "stdout" option to "pipe" use pipeStdout()', invalidSourceMacro, 'pipeStdout'); -test('Must set "stderr" option to "pipe" use pipeStderr()', invalidSourceMacro, 'pipeStderr'); -test('Must set "stdout" or "stderr" option to "pipe" use pipeAll()', invalidSourceMacro, 'pipeAll'); +test('Must set "stdout" option to "pipe" to use pipeStdout()', invalidSource, 'pipeStdout'); +test('Must set "stderr" option to "pipe" to use pipeStderr()', invalidSource, 'pipeStderr'); +test('Must set "stdout" or "stderr" option to "pipe" to use pipeAll()', invalidSource, 'pipeAll'); + +const invalidPipeToProcess = async (t, fixtureName, funcName) => { + t.throws(() => execa(fixtureName, ['test'], {all: true})[funcName](execa('stdin.js', {stdin: 'ignore'})), { + message: /stdin must be available/, + }); +}; + +test('Must set target "stdin" option to "pipe" to use pipeStdout()', invalidPipeToProcess, 'noop.js', 'pipeStdout'); +test('Must set target "stdin" option to "pipe" to use pipeStderr()', invalidPipeToProcess, 'noop-err.js', 'pipeStderr'); +test('Must set target "stdin" option to "pipe" to use pipeAll()', invalidPipeToProcess, 'noop.js', 'pipeAll'); From ecfedaec37cc68581f90650efe82f6fecd4118c6 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 7 Mar 2023 01:58:13 +0700 Subject: [PATCH 9/9] Update index.d.ts --- index.d.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index a332f1da6f..6c45b6265c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -453,29 +453,29 @@ export type ExecaChildPromise = { /** [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be: - - Another `execa()` return value - - A writable stream - - A file path string + - Another `execa()` return value + - A writable stream + - A file path string - If the `target` is another `execa()` return value, it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the final result. + If the `target` is another `execa()` return value, it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the final result. - The `stdout` option] must be kept as `pipe`, its default value. + The `stdout` option] must be kept as `pipe`, its default value. */ pipeStdout?>(target: Target): Target; pipeStdout?(target: WritableStream | string): ExecaChildProcess; /** - Like `pipeStdout()` but piping the child process's `stderr` instead. + Like `pipeStdout()` but piping the child process's `stderr` instead. - The `stderr` option must be kept as `pipe`, its default value. + The `stderr` option must be kept as `pipe`, its default value. */ pipeStderr?>(target: Target): Target; pipeStderr?(target: WritableStream | string): ExecaChildProcess; /** - Combines both `pipeStdout()` and `pipeStderr()`. + Combines both `pipeStdout()` and `pipeStderr()`. - Either the `stdout` option or the `stderr` option must be kept as `pipe`, their default value. Also, the `all` option must be set to `true`. + Either the `stdout` option or the `stderr` option must be kept as `pipe`, their default value. Also, the `all` option must be set to `true`. */ pipeAll?>(target: Target): Target; pipeAll?(target: WritableStream | string): ExecaChildProcess;