/nano-spawn

Tiny process execution for humans — a better child_process

Primary LanguageJavaScriptMIT LicenseMIT

nano-spawn logo

Test coverage

Tiny process execution for humans — a better child_process

Features

No dependencies. Small package size: npm package minzipped size Install size

Despite the small size, this is packed with some essential features:

For additional features, please check out Execa.

Install

npm install nano-spawn

One of the maintainers @ehmicky is looking for a remote full-time position. Specialized in Node.js back-ends and CLIs, he led Netlify Build, Plugins and Configuration for 2.5 years. Feel free to contact him on his website or on LinkedIn!


Usage

Run commands

import spawn from 'nano-spawn';

const result = await spawn('echo', ['🦄']);

console.log(result.output);
//=> '🦄'

Iterate over output lines

for await (const line of spawn('ls', ['--oneline'])) {
	console.log(line);
}
//=> index.d.ts
//=> index.js
//=> …

Pipe commands

const result = await spawn('npm', ['run', 'build'])
	.pipe('sort')
	.pipe('head', ['-n', '2']);

API

spawn(file, arguments?, options?) default export

file: string
arguments: string[]
options: Options
Returns: Subprocess

Executes a command using file ...arguments.

This has the same syntax as child_process.spawn().

If file is 'node', the current Node.js version and flags 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 can be passed to spawn().

options.env

Type: object
Default: {}

Override specific environment variables. Other environment variables are inherited from the current process (process.env).

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/output/error.

All values supported by node:child_process are available. The most common ones are:

Subprocess

Subprocess started by spawn().

await subprocess

Returns: Result
Throws: SubprocessError

A subprocess is a promise that is either resolved with a successful result object or rejected with a subprocessError.

subprocess.stdout

Returns: AsyncIterable<string>
Throws: SubprocessError

Iterates over each stdout line, as soon as it is available.

The iteration waits for the subprocess to end (even when using break or return). It throws if the subprocess fails. This means you do not need to call await subprocess.

subprocess.stderr

Returns: AsyncIterable<string>
Throws: SubprocessError

Same as subprocess.stdout but for stderr instead.

subprocess[Symbol.asyncIterator]()

Returns: AsyncIterable<string>
Throws: SubprocessError

Same as subprocess.stdout but for both stdout and stderr.

subprocess.pipe(file, arguments?, options?)

file: string
arguments: string[]
options: Options
Returns: Subprocess

Similar to the | symbol in shells. Pipe the subprocess'sstdout to a second subprocess's 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.

await subprocess.nodeChildProcess

Type: ChildProcess

Underlying Node.js child process.

Among other things, this can be used to terminate the subprocess using .kill() or exchange IPC message using .send().

Result

When the subprocess succeeds, its promise is resolved with an object with the following properties.

result.stdout

Type: string

The output of the subprocess on standard output.

If the output ends with a newline, that newline is automatically stripped.

This is an empty string if either:

result.stderr

Type: string

Like result.stdout but for the standard error instead.

result.output

Type: string

Like result.stdout but for both the standard output and standard error, 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() was used, the result or error of the other subprocess that was piped into this subprocess.

SubprocessError

Type: Error

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 option.

Subprocess errors have the same shape as successful results, with the following additional properties.

subprocessError.exitCode

Type: number | undefined

The numeric exit code of the subprocess that was run.

This is undefined when the subprocess could not be started, or when it was terminated by a signal.

subprocessError.signalName

Type: string | undefined

The name of the signal (like SIGTERM) that terminated the subprocess, sent by either:

If a signal terminated the subprocess, this property is defined and included in the error message. Otherwise it is undefined.

Windows support

This package fixes several cross-platform issues with node:child_process. It brings full Windows support for:

  • Node modules binaries (without requiring the shell option). This includes running npm ... or yarn ....
  • .cmd, .bat, and other shell files.
  • The PATHEXT environment variable.
  • Windows-specific newlines.

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 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.

On one hand, it has a bigger size: Install size

On the other hand, it provides a bunch of additional features: scripts, template string syntax, synchronous execution, file input/output, binary input/output, advanced piping, verbose mode, graceful or forceful termination, IPC, shebangs on Windows, and much more. Also, it is very widely used and battle-tested.

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 core module.

If you'd prefer avoiding adding any dependency, you may use node:child_process directly. However, you might miss the features nano-spawn provides: proper error handling, full Windows support, local binaries, piping, lines iteration, interleaved output, and more.

import {execFile} from 'node:child_process';
import {promisify} from 'node:util';

const pExecFile = promisify(execFile);

const result = await pExecFile('npm', ['run', 'build']);

Maintainers