Skip to content

Commit

Permalink
Add pipeStdout() method
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Feb 26, 2023
1 parent 2c58762 commit cfce769
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 10 deletions.
8 changes: 6 additions & 2 deletions index.js
Expand Up @@ -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';
Expand Down Expand Up @@ -97,7 +98,8 @@ export function execa(file, args, options) {
isCanceled: false,
killed: false,
}));
return mergePromise(dummySpawned, errorPromise);
mergePromise(dummySpawned, errorPromise);
return dummySpawned;
}

const spawnedPromise = getSpawnedPromise(spawned);
Expand Down Expand Up @@ -158,7 +160,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) {
Expand Down
38 changes: 38 additions & 0 deletions 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');
}
};
2 changes: 0 additions & 2 deletions lib/promise.js
Expand Up @@ -16,8 +16,6 @@ export const mergePromise = (spawned, promise) => {

Reflect.defineProperty(spawned, property, {...descriptor, value});
}

return spawned;
};

// Use promises instead of `child_process` events
Expand Down
8 changes: 2 additions & 6 deletions readme.md
Expand Up @@ -677,10 +677,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);
```

Expand All @@ -689,8 +686,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
Expand Down
71 changes: 71 additions & 0 deletions 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');

0 comments on commit cfce769

Please sign in to comment.