Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 54 additions & 7 deletions docs/windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@ await execa`node ./script.js`;

Although Windows does not natively support shebangs, Execa adds support for them.

## Signals
## PATHEXT support

Only few [signals](termination.md#other-signals) work on Windows with Node.js: [`SIGTERM`](termination.md#sigterm), [`SIGKILL`](termination.md#sigkill), [`SIGINT`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGINT) and [`SIGQUIT`](termination.md#sigquit). Also, sending signals from other processes is [not supported](termination.md#signal-name-and-description). Finally, the [`forceKillAfterDelay`](api.md#optionsforcekillafterdelay) option [is a noop](termination.md#forceful-termination) on Windows.
On Windows, the [`PATHEXT`](https://ss64.com/nt/path.html#pathext) environment variable defines which file extensions can be executed without specifying the extension. For example, if `.JS` is in `PATHEXT`:

## Asynchronous I/O

The default value for the [`stdin`](api.md#optionsstdin), [`stdout`](api.md#optionsstdout) and [`stderr`](api.md#optionsstderr) options is [`'pipe'`](output.md#stdout-and-stderr). This returns the output as [`result.stdout`](api.md#resultstdout) and [`result.stderr`](api.md#resultstderr) and allows for [manual streaming](streams.md#manual-streaming).
```js
// On Windows, if PATHEXT includes .JS, both work:
await execa`node script.js`;
await execa`node script`; // Automatically finds script.js
```

Instead of `'pipe'`, `'overlapped'` can be used instead to use [asynchronous I/O](https://learn.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o) under-the-hood on Windows, instead of the default behavior which is synchronous. On other platforms, asynchronous I/O is always used, so `'overlapped'` behaves the same way as `'pipe'`.
Execa uses [`node-which`](https://github.com/npm/node-which) internally to resolve the absolute file path of the executable, ensuring proper `PATHEXT` support on Windows.

## Escaping
## Commands with spaces

Windows requires files and arguments to be quoted when they contain spaces, tabs, backslashes or double quotes. Unlike Unix, this is needed even when no [shell](shell.md) is used.

Expand All @@ -44,6 +46,51 @@ When not using any shell, Execa performs that quoting automatically. This ensure
await execa`npm run ${'task with space'}`;
```

## How Windows execution works

Under the hood, Execa uses [`node-cross-spawn`](https://github.com/moxystudio/node-cross-spawn) to fix several Windows-specific issues. Here's what happens when you run a command on Windows:

1. **Resolve the executable**: The command name is resolved to an absolute path using `node-which`, which properly handles `PATHEXT`.

2. **Detect shebangs**: If the file is not a `.exe` or `.com` file, Execa reads the first 150 bytes to detect any shebang (e.g., `#!/usr/bin/env node`).

3. **Execute via cmd.exe**: For files with shebangs or scripts (like `.bat`, `.cmd` files), Execa runs them through `cmd.exe /d /s /c` with proper escaping, rather than using Node.js's default behavior.

This process is automatic and only applies to Windows. It is skipped when using [`shell: true`](shell.md), though using a shell has different trade-offs (see below).

## Comparison: Automatic fixes vs shell mode

When deciding whether to use a shell on Windows, consider the following:

| Feature | Automatic fixes (default) | `shell: true` |
|---------|--------------------------|---------------|
| PATHEXT support | ✅ Yes | ✅ Yes (via `cmd.exe`) |
| Shebang support | ✅ Yes | ❌ No |
| Escaping quality | ✅ Excellent (cross-spawn) | ⚠️ Good (Node.js built-in) |
| Extra overhead | Minimal | One more process |

**Recommendation**: Use the default (automatic fixes) for most cases. Use `shell: true` only when you specifically need shell features like redirection (`>`, `|`) or environment variable expansion.

```js
// Default: Uses automatic Windows fixes
await execa`./my-script.js`;

// Shell mode: Use only when you need shell features
await execa({shell: true})`echo %PATH% | findstr "Node"`;
```

## Signals

Only few [signals](termination.md#other-signals) work on Windows with Node.js: [`SIGTERM`](termination.md#sigterm), [`SIGKILL`](termination.md#sigkill), [`SIGINT`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGINT) and [`SIGQUIT`](termination.md#sigquit). Also, sending signals from other processes is [not supported](termination.md#signal-name-and-description). Finally, the [`forceKillAfterDelay`](api.md#optionsforcekillafterdelay) option [is a noop](termination.md#forceful-termination) on Windows.

## Asynchronous I/O

The default value for the [`stdin`](api.md#optionsstdin), [`stdout`](api.md#optionsstdout) and [`stderr`](api.md#optionsstderr) options is [`'pipe'`](output.md#stdout-and-stderr). This returns the output as [`result.stdout`](api.md#resultstdout) and [`result.stderr`](api.md#resultstderr) and allows for [manual streaming](streams.md#manual-streaming).

Instead of `'pipe'`, `'overlapped'` can be used instead to use [asynchronous I/O](https://learn.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o) under-the-hood on Windows, instead of the default behavior which is synchronous. On other platforms, asynchronous I/O is always used, so `'overlapped'` behaves the same way as `'pipe'`.

## Escaping with shells

When using a [shell](shell.md), the user must manually perform shell-specific quoting, on both Unix and Windows. When the [`shell`](api.md#optionsshell) option is `true`, [`cmd.exe`](https://en.wikipedia.org/wiki/Cmd.exe) is used on Windows and `sh` on Unix. Unfortunately, both shells use different quoting rules. With `cmd.exe`, this mostly involves double quoting arguments and prepending double quotes with a backslash.

```js
Expand Down
39 changes: 39 additions & 0 deletions examples/build-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { execa } from 'execa';

/**
* Build automation example with error handling
* Demonstrates running build commands with proper error handling
*/

async function runBuild() {
console.log('🔨 Starting build process...\n');

try {
// Clean previous build
console.log('🧹 Cleaning previous build...');
await execa('rm', ['-rf', 'dist']);
console.log('✅ Clean complete\n');

// Type checking
console.log('🔍 Running type check...');
await execa('tsc', ['--noEmit'], { stdio: 'inherit' });
console.log('✅ Type check passed\n');

// Building
console.log('📦 Building project...');
const { stdout } = await execa('npm', ['run', 'build']);
console.log(stdout);
console.log('✅ Build successful!\n');

} catch (error) {
console.error('❌ Build failed:', error.message);
process.exit(1);
}
}

// Run if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
runBuild();
}

export { runBuild };
70 changes: 70 additions & 0 deletions examples/git-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { execa } from 'execa';

/**
* Git helper functions using execa
* Wrappers for common git operations
*/

/**
* Get the current git branch
*/
async function getCurrentBranch() {
const { stdout } = await execa('git', ['branch', '--show-current']);
return stdout.trim();
}

/**
* Get the latest commit message
*/
async function getLatestCommit() {
const { stdout } = await execa('git', ['log', '-1', '--pretty=%s']);
return stdout.trim();
}

/**
* Check if working directory is clean
*/
async function isWorkingDirectoryClean() {
try {
await execa('git', ['diff', '--quiet']);
await execa('git', ['diff', '--staged', '--quiet']);
return true;
} catch {
return false;
}
}

/**
* Get repository status
*/
async function getStatus() {
const { stdout } = await execa('git', ['status', '--short']);
return stdout || 'Working directory clean';
}

/**
* Stage all changes and commit
*/
async function stageAndCommit(message) {
await execa('git', ['add', '.']);
await execa('git', ['commit', '-m', message]);
console.log(`✅ Committed: ${message}`);
}

// Demo
if (import.meta.url === `file://${process.argv[1]}`) {
console.log('📁 Git Repository Info\n');

console.log(`Branch: ${await getCurrentBranch()}`);
console.log(`Last commit: ${await getLatestCommit()}`);
console.log(`Working directory: ${await isWorkingDirectoryClean() ? '✅ Clean' : '⚠️ Dirty'}`);
console.log(`Status:\n${await getStatus()}`);
}

export {
getCurrentBranch,
getLatestCommit,
isWorkingDirectoryClean,
getStatus,
stageAndCommit,
};
80 changes: 80 additions & 0 deletions examples/parallel-tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { execa } from 'execa';

/**
* Running commands in parallel
* Demonstrates concurrent execution with proper error handling
*/

/**
* Run multiple linting tasks in parallel
*/
async function runParallelLints() {
console.log('🔍 Running linters in parallel...\n');

try {
const tasks = [
{ name: 'ESLint', cmd: ['eslint', '.'] },
{ name: 'Prettier', cmd: ['prettier', '--check', '.'] },
{ name: 'TypeScript', cmd: ['tsc', '--noEmit'] },
];

const results = await Promise.allSettled(
tasks.map(async ({ name, cmd }) => {
console.log(`🚀 Starting ${name}...`);
await execa('npx', cmd);
return { name, status: 'passed' };
})
);

console.log('\n📊 Results:');
let hasErrors = false;

for (const result of results) {
if (result.status === 'fulfilled') {
console.log(` ✅ ${result.value.name}`);
} else {
console.log(` ❌ ${result.reason.name || 'Task'}`);
hasErrors = true;
}
}

if (hasErrors) {
process.exit(1);
}

} catch (error) {
console.error('Unexpected error:', error);
process.exit(1);
}
}

/**
* Run multiple independent commands in parallel
* with a limit on concurrency
*/
async function runWithLimit(tasks, limit = 3) {
const results = [];
const executing = [];

for (const [index, task] of tasks.entries()) {
const promise = task().then(result => ({ index, result }));
results.push(promise);

if (tasks.length >= limit) {
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(executing.findIndex(p => p === promise), 1);
}
}
}

return Promise.all(results);
}

// Demo
if (import.meta.url === `file://${process.argv[1]}`) {
runParallelLints();
}

export { runParallelLints, runWithLimit };
46 changes: 46 additions & 0 deletions examples/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Execa Examples

This directory contains practical examples demonstrating common use cases with `execa`.

## Examples

### build-script.js
Build automation with error handling. Shows how to chain build commands and handle failures gracefully.

```bash
node examples/build-script.js
```

### git-helpers.js
Git operation wrappers. Demonstrates async helper functions for common git operations.

```bash
node examples/git-helpers.js
```

### parallel-tasks.js
Running commands in parallel. Shows concurrent execution with `Promise.allSettled()` and progress tracking.

```bash
node examples/parallel-tasks.js
```

### security-scanning.js
Security scanning utilities. Demonstrates integrating security tools like npm audit, semgrep, and gitleaks.

```bash
node examples/security-scanning.js
```

### stream-processing.js
Output filtering with transforms. Demonstrates using Node.js Transform streams to process command output.

```bash
node examples/stream-processing.js
```

## Notes

- All examples use ES modules (`.js` extension with `import` syntax)
- Each example can be run directly or imported as a module
- Error handling is included in all examples
Loading