Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
56 changes: 56 additions & 0 deletions docs/concepts/runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,62 @@ try {
}
~~~~

### Piped application output on Bun

On affected Bun versions, importing *@optique/run* can cause large
`console.log()` output to be silently truncated when piped to another process.
Parsing can succeed and the application can exit naturally with code `0` while
still losing output. [Bun issue #36419] reports this behavior on macOS arm64
with Bun 1.3.14 and 1.4.2.
Comment thread
dahlia marked this conversation as resolved.
Resolved

Accessing `process.stdout` makes the pipe nonblocking, and Bun's native console
writer can drop the unwritten remainder when the pipe is full. In tests with
Bun 1.3.14, importing `node:process` alone was enough to trigger the problem.
*@optique/run* imports that module and reads stdout's terminal capabilities.
Setting `colors` and `maxWidth` explicitly to skip TTY detection does not avoid
the import's effect.

Write application data through `process.stdout.write()` instead of
`console.log()`, then let the process exit naturally:

~~~~ typescript twoslash
import { object } from "@optique/core/constructs";
import { run } from "@optique/run";
import process from "node:process";

const parser = object({});
const result = run(parser);
process.stdout.write(`${JSON.stringify(result)}\n`);
~~~~

If you must call `process.exit()`, wait for the completion callbacks of all
pending stdout/stderr writes first. For example, when this is the only pending
write:

~~~~ typescript twoslash
import process from "node:process";

const result = { data: "x".repeat(200_000) };
await new Promise<void>((resolve, reject) => {
process.stdout.write(`${JSON.stringify(result)}\n`, (error) => {
if (error != null) reject(error);
else resolve();
});
});
process.exit(0);
~~~~

An arbitrary delay is not a flush guarantee. Waiting after `console.log()`
cannot recover bytes that Bun has already dropped. These workarounds apply to
application output and custom output handlers; Optique's default writers have
the behavior described in [Error handling behavior](#error-handling-behavior).

The upstream fix was merged in [Bun PR #43868]. Check whether your Bun release
includes it before relying on piped console output.

[Bun issue #36419]: https://github.com/oven-sh/bun/issues/36419
[Bun PR #43868]: https://github.com/oven-sh/bun/pull/43868


Async parser execution
----------------------
Expand Down
29 changes: 29 additions & 0 deletions packages/run/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export interface RunOptions {
* after the whole text has been written, even if the reader is slow, so that
* exiting the process right afterward does not truncate the output.
*
* A custom writer using `console.log()` can lose large piped output on
* affected Bun versions. See the Bun output warning on {@link run}.
*
* @default Writes to `process.stdout` with a trailing newline
*/
readonly stdout?: (text: string) => void;
Expand All @@ -72,6 +75,12 @@ export interface RunOptions {
/**
* Function used to exit the process on help/version display or parse error.
*
* The default `process.exit()` handler does not wait for writes queued by
* application code or custom output handlers. Before calling
* `process.exit()`, wait for the completion callbacks of pending
* `process.stdout.write()` and `process.stderr.write()` calls. An arbitrary
* delay does not guarantee that output has been flushed.
Comment thread
dahlia marked this conversation as resolved.
Resolved
*
* @default `process.exit`
*/
readonly onExit?: (exitCode: number) => never;
Expand Down Expand Up @@ -435,6 +444,22 @@ function resolveProgramInput<
* - Exit the process with appropriate codes on help or error
* - Format output according to terminal capabilities
*
* On affected Bun versions, importing `@optique/run` can cause large
* application `console.log()` output to be silently truncated when piped,
* even after successful parsing and natural process exit. This is
* [Bun issue #36419](https://github.com/oven-sh/bun/issues/36419), reported
* on macOS arm64 with Bun 1.3.14 and 1.4.2. Write application data with
* `process.stdout.write()` and let the process exit naturally. If explicit
* termination is necessary, wait for the completion callbacks of all pending
* stdout/stderr writes before calling `process.exit()`; a delay is not a
* flush guarantee. Waiting after `console.log()` cannot recover dropped bytes.
*
* Setting `colors` and `maxWidth` explicitly does not avoid this Bun bug:
* importing `node:process`, which this module does, was enough to trigger it
* in Bun 1.3.14. The upstream fix was merged in
* [Bun PR #43868](https://github.com/oven-sh/bun/pull/43868); check whether
* your Bun release includes it.
*
* @template T The parser type being executed.
* @param parser The command-line parser to execute.
* @param options Configuration options for customizing behavior.
Expand Down Expand Up @@ -618,6 +643,8 @@ export function run<T extends Parser<Mode, unknown, unknown>>(
* Use this when you know your parser is sync-only to get direct return values
* without Promise wrappers.
*
* See {@link run} for Bun's piped console output warning and workarounds.
*
* @template T The sync parser type being executed.
* @param parser The synchronous command-line parser to execute.
* @param options Configuration options for customizing behavior.
Expand Down Expand Up @@ -732,6 +759,8 @@ export function runSync<T extends Parser<"sync", unknown, unknown>>(
* Promise. Use this when working with parsers that may contain async
* value parsers.
*
* See {@link run} for Bun's piped console output warning and workarounds.
*
* @template T The parser type being executed.
* @param parser The command-line parser to execute.
* @param options Configuration options for customizing behavior.
Expand Down
Loading