Skip to content

Commit 9554a78

Browse files
authored
Improve process piping API (#757)
1 parent 192d818 commit 9554a78

File tree

7 files changed

+167
-109
lines changed

7 files changed

+167
-109
lines changed

docs/scripts.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ With Execa, you can write scripts with Node.js instead of a shell language. It i
66
import {$} from 'execa';
77

88
const {stdout: name} = await $`cat package.json`
9-
.pipeStdout($({stdin: 'pipe'})`grep name`);
9+
.pipe($({stdin: 'pipe'})`grep name`);
1010
console.log(name);
1111

1212
const branch = await $`git branch --show-current`;
@@ -598,7 +598,7 @@ await $`echo example | cat`;
598598
599599
```js
600600
// Execa
601-
await $`echo example`.pipeStdout($({stdin: 'pipe'})`cat`);
601+
await $`echo example`.pipe($({stdin: 'pipe'})`cat`);
602602
```
603603
604604
### Piping stdout and stderr to another command
@@ -619,7 +619,7 @@ await Promise.all([echo, cat]);
619619
620620
```js
621621
// Execa
622-
await $({all: true})`echo example`.pipeAll($({stdin: 'pipe'})`cat`);
622+
await $({all: true})`echo example`.pipe($({stdin: 'pipe'})`cat`, 'all');
623623
```
624624
625625
### Piping stdout to a file

index.d.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -812,25 +812,13 @@ export type ExecaChildPromise<OptionsType extends Options = Options> = {
812812
/**
813813
[Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process' `stdout` to another Execa child process' `stdin`.
814814
815-
Returns `execaChildProcess`, which allows chaining `pipeStdout()` then `await`ing the final result.
815+
A `streamName` can be passed to pipe `"stderr"`, `"all"` (both `stdout` and `stderr`) or any another file descriptor instead of `stdout`.
816816
817-
`childProcess.stdout` must not be `undefined`.
818-
*/
819-
pipeStdout?<Target extends ExecaChildProcess>(target: Target): Target;
820-
821-
/**
822-
Like `pipeStdout()` but piping the child process's `stderr` instead.
823-
824-
`childProcess.stderr` must not be `undefined`.
825-
*/
826-
pipeStderr?<Target extends ExecaChildProcess>(target: Target): Target;
827-
828-
/**
829-
Combines both `pipeStdout()` and `pipeStderr()`.
817+
`childProcess.stdout` (and/or `childProcess.stderr` depending on `streamName`) must not be `undefined`. When `streamName` is `"all"`, the `all` option must be set to `true`.
830818
831-
The `all` option must be set to `true`.
819+
Returns `execaChildProcess`, which allows chaining `.pipe()` then `await`ing the final result.
832820
*/
833-
pipeAll?<Target extends ExecaChildProcess>(target: Target): Target;
821+
pipe<Target extends ExecaChildProcess>(target: Target, streamName?: 'stdout' | 'stderr' | 'all' | number): Target;
834822
};
835823

836824
export type ExecaChildProcess<OptionsType extends Options = Options> = ChildProcess &
@@ -865,13 +853,13 @@ console.log(stdout);
865853
import {execa} from 'execa';
866854
867855
// Similar to `echo unicorns > stdout.txt` in Bash
868-
await execa('echo', ['unicorns']).pipeStdout('stdout.txt');
856+
await execa('echo', ['unicorns'], {stdout: {file: 'stdout.txt'}});
869857
870858
// Similar to `echo unicorns 2> stdout.txt` in Bash
871-
await execa('echo', ['unicorns']).pipeStderr('stderr.txt');
859+
await execa('echo', ['unicorns'], {stderr: {file: 'stderr.txt'}});
872860
873861
// Similar to `echo unicorns &> stdout.txt` in Bash
874-
await execa('echo', ['unicorns'], {all: true}).pipeAll('all.txt');
862+
await execa('echo', ['unicorns'], {stdout: {file: 'all.txt'}, stderr: {file: 'all.txt'}});
875863
```
876864
877865
@example <caption>Redirect input from a file</caption>
@@ -888,7 +876,7 @@ console.log(stdout);
888876
```
889877
import {execa} from 'execa';
890878
891-
const {stdout} = await execa('echo', ['unicorns']).pipeStdout(process.stdout);
879+
const {stdout} = await execa('echo', ['unicorns'], {stdout: ['pipe', 'inherit']});
892880
// Prints `unicorns`
893881
console.log(stdout);
894882
// Also returns 'unicorns'
@@ -899,7 +887,7 @@ console.log(stdout);
899887
import {execa} from 'execa';
900888
901889
// Similar to `echo unicorns | cat` in Bash
902-
const {stdout} = await execa('echo', ['unicorns']).pipeStdout(execa('cat'));
890+
const {stdout} = await execa('echo', ['unicorns']).pipe(execa('cat'));
903891
console.log(stdout);
904892
//=> 'unicorns'
905893
```

index.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {handleInputAsync, pipeOutputAsync} from './lib/stdio/async.js';
1111
import {handleInputSync, pipeOutputSync} from './lib/stdio/sync.js';
1212
import {normalizeStdioNode} from './lib/stdio/normalize.js';
1313
import {spawnedKill, validateTimeout, normalizeForceKillAfterDelay} from './lib/kill.js';
14-
import {addPipeMethods} from './lib/pipe.js';
14+
import {pipeToProcess} from './lib/pipe.js';
1515
import {getSpawnedResult, makeAllStream} from './lib/stream.js';
1616
import {mergePromise} from './lib/promise.js';
1717
import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js';
@@ -154,10 +154,9 @@ export function execa(rawFile, rawArgs, rawOptions) {
154154

155155
pipeOutputAsync(spawned, stdioStreamsGroups);
156156

157-
spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned), options, controller);
157+
spawned.kill = spawnedKill.bind(undefined, spawned.kill.bind(spawned), options, controller);
158158
spawned.all = makeAllStream(spawned, options);
159-
160-
addPipeMethods(spawned);
159+
spawned.pipe = pipeToProcess.bind(undefined, {spawned, stdioStreamsGroups, options});
161160

162161
const promise = handlePromise({spawned, options, stdioStreamsGroups, command, escapedCommand, controller});
163162
mergePromise(spawned, promise);

index.test-d.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,14 @@ try {
8282
const execaBufferPromise = execa('unicorns', {encoding: 'buffer', all: true});
8383
const writeStream = createWriteStream('output.txt');
8484

85-
expectAssignable<Function | undefined>(execaPromise.pipeStdout);
86-
expectAssignable<ExecaChildProcess>(execaPromise.pipeStdout!(execaPromise));
87-
expectAssignable<ExecaChildProcess>(execaPromise.pipeStdout!(execaBufferPromise));
88-
expectAssignable<ExecaChildProcess>(execaBufferPromise.pipeStdout!(execaPromise));
89-
expectAssignable<ExecaChildProcess>(execaBufferPromise.pipeStdout!(execaBufferPromise));
90-
91-
expectAssignable<Function | undefined>(execaPromise.pipeStderr);
92-
expectAssignable<ExecaChildProcess>(execaPromise.pipeStderr!(execaPromise));
93-
expectAssignable<ExecaChildProcess>(execaPromise.pipeStderr!(execaBufferPromise));
94-
expectAssignable<ExecaChildProcess>(execaBufferPromise.pipeStderr!(execaPromise));
95-
expectAssignable<ExecaChildProcess>(execaBufferPromise.pipeStderr!(execaBufferPromise));
96-
97-
expectAssignable<Function | undefined>(execaPromise.pipeAll);
98-
expectAssignable<ExecaChildProcess>(execaPromise.pipeAll!(execaPromise));
99-
expectAssignable<ExecaChildProcess>(execaPromise.pipeAll!(execaBufferPromise));
100-
expectAssignable<ExecaChildProcess>(execaBufferPromise.pipeAll!(execaPromise));
101-
expectAssignable<ExecaChildProcess>(execaBufferPromise.pipeAll!(execaBufferPromise));
85+
expectType<typeof execaPromise>(execaBufferPromise.pipe(execaPromise));
86+
expectError(execaBufferPromise.pipe(writeStream));
87+
expectError(execaBufferPromise.pipe('output.txt'));
88+
await execaBufferPromise.pipe(execaPromise, 'stdout');
89+
await execaBufferPromise.pipe(execaPromise, 'stderr');
90+
await execaBufferPromise.pipe(execaPromise, 'all');
91+
await execaBufferPromise.pipe(execaPromise, 3);
92+
expectError(execaBufferPromise.pipe(execaPromise, 'other'));
10293

10394
expectType<Readable>(execaPromise.all);
10495
const noAllPromise = execa('unicorns');
@@ -551,9 +542,7 @@ try {
551542
expectType<string>(unicornsResult.command);
552543
expectType<string>(unicornsResult.escapedCommand);
553544
expectType<number | undefined>(unicornsResult.exitCode);
554-
expectError(unicornsResult.pipeStdout);
555-
expectError(unicornsResult.pipeStderr);
556-
expectError(unicornsResult.pipeAll);
545+
expectError(unicornsResult.pipe);
557546
expectType<boolean>(unicornsResult.failed);
558547
expectType<boolean>(unicornsResult.timedOut);
559548
expectType<boolean>(unicornsResult.isCanceled);

lib/pipe.js

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,79 @@
11
import {ChildProcess} from 'node:child_process';
22
import {isWritableStream} from 'is-stream';
33

4-
const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function';
4+
export const pipeToProcess = ({spawned, stdioStreamsGroups, options}, targetProcess, streamName = 'stdout') => {
5+
validateTargetProcess(targetProcess);
6+
7+
const inputStream = getInputStream(spawned, streamName, stdioStreamsGroups);
8+
validateStdioOption(inputStream, spawned, streamName, options);
9+
10+
inputStream.pipe(targetProcess.stdin);
11+
return targetProcess;
12+
};
513

6-
const pipeToTarget = (spawned, streamName, target) => {
7-
if (!isExecaChildProcess(target)) {
8-
throw new TypeError('The second argument must be an Execa child process.');
14+
const validateTargetProcess = targetProcess => {
15+
if (!isExecaChildProcess(targetProcess)) {
16+
throw new TypeError('The first argument must be an Execa child process.');
917
}
1018

11-
if (!isWritableStream(target.stdin)) {
19+
if (!isWritableStream(targetProcess.stdin)) {
1220
throw new TypeError('The target child process\'s stdin must be available.');
1321
}
22+
};
23+
24+
const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function';
25+
26+
const getInputStream = (spawned, streamName, stdioStreamsGroups) => {
27+
if (VALID_STREAM_NAMES.has(streamName)) {
28+
return spawned[streamName];
29+
}
1430

15-
spawned[streamName].pipe(target.stdin);
16-
return target;
31+
if (streamName === 'stdin') {
32+
throw new TypeError('The second argument must not be "stdin".');
33+
}
34+
35+
if (!Number.isInteger(streamName) || streamName < 0) {
36+
throw new TypeError(`The second argument must not be "${streamName}".
37+
It must be "stdout", "stderr", "all" or a file descriptor integer.
38+
It is optional and defaults to "stdout".`);
39+
}
40+
41+
const stdioStreams = stdioStreamsGroups[streamName];
42+
if (stdioStreams === undefined) {
43+
throw new TypeError(`The second argument must not be ${streamName}: that file descriptor does not exist.
44+
Please set the "stdio" option to ensure that file descriptor exists.`);
45+
}
46+
47+
if (stdioStreams[0].direction === 'input') {
48+
throw new TypeError(`The second argument must not be ${streamName}: it must be a readable stream, not writable.`);
49+
}
50+
51+
return spawned.stdio[streamName];
1752
};
1853

19-
export const addPipeMethods = spawned => {
20-
if (spawned.stdout !== null) {
21-
spawned.pipeStdout = pipeToTarget.bind(undefined, spawned, 'stdout');
54+
const VALID_STREAM_NAMES = new Set(['stdout', 'stderr', 'all']);
55+
56+
const validateStdioOption = (inputStream, spawned, streamName, options) => {
57+
if (inputStream !== null && inputStream !== undefined) {
58+
return;
2259
}
2360

24-
if (spawned.stderr !== null) {
25-
spawned.pipeStderr = pipeToTarget.bind(undefined, spawned, 'stderr');
61+
if (streamName === 'all' && !options.all) {
62+
throw new TypeError('The "all" option must be true to use `childProcess.pipe(targetProcess, "all")`.');
2663
}
2764

28-
if (spawned.all !== undefined) {
29-
spawned.pipeAll = pipeToTarget.bind(undefined, spawned, 'all');
65+
throw new TypeError(`The "${getInvalidStdioOption(inputStream, spawned, options)}" option's value is incompatible with using \`childProcess.pipe(targetProcess)\`.
66+
Please set this option with "pipe" instead.`);
67+
};
68+
69+
const getInvalidStdioOption = (inputStream, spawned, options) => {
70+
if (inputStream === spawned.stdout && options.stdout !== undefined) {
71+
return 'stdout';
3072
}
73+
74+
if (inputStream === spawned.stderr && options.stderr !== undefined) {
75+
return 'stderr';
76+
}
77+
78+
return 'stdio';
3179
};

readme.md

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ console.log(stdout);
183183
import {execa} from 'execa';
184184

185185
// Similar to `echo unicorns | cat` in Bash
186-
const {stdout} = await execa('echo', ['unicorns']).pipeStdout(execa('cat'));
186+
const {stdout} = await execa('echo', ['unicorns']).pipe(execa('cat'));
187187
console.log(stdout);
188188
//=> 'unicorns'
189189
```
@@ -319,31 +319,18 @@ This is `undefined` if either:
319319
- the [`all` option](#all-2) is `false` (the default value)
320320
- both [`stdout`](#stdout-1) and [`stderr`](#stderr-1) options are set to [`'inherit'`, `'ipc'`, `'ignore'`, `Stream` or `integer`](https://nodejs.org/api/child_process.html#child_process_options_stdio)
321321

322-
#### pipeStdout(execaChildProcess)
322+
#### pipe(execaChildProcess, streamName?)
323323

324324
`execaChildProcess`: [`execa()` return value](#pipe-multiple-processes)
325+
`streamName`: `"stdout"` (default), `"stderr"`, `"all"` or file descriptor index
325326

326327
[Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process' `stdout` to another Execa child process' `stdin`.
327328

328-
Returns `execaChildProcess`, which allows chaining `pipeStdout()` then `await`ing the [final result](#childprocessresult).
329+
A `streamName` can be passed to pipe `"stderr"`, `"all"` (both `stdout` and `stderr`) or any another file descriptor instead of `stdout`.
329330

330-
[`childProcess.stdout`](#stdout) must not be `undefined`.
331+
[`childProcess.stdout`](#stdout) (and/or [`childProcess.stderr`](#stderr) depending on `streamName`) must not be `undefined`. When `streamName` is `"all"`, the [`all` option](#all-2) must be set to `true`.
331332

332-
#### pipeStderr(execaChildProcess)
333-
334-
`execaChildProcess`: [`execa()` return value](#pipe-multiple-processes)
335-
336-
Like [`pipeStdout()`](#pipestdoutexecachildprocess) but piping the child process's `stderr` instead.
337-
338-
[`childProcess.stderr`](#stderr) must not be `undefined`.
339-
340-
#### pipeAll(execaChildProcess)
341-
342-
`execaChildProcess`: [`execa()` return value](#pipe-multiple-processes)
343-
344-
Combines both [`pipeStdout()`](#pipestdoutexecachildprocess) and [`pipeStderr()`](#pipestderrexecachildprocess).
345-
346-
The [`all` option](#all-2) must be set to `true`.
333+
Returns `execaChildProcess`, which allows chaining `.pipe()` then `await`ing the [final result](#childprocessresult).
347334

348335
### childProcessResult
349336

0 commit comments

Comments
 (0)