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 option #804

Merged
merged 1 commit into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 33 additions & 29 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,39 @@ type CommonOptions<IsSync extends boolean = boolean> = {
*/
readonly localDir?: string | URL;

/**
If `true`, runs with Node.js. The first argument must be a Node.js file.
@default `true` with `execaNode()`, `false` otherwise
*/
readonly node?: boolean;

/**
Node.js executable used to create the child process.
Requires the `node` option to be `true`.
@default [`process.execPath`](https://nodejs.org/api/process.html#process_process_execpath) (current Node.js executable)
*/
readonly nodePath?: string | URL;

/**
List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable.
Requires the `node` option to be `true`.
@default [`process.execArgv`](https://nodejs.org/api/process.html#process_process_execargv) (current Node.js CLI options)
*/
readonly nodeOptions?: string[];

/**
Path to the Node.js executable to use in child processes.
Requires `preferLocal` to be `true`.
Requires the `preferLocal` option to be `true`.
For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process.
@default process.execPath
@default [`process.execPath`](https://nodejs.org/api/process.html#process_process_execpath) (current Node.js executable)
*/
readonly execPath?: string | URL;

Expand Down Expand Up @@ -556,6 +581,8 @@ type CommonOptions<IsSync extends boolean = boolean> = {

/**
Enables exchanging messages with the child process using [`childProcess.send(value)`](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback) and [`childProcess.on('message', (value) => {})`](https://nodejs.org/api/child_process.html#event-message).
@default `true` if the `node` option is enabled, `false` otherwise
*/
readonly ipc?: IfAsync<IsSync, boolean>;

Expand Down Expand Up @@ -607,22 +634,6 @@ type CommonOptions<IsSync extends boolean = boolean> = {
export type Options = CommonOptions<false>;
export type SyncOptions = CommonOptions<true>;

export type NodeOptions<OptionsType extends Options = Options> = {
/**
The Node.js executable to use.
@default process.execPath
*/
readonly nodePath?: string | URL;

/**
List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable.
@default process.execArgv
*/
readonly nodeOptions?: string[];
} & OptionsType;

/**
Result of a child process execution. On success this is a plain object. On failure this is also an `Error` instance.
Expand Down Expand Up @@ -1262,16 +1273,9 @@ await $$`echo rainbows`;
export const $: Execa$;

/**
Executes a Node.js file using `node scriptPath ...arguments`. `file` is a string or a file URL. `arguments` are an array of strings. Returns a `childProcess`.
Arguments are automatically escaped. They can contain any character, including spaces.
This is the preferred method when executing Node.js files.
Same as `execa()` but using the `node` option.
Like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options):
- the current Node version and options are used. This can be overridden using the `nodePath` and `nodeOptions` options.
- the `shell` option cannot be used
- the `ipc` option defaults to `true`
Executes a Node.js file using `node scriptPath ...arguments`.
@param scriptPath - Node.js script to execute, as a string or file URL
@param arguments - Arguments to pass to `scriptPath` on execution.
Expand All @@ -1287,12 +1291,12 @@ import {execa} from 'execa';
await execaNode('scriptPath', ['argument']);
```
*/
export function execaNode<OptionsType extends NodeOptions = {}>(
export function execaNode<OptionsType extends Options = {}>(
scriptPath: string | URL,
arguments?: readonly string[],
options?: OptionsType
): ExecaChildProcess<OptionsType>;
export function execaNode<OptionsType extends NodeOptions = {}>(
export function execaNode<OptionsType extends Options = {}>(
scriptPath: string | URL,
options?: OptionsType
): ExecaChildProcess<OptionsType>;
28 changes: 14 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import crossSpawn from 'cross-spawn';
import stripFinalNewline from 'strip-final-newline';
import {npmRunPathEnv} from 'npm-run-path';
import {makeError} from './lib/error.js';
import {handleNodeOption} from './lib/node.js';
import {handleInputAsync, pipeOutputAsync, cleanupStdioStreams} from './lib/stdio/async.js';
import {handleInputSync, pipeOutputSync} from './lib/stdio/sync.js';
import {spawnedKill, validateTimeout, normalizeForceKillAfterDelay, cleanupOnExit} from './lib/kill.js';
Expand Down Expand Up @@ -39,7 +40,9 @@ const handleArguments = (rawFile, rawArgs, rawOptions = {}) => {
const command = joinCommand(filePath, rawArgs);
const escapedCommand = getEscapedCommand(filePath, rawArgs);

const {command: file, args, options: initialOptions} = crossSpawn._parse(filePath, rawArgs, rawOptions);
const [processedFile, processedArgs, processedOptions] = handleNodeOption(filePath, rawArgs, rawOptions);

const {command: file, args, options: initialOptions} = crossSpawn._parse(processedFile, processedArgs, processedOptions);

const options = addDefaultOptions(initialOptions);
validateTimeout(options);
Expand Down Expand Up @@ -215,7 +218,8 @@ const handlePromise = async ({spawned, options, stdioStreamsGroups, originalStre
};

export function execaSync(rawFile, rawArgs, rawOptions) {
const {file, args, command, escapedCommand, options} = handleArguments(rawFile, rawArgs, rawOptions);
const syncOptions = normalizeSyncOptions(rawOptions);
const {file, args, command, escapedCommand, options} = handleArguments(rawFile, rawArgs, syncOptions);
validateSyncOptions(options);

const stdioStreamsGroups = handleInputSync(options);
Expand Down Expand Up @@ -281,6 +285,8 @@ export function execaSync(rawFile, rawArgs, rawOptions) {
};
}

const normalizeSyncOptions = (options = {}) => options.node && !options.ipc ? {...options, ipc: false} : options;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

node: true can be used with synchronous methods, but ipc: true cannot.
Therefore, ipc: true is not added when using node: true with synchronous methods.


const validateSyncOptions = ({ipc}) => {
if (ipc) {
throw new TypeError('The "ipc: true" option cannot be used with synchronous methods.');
Expand Down Expand Up @@ -333,21 +339,15 @@ export function execaCommandSync(command, options) {
return execaSync(file, args, options);
}

export function execaNode(scriptPath, args = [], options = {}) {
export function execaNode(file, args = [], options = {}) {
if (!Array.isArray(args)) {
options = args;
args = [];
}

const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect'));
const {
nodePath = process.execPath,
nodeOptions = defaultExecArgv,
} = options;

return execa(
nodePath,
[...nodeOptions, getFilePath(scriptPath), ...args],
{ipc: true, ...options, shell: false},
);
if (options.node === false) {
throw new TypeError('The "node" option cannot be false with `execaNode()`.');
}

return execa(file, args, {...options, node: true});
}
8 changes: 8 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,14 @@ execa('unicorns', {shell: '/bin/sh'});
execaSync('unicorns', {shell: '/bin/sh'});
execa('unicorns', {shell: fileUrl});
execaSync('unicorns', {shell: fileUrl});
execa('unicorns', {node: true});
execaSync('unicorns', {node: true});
execa('unicorns', {nodePath: './node'});
execaSync('unicorns', {nodePath: './node'});
execa('unicorns', {nodePath: fileUrl});
execaSync('unicorns', {nodePath: fileUrl});
execa('unicorns', {nodeOptions: ['--async-stack-traces']});
execaSync('unicorns', {nodeOptions: ['--async-stack-traces']});
execa('unicorns', {timeout: 1000});
execaSync('unicorns', {timeout: 1000});
execa('unicorns', {maxBuffer: 1000});
Expand Down
24 changes: 24 additions & 0 deletions lib/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {execPath, execArgv} from 'node:process';
import {basename} from 'node:path';
import {safeNormalizeFileUrl} from './cwd.js';

export const handleNodeOption = (file, args, {
node: shouldHandleNode = false,
nodePath = execPath,
nodeOptions = execArgv.filter(arg => !arg.startsWith('--inspect')),
...options
}) => {
if (!shouldHandleNode) {
return [file, args, options];
}
if (basename(file, '.exe') === 'node') {
throw new TypeError('When the "node" option is true, the first argument does not need to be "node".');
}
return [
safeNormalizeFileUrl(nodePath, 'The "nodePath" option'),
[...nodeOptions, file, ...args],
{ipc: true, ...options, shell: false},
];
};
56 changes: 30 additions & 26 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,19 +233,6 @@ Arguments are [automatically escaped](#shell-syntax). They can contain any chara

This is the preferred method when executing single commands.

#### execaNode(scriptPath, arguments?, options?)

Executes a Node.js file using `node scriptPath ...arguments`. `file` is a string or a file URL. `arguments` are an array of strings. Returns a [`childProcess`](#childprocess).

Arguments are [automatically escaped](#shell-syntax). They can contain any character, including spaces.

This is the preferred method when executing Node.js files.

Like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options):
- the current Node version and options are used. This can be overridden using the [`nodePath`](#nodepath-for-node-only) and [`nodeOptions`](#nodeoptions-for-node-only) options.
- the [`shell`](#shell) option cannot be used
- the [`ipc`](#ipc) option defaults to `true`

#### $\`command\`

Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a [`childProcess`](#childprocess).
Expand Down Expand Up @@ -274,6 +261,12 @@ Arguments are [automatically escaped](#shell-syntax). They can contain any chara

This is the preferred method when executing a user-supplied `command` string, such as in a REPL.

#### execaNode(scriptPath, arguments?, options?)

Same as [`execa()`](#execacommandcommand-options) but using the [`node`](#node) option.

Executes a Node.js file using `node scriptPath ...arguments`.

#### execaSync(file, arguments?, options?)

Same as [`execa()`](#execacommandcommand-options) but synchronous.
Expand Down Expand Up @@ -544,30 +537,41 @@ Default: `process.cwd()`

Preferred path to find locally installed binaries in (use with `preferLocal`).

#### execPath
#### node

Type: `string | URL`\
Default: `process.execPath` (Current Node.js executable)
Type: `boolean`\
Default: `true` with [`execaNode()`](#execanodescriptpath-arguments-options), `false` otherwise

Path to the Node.js executable to use in child processes.
If `true`, runs with Node.js. The first argument must be a Node.js file.

Requires [`preferLocal`](#preferlocal) to be `true`.
#### nodeOptions

For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process.
Type: `string[]`\
Default: [`process.execArgv`](https://nodejs.org/api/process.html#process_process_execargv) (current Node.js CLI options)

List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the [Node.js executable](#nodepath).

#### nodePath *(For `.node()` only)*
Requires the [`node`](#node) option to be `true`.

#### nodePath

Type: `string | URL`\
Default: [`process.execPath`](https://nodejs.org/api/process.html#process_process_execpath)
Default: [`process.execPath`](https://nodejs.org/api/process.html#process_process_execpath) (current Node.js executable)

Node.js executable used to create the child process.

#### nodeOptions *(For `.node()` only)*
Requires the [`node`](#node) option to be `true`.

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

List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable.
Type: `string | URL`\
Default: [`process.execPath`](https://nodejs.org/api/process.html#process_process_execpath) (current Node.js executable)

Path to the Node.js executable to use in child processes.

Requires the [`preferLocal`](#preferlocal) option to be `true`.

For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process.

#### verbose

Expand Down Expand Up @@ -719,7 +723,7 @@ Largest amount of data in bytes allowed on [`stdout`](#stdout), [`stderr`](#stde
#### ipc

Type: `boolean`\
Default: `true` with [`execaNode()`](#execanodescriptpath-arguments-options), `false` otherwise
Default: `true` if the [`node`](#node) option is enabled, `false` otherwise

Enables exchanging messages with the child process using [`childProcess.send(value)`](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback) and [`childProcess.on('message', (value) => {})`](https://nodejs.org/api/child_process.html#event-message).

Expand Down
9 changes: 6 additions & 3 deletions test/fixtures/nested-node.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#!/usr/bin/env node
import process from 'node:process';
import {writeSync} from 'node:fs';
import {execaNode} from '../../index.js';
import {execa, execaNode} from '../../index.js';

const [fakeExecArgv, nodeOptions, file, ...args] = process.argv.slice(2);
const [fakeExecArgv, execaMethod, nodeOptions, file, ...args] = process.argv.slice(2);

if (fakeExecArgv !== '') {
process.execArgv = [fakeExecArgv];
}

const {stdout, stderr} = await execaNode(file, args, {nodeOptions: [nodeOptions].filter(Boolean)});
const filteredNodeOptions = [nodeOptions].filter(Boolean);
const {stdout, stderr} = await (execaMethod === 'execaNode'
? execaNode(file, args, {nodeOptions: filteredNodeOptions})
: execa(file, args, {nodeOptions: filteredNodeOptions, node: true}));
console.log(stdout);
writeSync(3, stderr);