Skip to content

Commit

Permalink
Add multiple guides and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Apr 21, 2024
1 parent fdd8a9f commit 66a183e
Show file tree
Hide file tree
Showing 32 changed files with 2,975 additions and 1,507 deletions.
70 changes: 70 additions & 0 deletions docs/binary.md
@@ -0,0 +1,70 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../media/logo_dark.svg">
<img alt="execa logo" src="media/logo.svg" width="400">
</picture>
<br>

# 🤖 Binary data

## Binary output

By default, the subprocess [output](../readme.md#resultstdout) is a UTF8 string. If it is binary, the [`encoding`](../readme.md#optionsencoding) option should be set to `'buffer'` instead. The output will be an [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array).

```js
import {execa} from 'execa';

const {stdout} = await execa({encoding: 'buffer'})`unzip file.zip`;
console.log(stdout.byteLength);
```

## Encoding

When the output is binary, the [`encoding`](../readme.md#optionsencoding) option can also be set to `'hex'`, `'base64'` or `'base64url'`. The output will be a string then.

```js
const {stdout} = await execa({encoding: 'hex'})`unzip file.zip`;
console.log(stdout); // Hexadecimal string
```

## Iterable

By default, the subprocess [iterates](lines.md#progressive-splitting) over line strings. However, if the [`encoding`](../readme.md#optionsencoding) subprocess option is binary, or if the [`binary`](../readme.md#readableoptionsbinary) iterable option is `true`, it iterates over arbitrary chunks of [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) data instead.

```js
for await (const data of execa({encoding: 'buffer'})`unzip file.zip`) { /* ... */ }
```

## Streams

[Streams produced](streams.md#converting-a-subprocess-to-a-stream) by [`subprocess.readable()`](../readme.md#subprocessreadablereadableoptions) and [`subprocess.duplex()`](../readme.md#subprocessduplexduplexoptions) are binary by default, which means they iterate over arbitrary [`Buffer`](https://nodejs.org/api/buffer.html#class-buffer) chunks. However, if the [`binary`](../readme.md#readableoptionsbinary) option is `false`, they iterate over line strings instead, and the stream is [in object mode](https://nodejs.org/api/stream.html#object-mode).

```js
const readable = execa`npm run build`.readable({binary: false});
readable.on('data', lineString => { /* ... */ })
```

## Transforms

The same applies to transforms. When the [`encoding`](../readme.md#optionsencoding) subprocess option is binary, or when the [`binary`](transform.md#transformoptionsbinary) transform option is `true`, it iterates over arbitrary chunks of [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) data instead.

However, transforms can always `yield` either a `string` or an `Uint8Array`, regardless of whether the output is binary.

```js
const transform = function * (data) { /* ... */ }

await execa({stdout: {transform, binary: true}})`unzip file.zip`;
```

## Binary input

There are multiple ways to pass binary input using the [`stdin`](../readme.md#optionsstdin), [`input`](../readme.md#optionsinput) or [`inputFile`](../readme.md#optionsinputfile) options. This includes using a `Uint8Array`, a file, or a stream.

```js
await execa({stdin: new Uint8Array([/* ... */])})`hexdump`;
```

<hr>

[**Next**: 🧙 Transforms](transform.md)\
[**Previous**: 📃 Text lines](lines.md)\
[**Top**: Table of contents](../readme.md#documentation)
105 changes: 105 additions & 0 deletions docs/debugging.md
@@ -0,0 +1,105 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../media/logo_dark.svg">
<img alt="execa logo" src="media/logo.svg" width="400">
</picture>
<br>

# 🐛 Debugging

## Command

[`error.command`](../readme.md#resultcommand) contains the file and arguments that were run. It is intended for logging or debugging.

[`error.escapedCommand`](../readme.md#resultescapedcommand) is the same, except control characters are escaped. This makes it safe to either print or copy and paste in a terminal, for debugging purposes.

Since the escaping is fairly basic, this should not be executed directly as a subprocess, including using [`execa()`](#execafile-arguments-options) or [`execaCommand()`](#execacommandcommand-options).

```js
import {execa} from 'execa';

try {
await execa`npm run build\ntask`;
} catch (error) {
console.error(error.command); // 'npm run build\ntask'
console.error(error.escapedCommand); // 'npm run build\\ntask'
throw error;
}
```

## Duration

```js
try {
const result = execa`npm run build`;
console.log('Command duration:', result.durationMs); // 150
} catch (error) {
console.error('Command duration:', error.durationMs); // 150
throw error;
}
```

## Verbose mode

### Short mode

When the [`verbose`](../readme.md#optionsverbose) option is `'short'`, the [command](#command), [duration](#duration) and [error messages](errors.md#error-message) are printed on `stderr`.

```js
// build.js
await execa({verbose: 'short'})`npm run build`;
```

```sh
$ node build.js
[20:36:11.043] [0] $ npm run build
[20:36:11.885] [0] ✔ (done in 842ms)
```

### Full mode

When the [`verbose`](../readme.md#optionsverbose) option is `'full'`, the subprocess' [`stdout` and `stderr`](output.md) are also logged. Both are printed on `stderr`.

The output is not logged if either:
- the [`stdout`](../readme.md#optionsstdout)/[`stderr`](../readme.md#optionsstderr) option is [`ignore`](output.md#ignore-output) or [`inherit`](output.md#terminal-output).
- the `stdout`/`stderr` is redirected to [a stream](streams.md#output), [a file](output.md#file-output), a [file descriptor](output.md#terminal-output), or [another subprocess](pipe.md).
- the [`encoding`](../readme.md#optionsencoding) option is [binary](binary.md#binary-output).

```js
// build.js
await execa({verbose: 'full'})`npm run build`;
```

```sh
$ node build.js
[20:36:11.043] [0] $ npm run build
Building application...
Done building.
[20:36:11.885] [0] ✔ (done in 842ms)
```

### Global mode

When the `NODE_DEBUG=execa` environment variable is set, the [`verbose`](../readme.md#optionsverbose) option defaults to `'full'` for all commands.

```js
// build.js

// This is logged by default
await execa`npm run build`;
// This is not logged
await execa({verbose: 'none'})`npm run test`;
```

```sh
$ NODE_DEBUG=execa node build.js
[20:36:11.043] [0] $ npm run build
Building application...
Done building.
[20:36:11.885] [0] ✔ (done in 842ms)
```

<hr>

[**Next**: 📎 Windows](windows.md)\
[**Previous**: 📞 Inter-process communication](ipc.md)\
[**Top**: Table of contents](../readme.md#documentation)
64 changes: 64 additions & 0 deletions docs/environment.md
@@ -0,0 +1,64 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../media/logo_dark.svg">
<img alt="execa logo" src="media/logo.svg" width="400">
</picture>
<br>

# 🌐 Environment

## [Current directory](https://en.wikipedia.org/wiki/Working_directory)

```js
const {cwd} = await execa`npm run build`;
console.log(cwd); // Current directory when running the command
```

```js
await execa({cwd: '/path/to/cwd'})`npm run build`;
```

## Local binaries

```js
await execa('./node_modules/bin/eslint');
```

The [`preferLocal`](../readme.md#optionspreferlocal) option can be used to automatically find binaries installed in local `node_modules`.

```js
await execa('eslint', {preferLocal: true});
```

Those are searched in the current or any parent directory. The [`localDir`](../readme.md#optionslocaldir) option can select a different directory.

```js
await execa('eslint', {preferLocal: true, localDir: '/path/to/dir'});
```

## Current package's binary

Execa can be combined with [`get-bin-path`](https://github.com/ehmicky/get-bin-path) to test the current package's binary. As opposed to hard-coding the path to the binary, this validates that the `package.json` `bin` field is correctly set up.

```js
import {execa} from 'execa';
import {getBinPath} from 'get-bin-path';

const binPath = await getBinPath();
await execa(binPath);
```

## Background subprocess

When the [`detached`](../readme.md#optionsdetached) option is `true`, the subprocess [runs independently](https://en.wikipedia.org/wiki/Background_process) from the current process.

Specific behavior depends on the platform. [More info.](https://nodejs.org/api/child_process.html#child_process_options_detached).

```js
await execa({detached: true})`npm run start`;
```

<hr>

[**Next**: ❌ Errors](errors.md)\
[**Previous**: 🐢 Node.js files](node.md)\
[**Top**: Table of contents](../readme.md#documentation)
117 changes: 117 additions & 0 deletions docs/errors.md
@@ -0,0 +1,117 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../media/logo_dark.svg">
<img alt="execa logo" src="media/logo.svg" width="400">
</picture>
<br>

# ❌ Errors

## Subprocess failure

When the subprocess fails, the returned promise is rejected with an [`ExecaError`](../readme.md#execaerror) instance. The `error` has the same shape as successful [results](../readme.md#result), with a few additional [error-specific fields](../readme.md#execaerror). `error.failed` is always `true`.

```js
import {execa, ExecaError} from 'execa';

try {
const result = await execa`npm run build`;
console.log(result.failed); // false
} catch (error) {
if (error instanceof ExecaError) {
console.error(error.failed); // true
}
}
```

## Preventing exceptions

When the [`reject`](../readme.md#optionsreject) option is `false`, the `error` is returned instead.

```js
const resultOrError = await execa`npm run build`;
if (resultOrError.failed) {
console.error(resultOrError);
}
```

## Exit code

The subprocess fails when its [exit code](https://en.wikipedia.org/wiki/Exit_status) is not `0`. The exit code is available as [`error.exitCode`](../readme.md#resultexitcode). It is `undefined` when the subprocess fails to spawn or when it was [terminated by a signal](termination.md#signal-termination).

```js
try {
await execa`npm run build`;
} catch (error) {
// Either non-0 integer or undefined
console.error(error.exitCode);
}
```

## Failure reason

The subprocess can fail for other reasons. Each reason can be detected using a specific boolean property:
- [`error.isTerminated`](../readme.md#resultisterminated): [signal termination](termination.md#signal-termination), including [`subprocess.kill()`](termination.md#signal-termination) and the [`cancelSignal`](termination.md#canceling) option.
- [`error.timedOut`](../readme.md#resulttimedout): [`timeout`](termination.md#timeout) option.
- [`error.isCanceled`](../readme.md#resultiscanceled): [`cancelSignal`](termination.md#canceling) option.
- [`error.isMaxBuffer`](../readme.md#resultismaxbuffer): [`maxBuffer`](output.md#big-output) option.
- Otherwise, it could not be spawned because of either:
- The command's binary not found.
- An invalid argument or [option](../readme.md#options) was passed.
- Not enough memory or too many subprocesses.

```js
try {
await execa`npm run build`;
} catch (error) {
if (error.timedOut) {
handleTimeout(error);
}

throw error
}
```

## Error message

For better debugging, [`error.message`](../readme.md#errormessage) includes both:
- The command and the [reason it failed](#failure-reason).
- Its [`stdout`, `stderr`](output.md#stdout-and-stderr) and other [file descriptor' output](output.md#additional-file-descriptors), separated with newlines and not interleaved.

[`error.shortMessage`](../readme.md#errorshortmessage) is the same but without `stdout`/`stderr`. [`error.originalMessage`](../readme.md#errororiginalmessage) is the same but also without the command.

```js
try {
await execa`npm run build`;
} catch (error) {
console.error(error.originalMessage);
// The task "build" does not exist.

console.error(error.shortMessage);
// Command failed with exit code 3: npm run build
// The task "build" does not exist.

console.error(error.message);
// Command failed with exit code 3: npm run build
// The task "build" does not exist.
// [stderr contents...]
// [stdout contents...]
}
```

## Retry on error

Safely handle failures by using automatic retries and exponential backoff with the [`p-retry`](https://github.com/sindresorhus/p-retry) package.

```js
import pRetry from 'p-retry';
import {execa} from 'execa';

const run = () => execa`curl -sSL https://sindresorhus.com/unicorn`;
console.log(await pRetry(run, {retries: 5}));
```

<hr>

[**Next**: 🏁 Termination](termination.md)\
[**Previous**: 🌐 Environment](environment.md)\
[**Top**: Table of contents](../readme.md#documentation)

0 comments on commit 66a183e

Please sign in to comment.