tinyexec: Minimal Node.js Process Execution
tinyexec is a lightweight Node.js library designed for executing child processes, offering a streamlined, promise-based API as an alternative to Node.js's native `child_process` module or more feature-rich libraries like `execa`. It abstracts away direct stream manipulation, providing a simpler interface for spawning, piping, and awaiting process results. As of its current stable version, 1.1.1, it supports both asynchronous (`x`) and synchronous (`xSync`) command execution, including options for setting timeouts, integrating `AbortSignal` for cancellation, and passing `stdin` input. Key differentiators include its focus on minimalism, automatic resolution of local `node_modules` binaries, and the ability to iterate over process output lines asynchronously. The package is actively maintained with frequent minor releases and bug fixes, and it is ESM-only since version 1.0.0, requiring Node.js 18 or higher.
Common errors
-
Error [ERR_REQUIRE_ESM]: require() of ES Module ... tinyexec/dist/index.js not supported. Instead change the require of index.js in ... to a dynamic import() which is available in all CommonJS modules.
cause Attempting to import `tinyexec` using a CommonJS `require()` statement in a Node.js environment where tinyexec is treated as an ES Module.fixRefactor your code to use ES Module `import` syntax: `import { x } from 'tinyexec';`. Ensure your project's `package.json` includes `"type": "module"` or that your file has a `.mjs` extension. -
Error: Command failed with exit code 1
cause This error occurs when `throwOnError: true` is set in the options, and the executed process exits with a non-zero status code, indicating a failure.fixInspect the `stderr` output (e.g., `error.stderr`) for clues about why the command failed. If a non-zero exit code should not throw an error, set `throwOnError: false` in the options object. -
TypeError: proc.pipe is not a function
cause Attempting to call `.pipe()` on the `Output` result object after `await x(...)` has resolved, rather than on the live `Promise` returned by `x`.fixChain the `.pipe()` call directly onto the `x()` invocation before awaiting: `const proc1 = x('ls', ['-l']); const proc2 = proc1.pipe('grep', ['.js']); const result = await proc2;`
Warnings
- breaking tinyexec migrated to an ESM-only build starting with version 1.0.0. CommonJS `require()` statements are no longer supported.
- breaking The default behavior for `throwOnError` has changed across minor versions. In v0.3.0, `tinyexec` started throwing on non-zero exit codes by default. However, as of recent versions (including 1.x), it *does not* throw by default.
- gotcha Output from `stdout` and `stderr` streams does not automatically trim trailing newlines. The exact raw output is returned.
- gotcha tinyexec requires Node.js version 18 or higher due to its ESM-only nature and usage of modern Node.js features.
- gotcha When attempting to pass commands as a single shell-like string (e.g., `await x('echo "Hello, World!"')`), `tinyexec` expects the command and arguments to be split. It does not invoke a shell by default.
Install
-
npm install tinyexec -
yarn add tinyexec -
pnpm add tinyexec
Imports
- x
const x = require('tinyexec');import { x } from 'tinyexec'; - xSync
const { xSync } = require('tinyexec');import { xSync } from 'tinyexec'; - Output
import type { Output } from 'tinyexec';
Quickstart
import { x, xSync } from 'tinyexec';
import { tokenizeArgs } from 'args-tokenizer'; // Optional, for parsing string commands
async function runExample() {
console.log('--- Async Example (x) ---');
try {
const result = await x('ls', ['-l', '.'], {
timeout: 5000, // Process will be killed after 5 seconds
throwOnError: true, // Throws an error if exitCode is non-zero
env: { ...process.env, MY_CUSTOM_VAR: 'hello' } // Pass custom environment variables
});
console.log('stdout:', result.stdout.trim());
console.log('stderr:', result.stderr.trim());
console.log('exitCode:', result.exitCode);
} catch (error) {
console.error('Async process failed:', error);
}
console.log('\n--- Async Iteration Example ---');
const proc = x('node', ['-e', 'console.log("Line 1"); await new Promise(r => setTimeout(r, 10)); console.log("Line 2");']);
console.log('Output lines:');
for await (const line of proc) {
console.log(`- ${line.trim()}`);
}
console.log('\n--- Sync Example (xSync) ---');
try {
const syncResult = xSync('node', ['-e', 'console.log("Hello from sync"); process.exit(0);']);
console.log('sync stdout:', syncResult.stdout.trim());
} catch (error) {
console.error('Sync process failed:', error);
}
console.log('\n--- Piping Example ---');
// Using args-tokenizer for a more realistic command string parsing example
const commandString = 'grep .mjs';
const [grepCommand, ...grepArgs] = tokenizeArgs(commandString);
const proc1 = x('ls', ['.']);
const proc2 = proc1.pipe(grepCommand, grepArgs);
const pipedResult = await proc2;
console.log('Piped .mjs files:', pipedResult.stdout.trim());
}
runExample();