Skip to content

Minimalist `spawn` and `spawnSync` enhancements for Node.js

License

Notifications You must be signed in to change notification settings

alloc/picospawn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

picospawn

Minimalist spawn and spawnSync replacements with a focus on simplicity and ease of use.

  • ESM and CommonJS compatible.
  • Excellent TypeScript support.
  • …and a list of features below.
pnpm add picospawn

Usage

Asynchronous (spawn)

import $ from 'picospawn'

const proc = $('ls -l /', {
  // These are the default options.
  cwd: process.cwd(),
  env: process.env,
  stdio: 'pipe',
  reject: true,
  json: false,
  // …and any other `spawn` option.
})
proc.stdout // ReadableStream | null
proc.stderr // ReadableStream | null

const result = await proc
result.stdout // string
result.stderr // string

Synchronous (spawnSync)

The spawnSync function is purpose-built for replacing Shell scripts with Node.js by providing a simple way to block on a child process and exit with the same signal or non-zero status code as the child process.

Notably, spawnSync defaults to using stdio: "inherit" and encoding: "utf-8". You can override these by passing an options object.

I recommend importing it like this: { spawnSync as $ }

import { spawnSync as $ } from 'picospawn'

// By default, spawnSync exits the parent process if the child fails.
// Stdout is returned directly.
const stdout = $('echo', ['hello world'], { stdio: 'pipe' })
console.log(stdout) // Output: hello world

// To prevent exiting on failure and get the full result object:
const result = $('exit 1', { exit: false })
console.log(result.status) // Output: 1

When using spawnSync, it can sometimes be difficult to determine which command failed. If you set PICOSPAWN_TRACE=1 in your environment, the stack trace will be printed to the console.

Features

The argument array is optional.

You can pass a single string or an array of arguments to the command.

import $ from 'picospawn'

const proc = $('echo "hello world"')

You can even use %s placeholders if only one or a few arguments need to be interpolated. This is especially useful for arguments that could contain spaces or special characters.

import $ from 'picospawn'

await $('echo %s baz', ['foo bar'], { stdio: 'inherit' })
// Output: foo bar baz

Note that placeholders are unnecessary at the end of the command string.

Any extra arguments are included at the end of the command. In other words, you can specify arguments within the command string and pass an array of extra arguments too.

Await the result or process the output stream.

You can either wait for the child process to exit and get the result, or process the output stream as it's received.

import $ from 'picospawn'

// Process the output stream as it's received.
const proc = $('ls -l /')
proc.stdout?.on('data', chunk => console.log(chunk))

// Wait for the child process to exit and get the result.
const result = await proc
result.stdout // string
result.stderr // string

Parse the stdout as JSON.

You can parse the stdout as JSON by setting json: true or by calling $.json().

import $ from 'picospawn'

type Result = { foo: 'bar' }

const { stdout } = await $<Result>(`echo '{"foo": "bar"}'`, {
  json: true,
})

console.log(stdout.foo) // Output: bar

// Alternative syntax.
const { stdout } = await $.json<Result>(`echo '{"foo": "bar"}'`)

console.log(stdout.foo) // Output: bar

Create specialized spawn functions.

You can create specialized spawn functions with createSpawn().

import { createSpawn } from 'picospawn'

const $ = createSpawn({
  cwd: '/tmp',
  env: { ...process.env, FOO: 'bar' },
  shell: true,
})

const { stdout } = await $('echo $PWD $FOO')
console.log(stdout) // Output: /tmp bar

Prevent rejection on error.

By default, spawn will reject its promise if the command exits unexpectedly. You can opt-out by setting reject: false.

import $ from 'picospawn'

const proc = $('exit 1', { reject: false })
proc.on('exit', code => {
  console.log(code) // Output: 1
})

const { exitCode } = await proc
console.log(exitCode) // Output: 1

Argument arrays can be nested.

Especially useful for conditional arguments, since you can do condition && ['foo', 'bar'] where the arguments won't be used if the condition is false.

import $ from 'picospawn'

const { stdout } = await $('echo %s baz', ['foo bar', ['qux']])
console.log(stdout) // Output: foo bar baz qux

Transform stdio streams.

You can pass functions in the stdio array to transform the data as it streams.

import $ from 'picospawn'

async function* addPrefix(stream) {
  for await (const chunk of stream) {
    yield '>> ' + chunk
  }
}

// Pipe stdout through the addPrefix transformer.
await $('echo hello world', {
  stdio: ['inherit', addPrefix, 'inherit'],
})
// Output: >> hello world

Prior Art