diff --git a/readme.md b/readme.md index 329d665..0e7fa46 100644 --- a/readme.md +++ b/readme.md @@ -2,24 +2,31 @@ nano-spawn logo -[![Install size](https://packagephobia.com/badge?p=nano-spawn)](https://packagephobia.com/result?p=nano-spawn) -![npm package minzipped size](https://img.shields.io/bundlejs/size/nano-spawn) ![Test coverage](https://img.shields.io/badge/coverage-100%25-green) > Tiny process execution for humans — a better [`child_process`](https://nodejs.org/api/child_process.html) -> [!WARNING] -> This package is still a work in progress. +## Features -Check out [`execa`](https://github.com/sindresorhus/execa) for more features. +No dependencies. Small package size: ![npm package minzipped size](https://img.shields.io/bundlejs/size/nano-spawn) [![Install size](https://packagephobia.com/badge?p=nano-spawn)](https://packagephobia.com/result?p=nano-spawn) -## Features +Despite the small size, this is packed with some essential features: +- [Promise-based](#spawnfile-arguments-options-default-export) interface. +- [Iterate](#subprocesssymbolasynciterator) over the output lines. +- [Pipe](#subprocesspipefile-arguments-options) multiple subprocesses and retrieve [intermediate results](#resultpipedfrom). +- Execute [locally installed binaries](#optionspreferlocal) without `npx`. +- Improved [Windows support](#windows-support). +- Proper handling of [subprocess failures](#subprocesserror) and better error messages. +- Get [interleaved output](#resultoutput) from stdout and stderr similar to what is printed on the terminal. +- Strip [unnecessary newlines](#resultstdout). +- Pass strings as [`stdin` input](#optionsstdin-optionsstdout-optionsstderr) to the subprocess. +- Preserve the current [Node.js version and flags](#spawnfile-arguments-options-default-export). +- Simpler syntax to set [environment variables](#optionsenv) or [`stdin`/`stdout`/`stderr`](#optionsstdin-optionsstdout-optionsstderr). +- Compute the command [duration](#resultdurationms). -- Outputs combined result of stdout and stderr, similar to what you get in terminals -- Outputs lines -- No dependencies +For additional features, please check out [Execa](#execa). ## Install @@ -35,21 +42,21 @@ npm install nano-spawn ## Usage +### Run commands + ```js -import $ from 'nano-spawn'; +import spawn from 'nano-spawn'; -const result = await $('echo', ['🦄']); +const result = await spawn('echo', ['🦄']); -console.log(result.exitCode); -//=> 0 +console.log(result.output); +//=> '🦄' ``` -**Advanced** +### Iterate over output lines ```js -import $ from 'nano-spawn'; - -for await (const line of $('ls', ['--oneline'])) { +for await (const line of spawn('ls', ['--oneline'])) { console.log(line); } //=> index.d.ts @@ -57,20 +64,231 @@ for await (const line of $('ls', ['--oneline'])) { //=> … ``` +### Pipe commands + +```js +const result = await spawn('npm', ['run', 'build']) + .pipe('sort') + .pipe('head', ['-n', '2']); +``` + ## API -See the [types](source/index.d.ts) for now. +### spawn(file, arguments?, options?) default export + +`file`: `string`\ +`arguments`: `string[]`\ +`options`: [`Options`](#options)\ +_Returns_: [`Subprocess`](#subprocess) + +Executes a command using `file ...arguments`. + +This has the same syntax as [`child_process.spawn()`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options). + +If `file` is `'node'`, the current Node.js version and [flags](https://nodejs.org/api/cli.html#options) are inherited. + +#### Options + +##### options.stdio, options.shell, options.timeout, options.signal, options.cwd, options.killSignal, options.serialization, options.detached, options.uid, options.gid, options.windowsVerbatimArguments, options.windowsHide, options.argv0 + +All [`child_process.spawn()` options](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) can be passed to [`spawn()`](#spawnfile-arguments-options-default-export). + +##### options.env + +_Type_: `object`\ +_Default_: `{}` + +Override specific [environment variables](https://en.wikipedia.org/wiki/Environment_variable). Other environment variables are inherited from the current process ([`process.env`](https://nodejs.org/api/process.html#processenv)). + +##### options.preferLocal + +_Type_: `boolean`\ +_Default_: `false` + +Allows executing binaries installed locally with `npm` (or `yarn`, etc.). + +##### options.stdin, options.stdout, options.stderr + +_Type_: `string | number | Stream | {string: string}` + +Subprocess's standard [input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin))/[output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout))/[error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). + +[All values supported](https://nodejs.org/api/child_process.html#optionsstdio) by `node:child_process` are available. The most common ones are: +- `'pipe'` (default value): returns the output using [`result.stdout`](#resultstdout), [`result.stderr`](#resultstderr) and [`result.output`](#resultoutput). +- `'inherit'`: uses the current process's [input](https://nodejs.org/api/process.html#processstdin)/[output](https://nodejs.org/api/process.html#processstdout). This is useful when running in a terminal. +- `'ignore'`: discards the input/output. +- [`Stream`](https://nodejs.org/api/stream.html#stream): redirects the input/output from/to a stream. For example, [`fs.createReadStream()`](https://nodejs.org/api/fs.html#fscreatereadstreampath-options)/[`fs.createWriteStream()`](https://nodejs.org/api/fs.html#fscreatewritestreampath-options) can be used, once the stream's [`open`](https://nodejs.org/api/fs.html#event-open) event has been emitted. +- `{string: '...'}`: passes a string as input to `stdin`. + +#### Subprocess + +Subprocess started by [`spawn()`](#spawnfile-arguments-options-default-export). + +##### await subprocess + +_Returns_: [`Result`](#result)\ +_Throws_: [`SubprocessError`](#subprocesserror) + +A subprocess is a promise that is either resolved with a successful [`result` object](#result) or rejected with a [`subprocessError`](#error). + +##### subprocess.stdout + +_Returns_: `AsyncIterable`\ +_Throws_: [`SubprocessError`](#subprocesserror) + +Iterates over each [`stdout`](#resultstdout) line, as soon as it is available. + +The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess [fails](#subprocesserror). This means you do not need to call [`await subprocess`](#await-subprocess). + +##### subprocess.stderr + +_Returns_: `AsyncIterable`\ +_Throws_: [`SubprocessError`](#subprocesserror) + +Same as [`subprocess.stdout`](#subprocessstdout) but for [`stderr`](#resultstderr) instead. + +##### subprocess[Symbol.asyncIterator]\() + +_Returns_: `AsyncIterable`\ +_Throws_: [`SubprocessError`](#subprocesserror) + +Same as [`subprocess.stdout`](#subprocessstdout) but for both [`stdout` and `stderr`](#resultoutput). + +##### subprocess.pipe(file, arguments?, options?) + +`file`: `string`\ +`arguments`: `string[]`\ +`options`: [`Options`](#options)\ +_Returns_: [`Subprocess`](#subprocess) + +Similar to the `|` symbol in shells. [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the subprocess's[`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) to a second subprocess's [`stdin`](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). + +This resolves with that second subprocess's [result](#result). If either subprocess is rejected, this is rejected with that subprocess's [error](#subprocesserror) instead. + +This follows the same syntax as [`spawn(file, arguments?, options?)`](#spawnfile-arguments-options-default-export). It can be done multiple times in a row. + +##### await subprocess.nodeChildProcess + +_Type_: `ChildProcess` + +Underlying [Node.js child process](https://nodejs.org/api/child_process.html#class-childprocess). + +Among other things, this can be used to terminate the subprocess using [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) or exchange IPC message using [`.send()`](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback). + +#### Result + +When the subprocess succeeds, its [promise](#await-subprocess) is resolved with an object with the following properties. -## Limitations +##### result.stdout -- It does not handle binary output. Use [`execa`](https://github.com/sindresorhus/execa) for that. +_Type_: `string` + +The output of the subprocess on [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). + +If the output ends with a [newline](https://en.wikipedia.org/wiki/Newline), that newline is automatically stripped. + +This is an empty string if either: +- The [`stdout`](#optionsstdin-optionsstdout-optionsstderr) option is set to another value than `'pipe'` (its default value). +- The output is being iterated using [`subprocess.stdout`](#subprocessstdout) or [`subprocess[Symbol.asyncIterator]`](#subprocesssymbolasynciterator). + +##### result.stderr + +_Type_: `string` + +Like [`result.stdout`](#resultstdout) but for the [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)) instead. + +##### result.output + +_Type_: `string` + +Like [`result.stdout`](#resultstdout) but for both the [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) and [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)), interleaved. + +##### result.command + +_Type_: `string` + +The file and arguments that were run. + +It is intended for logging or debugging. Since the escaping is fairly basic, it should not be executed directly. + +##### result.durationMs + +_Type_: `number` + +Duration of the subprocess, in milliseconds. + +##### result.pipedFrom + +_Type_: `Result | SubprocessError | undefined` + +If [`subprocess.pipe()`](#subprocesspipefile-arguments-options) was used, the [result](#result) or [error](#subprocesserror) of the other subprocess that was piped into this subprocess. + +#### SubprocessError + +_Type_: `Error` + +When the subprocess fails, its [promise](#await-subprocess) is rejected with this error. + +Subprocesses fail either when their [exit code](#subprocesserrorexitcode) is not `0` or when terminated by a [signal](#subprocesserrorsignalname). Other failure reasons include misspelling the command name or using the [`timeout`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) option. + +Subprocess errors have the same shape as [successful results](#result), with the following additional properties. + +##### subprocessError.exitCode + +_Type_: `number | undefined` + +The numeric [exit code](https://en.wikipedia.org/wiki/Exit_status) of the subprocess that was run. + +This is `undefined` when the subprocess could not be started, or when it was terminated by a [signal](#subprocesserrorsignalname). + +##### subprocessError.signalName + +_Type_: `string | undefined` + +The name of the [signal](https://en.wikipedia.org/wiki/Signal_(IPC)) (like [`SIGTERM`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM)) that terminated the subprocess, sent by either: +- The current process. +- Another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events). + +If a signal terminated the subprocess, this property is defined and included in the [error message](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message). Otherwise it is `undefined`. + +## Windows support + +This package fixes several cross-platform issues with [`node:child_process`](https://nodejs.org/api/child_process.html). It brings full Windows support for: +- Node modules binaries (without requiring the [`shell`](https://nodejs.org/api/child_process.html#default-windows-shell) option). This includes running `npm ...` or `yarn ...`. +- `.cmd`, `.bat`, and other shell files. +- The [`PATHEXT`](https://wiki.tcl-lang.org/page/PATHEXT) environment variable. +- Windows-specific [newlines](https://en.wikipedia.org/wiki/Newline#Representation). + +## Alternatives + +`nano-spawn`'s main goal is to be small, yet useful. Nonetheless, depending on your use case, there are other ways to run subprocesses in Node.js. + +### Execa + +[Execa](https://github.com/sindresorhus/execa) is a similar package: it provides the same features, but more. It is also built on top of `node:child_process`, and is maintained by the [same people](#maintainers). + +On one hand, it has a bigger size: [![Install size](https://packagephobia.com/badge?p=execa)](https://packagephobia.com/result?p=execa) + +On the other hand, it provides a bunch of additional features: [scripts](https://github.com/sindresorhus/execa/blob/main/docs/scripts.md), [template string syntax](https://github.com/sindresorhus/execa/blob/main/docs/execution.md#template-string-syntax), [synchronous execution](https://github.com/sindresorhus/execa/blob/main/docs/execution.md#synchronous-execution), [file input/output](https://github.com/sindresorhus/execa/blob/main/docs/output.md#file-output), [binary input/output](https://github.com/sindresorhus/execa/blob/main/docs/binary.md), [advanced piping](https://github.com/sindresorhus/execa/blob/main/docs/pipe.md), [verbose mode](https://github.com/sindresorhus/execa/blob/main/docs/debugging.md#verbose-mode), [graceful](https://github.com/sindresorhus/execa/blob/main/docs/termination.md#graceful-termination) or [forceful termination](https://github.com/sindresorhus/execa/blob/main/docs/termination.md#forceful-termination), [IPC](https://github.com/sindresorhus/execa/blob/main/docs/ipc.md), [shebangs on Windows](https://github.com/sindresorhus/execa/blob/main/docs/windows.md), [and much more](https://github.com/sindresorhus/nano-spawn/issues/14). Also, it is [very widely used](https://github.com/sindresorhus/execa/network/dependents) and [battle-tested](https://github.com/sindresorhus/execa/graphs/contributors). + +We recommend using Execa in most cases, unless your environment requires using small packages (for example, in a library or in a serverless function). It is definitely the best option inside scripts, servers, or apps. + +### `node:child_process` + +`nano-spawn` is built on top of the [`node:child_process`](https://nodejs.org/api/child_process.html) core module. + +If you'd prefer avoiding adding any dependency, you may use `node:child_process` directly. However, you might miss the [features](#features) `nano-spawn` provides: [proper error handling](#subprocesserror), [full Windows support](#windows-support), [local binaries](#optionspreferlocal), [piping](#subprocesspipefile-arguments-options), [lines iteration](#subprocesssymbolasynciterator), [interleaved output](#resultoutput), [and more](#features). + +```js +import {execFile} from 'node:child_process'; +import {promisify} from 'node:util'; + +const pExecFile = promisify(execFile); + +const result = await pExecFile('npm', ['run', 'build']); +``` ## Maintainers - [Sindre Sorhus](https://github.com/sindresorhus) - [@ehmicky](https://github.com/ehmicky) - -## Related - -- [execa](https://github.com/sindresorhus/execa) - Process execution for humans -- [unicorn-magic](https://github.com/sindresorhus/unicorn-magic/blob/6614e1e82a19f41d7cc8f04df7c90a4dfe781741/node.d.ts#L77-L125) - Slightly improved `child_process#execFile` diff --git a/source/index.d.ts b/source/index.d.ts index 15cb4c8..e2b5ae3 100644 --- a/source/index.d.ts +++ b/source/index.d.ts @@ -3,52 +3,241 @@ import type {ChildProcess, SpawnOptions} from 'node:child_process'; type StdioOption = Readonly[number]>; type StdinOption = StdioOption | {readonly string?: string}; +/** +Options passed to `nano-spawn`. + +All [`child_process.spawn()` options](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) can be passed. +*/ export type Options = Omit & Readonly>>; }>>; +/** +When the subprocess succeeds, its promise is resolved with this object. +*/ export type Result = { + /** + The output of the subprocess on [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)). + + If the output ends with a [newline](https://en.wikipedia.org/wiki/Newline), that newline is automatically stripped. + + This is an empty string if either: + - The `stdout` option is set to another value than `'pipe'` (its default value). + - The output is being iterated using `subprocess.stdout` or `subprocess[Symbol.asyncIterator]`. + */ stdout: string; + /** + The output of the subprocess on [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). + + If the output ends with a [newline](https://en.wikipedia.org/wiki/Newline), that newline is automatically stripped. + + This is an empty string if either: + - The `stderr` option is set to another value than `'pipe'` (its default value). + - The output is being iterated using `subprocess.stderr` or `subprocess[Symbol.asyncIterator]`. + */ stderr: string; + /** + Like `result.stdout` but for both the [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) and [standard error](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)), interleaved. + */ output: string; + /** + The file and arguments that were run. + + It is intended for logging or debugging. Since the escaping is fairly basic, it should not be executed directly. + */ command: string; + /** + Duration of the subprocess, in milliseconds. + */ durationMs: number; + /** + If `subprocess.pipe()` was used, the result or error of the other subprocess that was piped into this subprocess. + */ pipedFrom?: Result | SubprocessError; }; +/** +When the subprocess fails, its promise is rejected with this error. + +Subprocesses fail either when their exit code is not `0` or when terminated by a signal. Other failure reasons include misspelling the command name or using the [`timeout`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options) option. +*/ export type SubprocessError = Error & Result & { + /** + The numeric [exit code](https://en.wikipedia.org/wiki/Exit_status) of the subprocess that was run. + + This is `undefined` when the subprocess could not be started, or when it was terminated by a signal. + */ exitCode?: number; + /** + The name of the [signal](https://en.wikipedia.org/wiki/Signal_(IPC)) (like [`SIGTERM`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM)) that terminated the subprocess, sent by either: + - The current process. + - Another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events). + + If a signal terminated the subprocess, this property is defined and included in the [error message](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message). Otherwise it is `undefined`. + */ signalName?: string; }; +/** +Subprocess started by `spawn()`. + +A subprocess is a promise that is either resolved with a successful `result` object or rejected with a `subprocessError`. + +It is also an iterable, iterating over each `stdout`/`stderr` line, as soon as it is available. The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess fails. This means you do not need to call `await subprocess`. +*/ export type Subprocess = Promise & AsyncIterable & { + /** + Underlying [Node.js child process](https://nodejs.org/api/child_process.html#class-childprocess). + + Among other things, this can be used to terminate the subprocess using [`.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) or exchange IPC message using [`.send()`](https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback). + */ nodeChildProcess: Promise; + /** + Iterates over each `stdout` line, as soon as it is available. + + The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess fails. This means you do not need to call `await subprocess`. + */ stdout: AsyncIterable; + /** + Iterates over each `stderr` line, as soon as it is available. + + The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess fails. This means you do not need to call `await subprocess`. + */ stderr: AsyncIterable; + /** + Similar to the `|` symbol in shells. [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the subprocess's[`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) to a second subprocess's [`stdin`](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin)). + + This resolves with that second subprocess's result. If either subprocess is rejected, this is rejected with that subprocess's error instead. + + This follows the same syntax as `spawn(file, arguments?, options?)`. It can be done multiple times in a row. + + @param file - The program/script to execute + @param arguments - Arguments to pass to `file` on execution. + @param options + @returns `Subprocess` + + @example + + ``` + const result = await spawn('npm', ['run', 'build']) + .pipe('sort') + .pipe('head', ['-n', '2']); + ``` + */ pipe(file: string, arguments?: readonly string[], options?: Options): Subprocess; pipe(file: string, options?: Options): Subprocess; }; -export default function nanoSpawn(file: string, arguments?: readonly string[], options?: Options): Subprocess; -export default function nanoSpawn(file: string, options?: Options): Subprocess; +/** +Executes a command using `file ...arguments`. + +This has the same syntax as [`child_process.spawn()`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options). + +If `file` is `'node'`, the current Node.js version and [flags](https://nodejs.org/api/cli.html#options) are inherited. + +@param file - The program/script to execute +@param arguments - Arguments to pass to `file` on execution. +@param options +@returns `Subprocess` + +@example Run commands + +``` +import spawn from 'nano-spawn'; + +const result = await spawn('echo', ['🦄']); + +console.log(result.output); +//=> '🦄' +``` + +@example Iterate over output lines + +``` +for await (const line of spawn('ls', ['--oneline'])) { + console.log(line); +} +//=> index.d.ts +//=> index.js +//=> … +``` +*/ +export default function spawn(file: string, arguments?: readonly string[], options?: Options): Subprocess; +export default function spawn(file: string, options?: Options): Subprocess;