Skip to content

Commit

Permalink
Add .node() method (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
GMartigny authored and sindresorhus committed Jun 18, 2019
1 parent be1d70f commit c2f5edf
Show file tree
Hide file tree
Showing 33 changed files with 276 additions and 80 deletions.
8 changes: 0 additions & 8 deletions fixtures/detach

This file was deleted.

3 changes: 0 additions & 3 deletions fixtures/echo

This file was deleted.

39 changes: 37 additions & 2 deletions index.d.ts
Expand Up @@ -187,14 +187,29 @@ declare namespace execa {
readonly input?: string | Buffer | ReadableStream;
}

interface SyncOptions<EncodingType = string>
extends CommonOptions<EncodingType> {
interface SyncOptions<EncodingType = string> extends CommonOptions<EncodingType> {
/**
Write some input to the `stdin` of your binary.
*/
readonly input?: string | Buffer;
}

interface NodeOptions<EncodingType = string> extends Options<EncodingType> {
/**
The Node.js executable to use.
@default process.execPath
*/
readonly nodePath?: string;

/**
List of string arguments passed to the Node.js executable.
@default process.execArgv
*/
readonly nodeArguments?: string[];
}

interface ExecaReturnBase<StdoutStderrType> {
/**
The file and arguments that were run.
Expand Down Expand Up @@ -417,6 +432,26 @@ declare const execa: {
*/
commandSync(command: string, options?: execa.SyncOptions): execa.ExecaSyncReturnValue;
commandSync(command: string, options?: execa.SyncOptions<null>): execa.ExecaSyncReturnValue<Buffer>;

/**
Execute a Node.js script as a child process.
@param scriptPath - Node.js script to execute.
@param arguments - Arguments to pass to `scriptPath` on execution.
@returns A [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess), which is enhanced to also be a `Promise` for a result `Object` with `stdout` and `stderr` properties.
*/
node(
scriptPath: string,
arguments?: readonly string[],
options?: execa.NodeOptions
): execa.ExecaChildProcess;
node(
file: string,
arguments?: readonly string[],
options?: execa.Options<null>
): execa.ExecaChildProcess<Buffer>;
node(file: string, options?: execa.Options): execa.ExecaChildProcess;
node(file: string, options?: execa.Options<null>): execa.ExecaChildProcess<Buffer>;
};

export = execa;
27 changes: 27 additions & 0 deletions index.js
Expand Up @@ -520,3 +520,30 @@ module.exports.commandSync = (command, options) => {
const [file, ...args] = parseCommand(command);
return execa.sync(file, args, options);
};

module.exports.node = (scriptPath, args, options) => {
if (args && !Array.isArray(args) && typeof args === 'object') {
options = args;
args = [];
}

const stdioOption = stdio.node(options);
options = options || {};

return execa(
options.nodePath || process.execPath,
[
...(options.nodeArguments || process.execArgv),
scriptPath,
...(Array.isArray(args) ? args : [])
],
{
...options,
stdin: undefined,
stdout: undefined,
stderr: undefined,
stdio: stdioOption,
shell: false
}
);
};
13 changes: 13 additions & 0 deletions index.test-d.ts
Expand Up @@ -166,3 +166,16 @@ expectType<ExecaSyncReturnValue<string>>(execa.commandSync('unicorns', {encoding
expectType<ExecaSyncReturnValue<Buffer>>(execa.commandSync('unicorns', {encoding: null}));
expectType<ExecaSyncReturnValue<string>>(execa.commandSync('unicorns foo', {encoding: 'utf8'}));
expectType<ExecaSyncReturnValue<Buffer>>(execa.commandSync('unicorns foo', {encoding: null}));

expectType<ExecaChildProcess<string>>(execa.node('unicorns'));
expectType<ExecaReturnValue<string>>(await execa.node('unicorns'));
expectType<ExecaReturnValue<string>>(
await execa.node('unicorns', {encoding: 'utf8'})
);
expectType<ExecaReturnValue<Buffer>>(await execa.node('unicorns', {encoding: null}));
expectType<ExecaReturnValue<string>>(
await execa.node('unicorns', ['foo'], {encoding: 'utf8'})
);
expectType<ExecaReturnValue<Buffer>>(
await execa.node('unicorns', ['foo'], {encoding: null})
);
22 changes: 21 additions & 1 deletion lib/stdio.js
Expand Up @@ -3,7 +3,7 @@ const alias = ['stdin', 'stdout', 'stderr'];

const hasAlias = opts => alias.some(x => Boolean(opts[x]));

module.exports = opts => {
const stdio = opts => {
if (!opts) {
return;
}
Expand Down Expand Up @@ -39,3 +39,23 @@ module.exports = opts => {

return result;
};

module.exports = stdio;

module.exports.node = opts => {
const defaultOption = 'pipe';

let stdioOption = stdio(opts || {stdio: defaultOption});

if (typeof stdioOption === 'string') {
stdioOption = [...new Array(3)].fill(stdioOption);
} else if (Array.isArray(stdioOption)) {
stdioOption = stdioOption.map((channel = defaultOption) => channel);
}

if (!stdioOption.includes('ipc')) {
stdioOption.push('ipc');
}

return stdioOption;
};
22 changes: 22 additions & 0 deletions readme.md
Expand Up @@ -184,6 +184,15 @@ Same as [`execa.command()`](#execacommand-command-options) but synchronous.

Returns or throws a [`childProcessResult`](#childProcessResult).

### execa.node(scriptPath, [arguments], [options])

Execute a Node.js script as a child process.

Same as `execa('node', [file, ...arguments], options)` except (like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options)):
- the [`nodePath`](#nodepath-for-node-only) and [`nodeArguments`](#nodearguments-for-node-only) options can be used
- the [`shell`](#shell) option cannot be used
- an extra channel [`ipc`](https://nodejs.org/api/child_process.html#child_process_options_stdio) is passed to [`stdio`](#stdio)

### childProcessResult

Type: `object`
Expand Down Expand Up @@ -438,6 +447,19 @@ Default: `false`

If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`.

#### nodePath *(for `.node()` only)*

Type: `string`<br>
Default: [`process.execPath`](https://nodejs.org/api/process.html#process_process_execpath)

Node.js executable used to create the child process.

#### nodeArguments *(for `.node()` only)*

Type: `string[]`<br>
Default: [`process.execArgv`](https://nodejs.org/api/process.html#process_process_execargv)

List of string arguments passed to the Node.js executable.

## Tips

Expand Down
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions test/fixtures/detach
@@ -0,0 +1,8 @@
#!/usr/bin/env node
'use strict';

const execa = require('../..');

const subprocess = execa('node', ['./test/fixtures/forever'], {detached: true});
console.log(subprocess.pid);
process.exit();
3 changes: 3 additions & 0 deletions test/fixtures/echo
@@ -0,0 +1,3 @@
#!/usr/bin/env node
'use strict';
console.log(process.argv.slice(2).join('\n'));
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions test/fixtures/hello.sh
@@ -0,0 +1 @@
echo Hello World
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions test/fixtures/send
@@ -0,0 +1,9 @@
#!/usr/bin/env node
'use strict';
process.on('message', message => {
if (message === 'ping') {
process.send('pong');
} else {
throw new Error('Receive wrong message');
}
});
File renamed without changes.
4 changes: 2 additions & 2 deletions fixtures/sub-process → test/fixtures/sub-process
@@ -1,8 +1,8 @@
#!/usr/bin/env node
'use strict';
const execa = require('..');
const execa = require('../..');

const cleanup = process.argv[2] === 'true';
const detached = process.argv[3] === 'true';
const subprocess = execa('node', ['./fixtures/forever'], {cleanup, detached});
const subprocess = execa('node', ['./test/fixtures/forever'], {cleanup, detached});
process.send(subprocess.pid);
4 changes: 2 additions & 2 deletions fixtures/sub-process-exit → test/fixtures/sub-process-exit
@@ -1,7 +1,7 @@
#!/usr/bin/env node
'use strict';
const execa = require('..');
const execa = require('../..');

const cleanup = process.argv[2] === 'true';
const detached = process.argv[3] === 'true';
execa('node', ['./fixtures/noop'], {cleanup, detached});
execa('node', ['./test/fixtures/noop'], {cleanup, detached});
46 changes: 46 additions & 0 deletions test/node.js
@@ -0,0 +1,46 @@
import test from 'ava';
import pEvent from 'p-event';
import path from 'path';
import execa from '..';

process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH;

test('node()', async t => {
const {exitCode} = await execa.node('test/fixtures/noop');
t.is(exitCode, 0);
});

test('node pipe stdout', async t => {
const {stdout} = await execa.node('test/fixtures/noop', ['foo'], {
stdout: 'pipe'
});

t.is(stdout, 'foo');
});

test('node correctly use nodePath', async t => {
const {stdout} = await execa.node(process.platform === 'win32' ? 'hello.cmd' : 'hello.sh', {
stdout: 'pipe',
nodePath: process.platform === 'win32' ? 'cmd.exe' : 'bash',
nodeArguments: process.platform === 'win32' ? ['/c'] : []
});

t.is(stdout, 'Hello World');
});

test('node pass on nodeArguments', async t => {
const {stdout} = await execa.node('console.log("foo")', {
stdout: 'pipe',
nodeArguments: ['-e']
});

t.is(stdout, 'foo');
});

test('node\'s forked script has a communication channel', async t => {
const subprocess = execa.node('test/fixtures/send');
subprocess.send('ping');

const message = await pEvent(subprocess, 'message');
t.is(message, 'pong');
});
105 changes: 64 additions & 41 deletions test/stdio.js
Expand Up @@ -4,46 +4,69 @@ import stdio from '../lib/stdio';

util.inspect.styles.name = 'magenta';

function macro(t, input, expected) {
if (expected instanceof Error) {
t.throws(() => stdio(input), expected.message);
return;
}

const result = stdio(input);

if (typeof expected === 'object' && expected !== null) {
t.deepEqual(result, expected);
} else {
t.is(result, expected);
}
function createMacro(func) {
const macro = (t, input, expected) => {
if (expected instanceof Error) {
t.throws(() => {
stdio(input);
}, expected.message);
return;
}

const result = func(input);

if (typeof expected === 'object' && expected !== null) {
t.deepEqual(result, expected);
} else {
t.is(result, expected);
}
};

macro.title = (providedTitle, input) => `${func.name} ${(providedTitle || util.inspect(input, {colors: true}))}`;

return macro;
}

macro.title = (providedTitle, input) => providedTitle || util.inspect(input, {colors: true});

test(macro, undefined, undefined);

test(macro, {stdio: 'inherit'}, 'inherit');
test(macro, {stdio: 'pipe'}, 'pipe');
test(macro, {stdio: 'ignore'}, 'ignore');
test(macro, {stdio: [0, 1, 2]}, [0, 1, 2]);

test(macro, {}, [undefined, undefined, undefined]);
test(macro, {stdio: []}, [undefined, undefined, undefined]);
test(macro, {stdin: 'pipe'}, ['pipe', undefined, undefined]);
test(macro, {stdout: 'ignore'}, [undefined, 'ignore', undefined]);
test(macro, {stderr: 'inherit'}, [undefined, undefined, 'inherit']);
test(macro, {stdin: 'pipe', stdout: 'ignore', stderr: 'inherit'}, ['pipe', 'ignore', 'inherit']);
test(macro, {stdin: 'pipe', stdout: 'ignore'}, ['pipe', 'ignore', undefined]);
test(macro, {stdin: 'pipe', stderr: 'inherit'}, ['pipe', undefined, 'inherit']);
test(macro, {stdout: 'ignore', stderr: 'inherit'}, [undefined, 'ignore', 'inherit']);
test(macro, {stdin: 0, stdout: 1, stderr: 2}, [0, 1, 2]);
test(macro, {stdin: 0, stdout: 1}, [0, 1, undefined]);
test(macro, {stdin: 0, stderr: 2}, [0, undefined, 2]);
test(macro, {stdout: 1, stderr: 2}, [undefined, 1, 2]);

test(macro, {stdio: {foo: 'bar'}}, new TypeError('Expected `stdio` to be of type `string` or `Array`, got `object`'));

test(macro, {stdin: 'inherit', stdio: 'pipe'}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
test(macro, {stdin: 'inherit', stdio: ['pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
test(macro, {stdin: 'inherit', stdio: [undefined, 'pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
const stdioMacro = createMacro(stdio);

test(stdioMacro, undefined, undefined);
test(stdioMacro, null, undefined);

test(stdioMacro, {stdio: 'inherit'}, 'inherit');
test(stdioMacro, {stdio: 'pipe'}, 'pipe');
test(stdioMacro, {stdio: 'ignore'}, 'ignore');
test(stdioMacro, {stdio: [0, 1, 2]}, [0, 1, 2]);

test(stdioMacro, {}, [undefined, undefined, undefined]);
test(stdioMacro, {stdio: []}, [undefined, undefined, undefined]);
test(stdioMacro, {stdin: 'pipe'}, ['pipe', undefined, undefined]);
test(stdioMacro, {stdout: 'ignore'}, [undefined, 'ignore', undefined]);
test(stdioMacro, {stderr: 'inherit'}, [undefined, undefined, 'inherit']);
test(stdioMacro, {stdin: 'pipe', stdout: 'ignore', stderr: 'inherit'}, ['pipe', 'ignore', 'inherit']);
test(stdioMacro, {stdin: 'pipe', stdout: 'ignore'}, ['pipe', 'ignore', undefined]);
test(stdioMacro, {stdin: 'pipe', stderr: 'inherit'}, ['pipe', undefined, 'inherit']);
test(stdioMacro, {stdout: 'ignore', stderr: 'inherit'}, [undefined, 'ignore', 'inherit']);
test(stdioMacro, {stdin: 0, stdout: 1, stderr: 2}, [0, 1, 2]);
test(stdioMacro, {stdin: 0, stdout: 1}, [0, 1, undefined]);
test(stdioMacro, {stdin: 0, stderr: 2}, [0, undefined, 2]);
test(stdioMacro, {stdout: 1, stderr: 2}, [undefined, 1, 2]);

test(stdioMacro, {stdio: {foo: 'bar'}}, new TypeError('Expected `stdio` to be of type `string` or `Array`, got `object`'));

test(stdioMacro, {stdin: 'inherit', stdio: 'pipe'}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
test(stdioMacro, {stdin: 'inherit', stdio: ['pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
test(stdioMacro, {stdin: 'inherit', stdio: [undefined, 'pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));

const forkMacro = createMacro(stdio.node);

test(forkMacro, undefined, ['pipe', 'pipe', 'pipe', 'ipc']);
test(forkMacro, {stdio: 'ignore'}, ['ignore', 'ignore', 'ignore', 'ipc']);
test(forkMacro, {stdio: [0, 1, 2]}, [0, 1, 2, 'ipc']);
test(forkMacro, {stdio: [0, 1, 2, 3]}, [0, 1, 2, 3, 'ipc']);
test(forkMacro, {stdio: [0, 1, 2, 'ipc']}, [0, 1, 2, 'ipc']);

test(forkMacro, {stdout: 'ignore'}, ['pipe', 'ignore', 'pipe', 'ipc']);
test(forkMacro, {stdout: 'ignore', stderr: 'ignore'}, ['pipe', 'ignore', 'ignore', 'ipc']);

test(forkMacro, {stdio: {foo: 'bar'}}, new TypeError('Expected `stdio` to be of type `string` or `Array`, got `object`'));
test(forkMacro, {stdin: 'inherit', stdio: 'pipe'}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));

0 comments on commit c2f5edf

Please sign in to comment.