Skip to content

Commit

Permalink
Add node option
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Feb 8, 2024
1 parent 2fd9351 commit 20f3fe3
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 120 deletions.
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;

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);

0 comments on commit 20f3fe3

Please sign in to comment.