Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .node() method #200

Merged
merged 47 commits into from Jun 18, 2019
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ec492d8
Add fork method
Apr 3, 2019
f844651
Revert changes of stdio
GMartigny Apr 3, 2019
1a15ee7
Cover execArgv and compute stdio
GMartigny Apr 3, 2019
5f49784
Add tests for fork's stdio option
Apr 4, 2019
360f07c
Fix stdio fork tests
Apr 4, 2019
dbc4274
Fix stdio fork function
Apr 4, 2019
b03baf6
Add test for fork
Apr 4, 2019
0a116f8
Avoid mutating arguments to fork
Apr 5, 2019
2e83f03
Use execPath and execArgv
Apr 5, 2019
35e8167
Add more tests to the fork function
Apr 5, 2019
eb2f47a
Remove duplicate fixture file
GMartigny Apr 8, 2019
be63122
normalize
GMartigny Apr 8, 2019
483aa5c
Unshift unique args to windows OS cmd
GMartigny May 7, 2019
69e5571
Merge branch 'master' into addForkMethod
GMartigny May 7, 2019
e5783d0
Update readme, types and types tests
GMartigny May 10, 2019
5e6631a
Merge branch 'master' into addForkMethod
GMartigny May 10, 2019
cf79301
Remove unnecessary normalize
GMartigny May 10, 2019
2c2d727
Revert "Unshift unique args to windows OS cmd"
GMartigny May 10, 2019
f965897
Merge branch 'master' into addForkMethod
GMartigny May 21, 2019
0ed7fd6
Fix tests on windows
GMartigny May 21, 2019
7642806
Reword on readme
GMartigny May 21, 2019
e82b43a
Minor fixes
GMartigny May 21, 2019
054b49b
Fix tests on windows
GMartigny May 21, 2019
ac12a9f
Sync readme and type definition
GMartigny May 21, 2019
fe687c2
Readme fine tune
GMartigny May 22, 2019
497b23f
Allow for user to omit args on fork
GMartigny May 22, 2019
737e118
Add missing semicolon
GMartigny May 22, 2019
0f90a22
revert change to logo.sketch
GMartigny Jun 6, 2019
0fa3941
Move all tests to `test` directory
GMartigny Jun 6, 2019
b02d6f4
Rename `fork` to `node` and remove `silent` option
GMartigny Jun 6, 2019
c931818
Merge master
GMartigny Jun 6, 2019
5680794
Fix `.node()` tests
GMartigny Jun 6, 2019
d93cb69
Minor tweak to syntax
GMartigny Jun 6, 2019
f302e78
Remove linter warning
GMartigny Jun 6, 2019
7746b01
Add execution right to fixtures files
GMartigny Jun 6, 2019
9a65cc5
Fix path in fixtures
GMartigny Jun 6, 2019
f4b77c7
Capitalize `node.js`
GMartigny Jun 7, 2019
0fac604
Add shebang and `'use strict'`
GMartigny Jun 7, 2019
b37f98b
Move remaining fixtures files under `test`
GMartigny Jun 7, 2019
ed7e580
Fix file execution rights thanks to windows
GMartigny Jun 7, 2019
906045e
Rename `.node` specific params
GMartigny Jun 13, 2019
6e181cc
Link `.node` params to their definition
GMartigny Jun 13, 2019
87b861f
Change `.node` description
GMartigny Jun 13, 2019
6394e4d
Minor tweak in syntax
GMartigny Jun 13, 2019
656559c
Merge master
GMartigny Jun 13, 2019
00199ed
Merge master
GMartigny Jun 18, 2019
5c15cf2
Doc say `script` instead of `file` for `.node()`
GMartigny Jun 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -185,14 +185,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 CommonOptions<EncodingType> {
/**
Executable used to create the child process.
GMartigny marked this conversation as resolved.
Show resolved Hide resolved

@default process.execPath
*/
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
readonly execPath?: string;
GMartigny marked this conversation as resolved.
Show resolved Hide resolved

/**
List of string arguments passed to the executable.
GMartigny marked this conversation as resolved.
Show resolved Hide resolved

@default process.execArgv
*/
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
readonly execArgv?: string[];
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
}

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

/**
Run a file through a forked process.

@param file - The program/script to execute.
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
@param arguments - Arguments to pass to `file` 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(
file: string,
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
arguments?: readonly string[],
options?: execa.Options
): 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 @@ -475,3 +475,30 @@ module.exports.commandSync = (command, options) => {
const [file, ...args] = parseCommand(command);
return execa.sync(file, args, options);
};

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

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

return execa(
options.execPath || process.execPath,
[
...(options.execArgv || process.execArgv),
filePath,
...(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 @@ -167,3 +167,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});
ehmicky marked this conversation as resolved.
Show resolved Hide resolved

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')) {
ehmicky marked this conversation as resolved.
Show resolved Hide resolved
stdioOption.push('ipc');
}

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

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

### execa.node(file, [arguments], [options])
ehmicky marked this conversation as resolved.
Show resolved Hide resolved

Run a file through a forked process.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct. It's not just a file, it's a script, and it's not a forked process, it's "spawning Node.js". Forking a process is a completely different thing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to #200 (comment)


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 `execPath` and `execArgv` options can be used
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
- 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 @@ -432,6 +441,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`.

#### execPath *(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.

#### execArgv *(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('../../');
GMartigny marked this conversation as resolved.
Show resolved Hide resolved

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');
ehmicky marked this conversation as resolved.
Show resolved Hide resolved
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 execPath', async t => {
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
const {stdout} = await execa.node(process.platform === 'win32' ? 'hello.cmd' : 'hello.sh', {
stdout: 'pipe',
execPath: process.platform === 'win32' ? 'cmd.exe' : 'bash',
execArgv: process.platform === 'win32' ? ['/c'] : []
});

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

test('node pass on execArgv', async t => {
const {stdout} = await execa.node('console.log("foo")', {
stdout: 'pipe',
execArgv: ['-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']);
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
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']);
GMartigny marked this conversation as resolved.
Show resolved Hide resolved
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`'));