Skip to content

About

A cross platform solution to node's spawn and spawnSync (ESM fork of cross-spawn)

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

cross-spawn-esm

npm package github repo view on npmx

This is a drop-in replacement for cross-spawn ( a cross-platform solution to node's spawn and spawnSync ) which ports its codebase to modern ESM and TypeScript.

This package must behave the same as the original package, but small changes were made during the refactoring. Please refer to the Migration guide for further explanation.

Installation

With NPM:

npm install cross-spawn-esm

With Yarn:

yarn add cross-spawn-esm

With PNPM:

pnpm add cross-spawn-esm

With Bun:

bun add cross-spawn-esm

With Deno:

deno add cross-spawn-esm

Usage

Use it exactly the same way as node's spawn and spawnSync ( a drop-in replacement for them ) with the same arguments and options. There is no default export; always use named imports.

import { spawn, spawnSync } from "cross-spawn-esm";

// Spawn NPM asynchronously
const child = spawn("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });

// Spawn NPM synchronously
const result = spawnSync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });

Benefits

  • Overall smaller bundle size (~50% smaller)
  • Tree-shaking friendly
  • Zero dependencies
  • No need for an additional types package (@types/cross-spawn)
  • Modern codebase and active maintenance
  • Better documentation

Security

Windows batch files and shell: true have platform-specific escaping rules. Batch handling is controlled by the batchMode option:

import { spawn, spawnSync } from "cross-spawn-esm";

// Preserve the previous cross-spawn behavior (default).
spawn("npm", ["install"]);

// Opt in to quote-based batch escaping and extra validation.
spawn("npm", ["install"], { batchMode: "strict" });

// Refuse to run .bat/.cmd files at all. `spawn` throws, while `spawnSync`
// surfaces the same EINVAL through `result.error`.
const result = spawnSync("some-tool.cmd", [], { batchMode: "reject" });
if (result.error) throw result.error;

The default is "legacy" for backwards compatibility. Unknown runtime values also fall back to "legacy". Use "strict" or "reject" when the command or its arguments can be influenced by untrusted input. See SECURITY.md for the batch escaping model, the %~1 limitation, and safe shell: true usage.

Migration guide

Porting from cross-spawn to cross-spawn-esm is mostly a matter of changing how you import the package and how you call its sync API.

1. Install

Uninstall cross-spawn (and @types/cross-spawn if you had it) and install cross-spawn-esm:

npm remove cross-spawn @types/cross-spawn
npm install cross-spawn-esm

2. Update imports & sync calls

Before:

import spawn from "cross-spawn";

const child = spawn("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
const result = spawn.sync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });

After:

import { spawn, spawnSync } from "cross-spawn-esm";

const child = spawn("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });
const result = spawnSync("npm", ["list", "-g", "-depth", "0"], { stdio: "inherit" });

3. Internal utilities

If you were relying on the hidden internals:

Before: spawn._parse(...), spawn._enoent.verifyENOENT(...)

After:

import { _parse, _enoent } from "cross-spawn-esm";
Changes:
  • _enoent.verifyENOENT(status, parsed, syscall) now takes an explicit syscall argument ("spawn" or "spawnSync"), where the original shipped two separate functions (verifyENOENT / verifyENOENTSync).
  • _enoent.notFoundError(...) now returns a valid NodeJS.ErrnoException.
  • _parse now contains both parse and parseNonShell functions:

Before: const parsed = _parse(...)

After: const parsed = _parse.parse(...)

  • New _utils object exposes all the lower-level helpers. If you were relying on the original cross-spawn dependencies (path-key and shebang-command), their improved versions can be found in _utils.
  • Error constructors are internal, except for _enoent.notFoundError, which the original package already exposed. The public API also exports the SpawnError type.

4. TypeScript

cross-spawn-esm ships its own type definitions, so the third-party @types/cross-spawn package is no longer needed. The package exports BatchMode, BatchOptions, CrossSpawnOptions, ParsedCommand, PathKeyOptions, SpawnError, SpawnOptions, and SpawnSyncOptions; the option types extend Node's corresponding child_process types with the additional batchMode option.

5. Behavior notes

Beyond the API surface, a few implementation details intentionally differ:

  • original.args is an independent snapshot: parse clones the args and gives original.args its own copy, so shebang rewiring and cmd.exe escaping never mutate it.

  • Broader shebang support: Shebang detection on Windows reads the shebang of the resolved script and rewires the command to its interpreter. #!/usr/bin/env <program> is resolved from PATH and spawned directly. Any other shebang (#!/bin/sh, #!/bin/bash -e) is reduced to the interpreter's basename (plus its single argument, if any) and falls back to the cmd.exe wrapper, which works when that interpreter is on PATH.

  • Batch-file handling (Windows): batchMode controls how commands that resolve to or are named as .bat/.cmd files are handled.

    • "legacy" (default) preserves the previous cross-spawn caret escaping. It is kept for backwards compatibility and is not safe for untrusted input.
    • "strict" uses quote-based escaping (" becomes ""), rejects %, NUL, CR, and LF with ERR_INVALID_ARG_VALUE, rejects batch command names ending in a dot or space, and wraps the command in cmd /d /e:on /v:off /s /c. This blocks CVE-2024-27980 for batch files that forward arguments with %*. Batch files that strip quotes themselves (%~1) and use the value unquoted can still be injected; that is outside the caller's control. Strict validation errors are thrown by both spawn() and spawnSync().
    • "reject" mirrors Node's CVE-2024-27980 rejection: spawn() throws an EINVAL error synchronously, while spawnSync() returns the error in result.error (code: "EINVAL", errno: -4071) without throwing.

    batchMode only applies on Windows and when shell is not enabled. With shell: true, Node's shell semantics apply and this package does not touch the command or arguments.

The following behaviors are intentionally kept identical to cross-spawn:

  • When options.shell is used, parsing, escaping, and shebang enhancements are disabled - matching both the original and Node.js behavior.
  • Windows-only ENOENT detection: when the process exits with code 1 and the command could not be resolved, an error event (async) or result.error (sync) is produced.

API comparison

Feature cross-spawn cross-spawn-esm
Module format CommonJS ESM ("type": "module")
Async spawn spawn (default export) spawn (named export)
Sync spawn spawn.sync spawnSync
Parse internals spawn._parse _parse
ENOENT internals spawn._enoent _enoent
Other internals not exposed _utils
TypeScript types @types/cross-spawn built-in

License

Released under the MIT License.

About

A cross platform solution to node's spawn and spawnSync (ESM fork of cross-spawn)

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages