bun/no-unportable-commands

Disallow starting a process with a command that does not work the same on every system

A command that works on macOS and Linux can fail on Windows:

  • npm is a script there (npm.cmd), and you can't start a script without a shell.
  • ls, rm and grep don't exist.
  • && and | are shell syntax, and the shell isn't the same on every system.

The rule checks exec, spawn and execFile from node:child_process, their Sync versions, Bun.spawn and Bun.spawnSync, when the command is a string in the call. It also reports any string that contains ps with options, such as "ps -ef", because ps takes different options on each system.

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"bun/no-unportable-commands" = "error" # or "warn"

Every option, set to its default. The ones that are commented out are unset by default.

bunfig.toml
[lint.rules]
"bun/no-unportable-commands" = ["error", {
  scripts = ["npm", "npx", "pnpm", "pnpx", "yarn"],
  # posixOnly = <list of strings>,
  launchers = [], # list of strings
}]

Examples#

example.js3 problems
import { exec, spawn } from "node:child_process";

spawn("npm", ["install"]);
spawn("rm", ["-rf", "dist"]);
exec("bun run build && bun run deploy");
example.js1 problem
Bun.spawn(["cat", "notes.txt"]);
example.js1 problem
const listProcesses = "ps -ef";
example.jsNo problems
import { spawn } from "node:child_process";
import { rm } from "node:fs/promises";

spawn("npm", ["install"], { shell: true });
await rm("dist", { recursive: true, force: true });

Bun Shell has its own rm, ls, cat, && and |, which work the same on every system:

example.jsNo problems
import { $ } from "bun";

await $`rm -rf dist`;
await $`bun run build && bun run deploy`;

Options#

OptionDefaultWhat it does
scripts["npm", "npx", "pnpm", "pnpx", "yarn"]Commands that are .cmd scripts on Windows. Reported when they're started without a shell.
posixOnlyawk, cat, chmod, cp, grep, kill, ls, mv, rm, sed, sh, touch and 21 moreCommands that Windows doesn't have. Reported with or without a shell.
launchers[]Names of your own functions that start a process. Their first argument is checked like the first argument of spawn.

Setting a list replaces the default, so repeat the defaults you want to keep.

example.jswith { "scripts": ["npm", "tsc"] }1 problem
import { spawnSync } from "node:child_process";

spawnSync("tsc", ["--noEmit"]);
example.jswith { "launchers": ["run"] }1 problem
await run("grep", ["-r", "TODO", "src"]);

When not to use it#

Leave it off if your code never runs on Windows.