@optique/run 1.2.9 → 1.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const node_fs = require_rolldown_runtime.__toESM(require("node:fs"));
3
+ const node_process = require_rolldown_runtime.__toESM(require("node:process"));
4
+
5
+ //#region src/output.ts
6
+ let sleepCell;
7
+ function sleepSync(ms) {
8
+ sleepCell ??= new Int32Array(new SharedArrayBuffer(4));
9
+ Atomics.wait(sleepCell, 0, 0, ms);
10
+ }
11
+ function getErrorCode(error) {
12
+ return typeof error === "object" && error != null && "code" in error ? error.code : void 0;
13
+ }
14
+ /**
15
+ * Writes all of the given bytes to a file descriptor synchronously.
16
+ *
17
+ * Partial writes resume from the next unwritten byte. When the descriptor
18
+ * is non-blocking and temporarily full (`EAGAIN`/`EWOULDBLOCK`), this sleeps
19
+ * briefly and retries, blocking the current thread until the reader catches
20
+ * up. When the reader has closed the pipe (`EPIPE`) or reset the socket
21
+ * (`ECONNRESET`), the rest of the output cannot be delivered, so this returns
22
+ * silently.
23
+ *
24
+ * @param fd The file descriptor to write to.
25
+ * @param bytes The bytes to write.
26
+ * @param deps Dependencies to use instead of the defaults, for testing.
27
+ * @throws {Error} If writing fails for any other reason, or if a write makes
28
+ * no progress.
29
+ * @internal
30
+ */
31
+ function writeAllSync(fd, bytes, deps = {}) {
32
+ const writeSync = deps.writeSync ?? node_fs.writeSync;
33
+ const sleep = deps.sleep ?? sleepSync;
34
+ let offset = 0;
35
+ while (offset < bytes.length) {
36
+ let written;
37
+ try {
38
+ written = writeSync(fd, bytes, offset, bytes.length - offset);
39
+ } catch (error) {
40
+ const code = getErrorCode(error);
41
+ if (code === "EAGAIN" || code === "EWOULDBLOCK") {
42
+ sleep(1);
43
+ continue;
44
+ }
45
+ if (code === "EINTR") continue;
46
+ if (code === "EPIPE" || code === "ECONNRESET") return;
47
+ throw error;
48
+ }
49
+ if (written <= 0) throw new Error(`Failed to write output to file descriptor ${fd}: no bytes were written.`);
50
+ offset += written;
51
+ }
52
+ }
53
+ /**
54
+ * Reads the encoding a writable stream applies to strings, which
55
+ * `setDefaultEncoding()` changes. Node.js and Bun expose it only through
56
+ * the stream's internal state, so this returns `undefined` when that is not
57
+ * available.
58
+ *
59
+ * @param stream The stream to inspect.
60
+ * @returns The default encoding, or `undefined` if it cannot be read.
61
+ * @internal
62
+ */
63
+ function getDefaultEncoding(stream) {
64
+ if (!("_writableState" in stream)) return void 0;
65
+ const state = stream._writableState;
66
+ return typeof state === "object" && state != null && "defaultEncoding" in state ? state.defaultEncoding : void 0;
67
+ }
68
+ function isUtf8Encoding(encoding) {
69
+ return encoding === void 0 || typeof encoding === "string" && ["utf8", "utf-8"].includes(encoding.toLowerCase());
70
+ }
71
+ /**
72
+ * Decides whether output for the given stream should bypass the stream and
73
+ * be written directly to its file descriptor.
74
+ *
75
+ * TTYs keep going through the stream, since terminal writes are synchronous
76
+ * on POSIX and Windows consoles need the stream's own handling. Windows is
77
+ * excluded altogether. Streams without a file descriptor, such as
78
+ * `process.stdout` in a Node.js worker thread, also keep going through the
79
+ * stream, since writing to fd 1 or 2 would bypass the worker's redirection.
80
+ * Deno is excluded as well: its streams do not lose queued output on exit,
81
+ * and its `fs.writeSync()` can write part of the buffer to a non-blocking
82
+ * pipe and then throw `EAGAIN` without reporting how much it wrote, which
83
+ * would make retrying duplicate output. Finally, streams whose default
84
+ * encoding is not UTF-8 keep going through the stream, since direct writes
85
+ * always encode the text as UTF-8.
86
+ *
87
+ * @param stream The stream to inspect.
88
+ * @param platform The value of `process.platform`.
89
+ * @param deno Whether the current runtime is Deno.
90
+ * @returns `true` if output should be written directly to `stream.fd`.
91
+ * @internal
92
+ */
93
+ function shouldWriteDirectly(stream, platform, deno) {
94
+ const fd = stream.fd;
95
+ return !deno && platform !== "win32" && !stream.isTTY && typeof fd === "number" && Number.isInteger(fd) && fd >= 0 && isUtf8Encoding(stream.defaultEncoding);
96
+ }
97
+ const encoder = new TextEncoder();
98
+ /**
99
+ * Writes text to standard output or standard error such that, when the
100
+ * stream is a pipe or a file, the text has been completely handed to the
101
+ * operating system by the time this returns (unless the reader closed the
102
+ * pipe). Otherwise, it falls back to the stream's `write()` method.
103
+ *
104
+ * Note that output queued in the stream by earlier `write()` calls is not
105
+ * flushed, and that text written directly can overtake it.
106
+ *
107
+ * @param streamName The stream to write to.
108
+ * @param text The text to write.
109
+ * @throws {Error} If writing to the file descriptor fails.
110
+ * @internal
111
+ */
112
+ function writeOutput(streamName, text) {
113
+ const stream = node_process.default[streamName];
114
+ const streamLike = {
115
+ isTTY: stream.isTTY,
116
+ fd: stream.fd,
117
+ defaultEncoding: getDefaultEncoding(stream)
118
+ };
119
+ if (shouldWriteDirectly(streamLike, node_process.default.platform, "Deno" in globalThis)) writeAllSync(stream.fd, encoder.encode(text));
120
+ else stream.write(text);
121
+ }
122
+
123
+ //#endregion
124
+ exports.getDefaultEncoding = getDefaultEncoding;
125
+ exports.shouldWriteDirectly = shouldWriteDirectly;
126
+ exports.writeAllSync = writeAllSync;
127
+ exports.writeOutput = writeOutput;
@@ -0,0 +1,98 @@
1
+ //#region src/output.d.ts
2
+ /**
3
+ * Dependencies of {@link writeAllSync}, injectable for testing.
4
+ * @internal
5
+ */
6
+ interface WriteAllSyncDeps {
7
+ /**
8
+ * Writes bytes to a file descriptor and returns how many bytes were
9
+ * written.
10
+ * @default `writeSync()` from `node:fs`
11
+ */
12
+ readonly writeSync?: (fd: number, buffer: Uint8Array, offset: number, length: number) => number;
13
+ /**
14
+ * Blocks the current thread for the given number of milliseconds.
15
+ * @default A sleep based on `Atomics.wait()`
16
+ */
17
+ readonly sleep?: (ms: number) => void;
18
+ }
19
+ /**
20
+ * Writes all of the given bytes to a file descriptor synchronously.
21
+ *
22
+ * Partial writes resume from the next unwritten byte. When the descriptor
23
+ * is non-blocking and temporarily full (`EAGAIN`/`EWOULDBLOCK`), this sleeps
24
+ * briefly and retries, blocking the current thread until the reader catches
25
+ * up. When the reader has closed the pipe (`EPIPE`) or reset the socket
26
+ * (`ECONNRESET`), the rest of the output cannot be delivered, so this returns
27
+ * silently.
28
+ *
29
+ * @param fd The file descriptor to write to.
30
+ * @param bytes The bytes to write.
31
+ * @param deps Dependencies to use instead of the defaults, for testing.
32
+ * @throws {Error} If writing fails for any other reason, or if a write makes
33
+ * no progress.
34
+ * @internal
35
+ */
36
+ declare function writeAllSync(fd: number, bytes: Uint8Array, deps?: WriteAllSyncDeps): void;
37
+ /**
38
+ * The subset of a writable stdio stream that {@link shouldWriteDirectly}
39
+ * inspects.
40
+ * @internal
41
+ */
42
+ interface StdioStreamLike {
43
+ readonly isTTY?: boolean;
44
+ readonly fd?: unknown;
45
+ /** The encoding the stream applies to strings, if known. */
46
+ readonly defaultEncoding?: unknown;
47
+ }
48
+ /**
49
+ * Reads the encoding a writable stream applies to strings, which
50
+ * `setDefaultEncoding()` changes. Node.js and Bun expose it only through
51
+ * the stream's internal state, so this returns `undefined` when that is not
52
+ * available.
53
+ *
54
+ * @param stream The stream to inspect.
55
+ * @returns The default encoding, or `undefined` if it cannot be read.
56
+ * @internal
57
+ */
58
+ declare function getDefaultEncoding(stream: object): unknown;
59
+ /**
60
+ * Decides whether output for the given stream should bypass the stream and
61
+ * be written directly to its file descriptor.
62
+ *
63
+ * TTYs keep going through the stream, since terminal writes are synchronous
64
+ * on POSIX and Windows consoles need the stream's own handling. Windows is
65
+ * excluded altogether. Streams without a file descriptor, such as
66
+ * `process.stdout` in a Node.js worker thread, also keep going through the
67
+ * stream, since writing to fd 1 or 2 would bypass the worker's redirection.
68
+ * Deno is excluded as well: its streams do not lose queued output on exit,
69
+ * and its `fs.writeSync()` can write part of the buffer to a non-blocking
70
+ * pipe and then throw `EAGAIN` without reporting how much it wrote, which
71
+ * would make retrying duplicate output. Finally, streams whose default
72
+ * encoding is not UTF-8 keep going through the stream, since direct writes
73
+ * always encode the text as UTF-8.
74
+ *
75
+ * @param stream The stream to inspect.
76
+ * @param platform The value of `process.platform`.
77
+ * @param deno Whether the current runtime is Deno.
78
+ * @returns `true` if output should be written directly to `stream.fd`.
79
+ * @internal
80
+ */
81
+ declare function shouldWriteDirectly(stream: StdioStreamLike, platform: string, deno: boolean): boolean;
82
+ /**
83
+ * Writes text to standard output or standard error such that, when the
84
+ * stream is a pipe or a file, the text has been completely handed to the
85
+ * operating system by the time this returns (unless the reader closed the
86
+ * pipe). Otherwise, it falls back to the stream's `write()` method.
87
+ *
88
+ * Note that output queued in the stream by earlier `write()` calls is not
89
+ * flushed, and that text written directly can overtake it.
90
+ *
91
+ * @param streamName The stream to write to.
92
+ * @param text The text to write.
93
+ * @throws {Error} If writing to the file descriptor fails.
94
+ * @internal
95
+ */
96
+ declare function writeOutput(streamName: "stdout" | "stderr", text: string): void;
97
+ //#endregion
98
+ export { StdioStreamLike, WriteAllSyncDeps, getDefaultEncoding, shouldWriteDirectly, writeAllSync, writeOutput };
@@ -0,0 +1,98 @@
1
+ //#region src/output.d.ts
2
+ /**
3
+ * Dependencies of {@link writeAllSync}, injectable for testing.
4
+ * @internal
5
+ */
6
+ interface WriteAllSyncDeps {
7
+ /**
8
+ * Writes bytes to a file descriptor and returns how many bytes were
9
+ * written.
10
+ * @default `writeSync()` from `node:fs`
11
+ */
12
+ readonly writeSync?: (fd: number, buffer: Uint8Array, offset: number, length: number) => number;
13
+ /**
14
+ * Blocks the current thread for the given number of milliseconds.
15
+ * @default A sleep based on `Atomics.wait()`
16
+ */
17
+ readonly sleep?: (ms: number) => void;
18
+ }
19
+ /**
20
+ * Writes all of the given bytes to a file descriptor synchronously.
21
+ *
22
+ * Partial writes resume from the next unwritten byte. When the descriptor
23
+ * is non-blocking and temporarily full (`EAGAIN`/`EWOULDBLOCK`), this sleeps
24
+ * briefly and retries, blocking the current thread until the reader catches
25
+ * up. When the reader has closed the pipe (`EPIPE`) or reset the socket
26
+ * (`ECONNRESET`), the rest of the output cannot be delivered, so this returns
27
+ * silently.
28
+ *
29
+ * @param fd The file descriptor to write to.
30
+ * @param bytes The bytes to write.
31
+ * @param deps Dependencies to use instead of the defaults, for testing.
32
+ * @throws {Error} If writing fails for any other reason, or if a write makes
33
+ * no progress.
34
+ * @internal
35
+ */
36
+ declare function writeAllSync(fd: number, bytes: Uint8Array, deps?: WriteAllSyncDeps): void;
37
+ /**
38
+ * The subset of a writable stdio stream that {@link shouldWriteDirectly}
39
+ * inspects.
40
+ * @internal
41
+ */
42
+ interface StdioStreamLike {
43
+ readonly isTTY?: boolean;
44
+ readonly fd?: unknown;
45
+ /** The encoding the stream applies to strings, if known. */
46
+ readonly defaultEncoding?: unknown;
47
+ }
48
+ /**
49
+ * Reads the encoding a writable stream applies to strings, which
50
+ * `setDefaultEncoding()` changes. Node.js and Bun expose it only through
51
+ * the stream's internal state, so this returns `undefined` when that is not
52
+ * available.
53
+ *
54
+ * @param stream The stream to inspect.
55
+ * @returns The default encoding, or `undefined` if it cannot be read.
56
+ * @internal
57
+ */
58
+ declare function getDefaultEncoding(stream: object): unknown;
59
+ /**
60
+ * Decides whether output for the given stream should bypass the stream and
61
+ * be written directly to its file descriptor.
62
+ *
63
+ * TTYs keep going through the stream, since terminal writes are synchronous
64
+ * on POSIX and Windows consoles need the stream's own handling. Windows is
65
+ * excluded altogether. Streams without a file descriptor, such as
66
+ * `process.stdout` in a Node.js worker thread, also keep going through the
67
+ * stream, since writing to fd 1 or 2 would bypass the worker's redirection.
68
+ * Deno is excluded as well: its streams do not lose queued output on exit,
69
+ * and its `fs.writeSync()` can write part of the buffer to a non-blocking
70
+ * pipe and then throw `EAGAIN` without reporting how much it wrote, which
71
+ * would make retrying duplicate output. Finally, streams whose default
72
+ * encoding is not UTF-8 keep going through the stream, since direct writes
73
+ * always encode the text as UTF-8.
74
+ *
75
+ * @param stream The stream to inspect.
76
+ * @param platform The value of `process.platform`.
77
+ * @param deno Whether the current runtime is Deno.
78
+ * @returns `true` if output should be written directly to `stream.fd`.
79
+ * @internal
80
+ */
81
+ declare function shouldWriteDirectly(stream: StdioStreamLike, platform: string, deno: boolean): boolean;
82
+ /**
83
+ * Writes text to standard output or standard error such that, when the
84
+ * stream is a pipe or a file, the text has been completely handed to the
85
+ * operating system by the time this returns (unless the reader closed the
86
+ * pipe). Otherwise, it falls back to the stream's `write()` method.
87
+ *
88
+ * Note that output queued in the stream by earlier `write()` calls is not
89
+ * flushed, and that text written directly can overtake it.
90
+ *
91
+ * @param streamName The stream to write to.
92
+ * @param text The text to write.
93
+ * @throws {Error} If writing to the file descriptor fails.
94
+ * @internal
95
+ */
96
+ declare function writeOutput(streamName: "stdout" | "stderr", text: string): void;
97
+ //#endregion
98
+ export { StdioStreamLike, WriteAllSyncDeps, getDefaultEncoding, shouldWriteDirectly, writeAllSync, writeOutput };
package/dist/output.js ADDED
@@ -0,0 +1,123 @@
1
+ import { writeSync } from "node:fs";
2
+ import process from "node:process";
3
+
4
+ //#region src/output.ts
5
+ let sleepCell;
6
+ function sleepSync(ms) {
7
+ sleepCell ??= new Int32Array(new SharedArrayBuffer(4));
8
+ Atomics.wait(sleepCell, 0, 0, ms);
9
+ }
10
+ function getErrorCode(error) {
11
+ return typeof error === "object" && error != null && "code" in error ? error.code : void 0;
12
+ }
13
+ /**
14
+ * Writes all of the given bytes to a file descriptor synchronously.
15
+ *
16
+ * Partial writes resume from the next unwritten byte. When the descriptor
17
+ * is non-blocking and temporarily full (`EAGAIN`/`EWOULDBLOCK`), this sleeps
18
+ * briefly and retries, blocking the current thread until the reader catches
19
+ * up. When the reader has closed the pipe (`EPIPE`) or reset the socket
20
+ * (`ECONNRESET`), the rest of the output cannot be delivered, so this returns
21
+ * silently.
22
+ *
23
+ * @param fd The file descriptor to write to.
24
+ * @param bytes The bytes to write.
25
+ * @param deps Dependencies to use instead of the defaults, for testing.
26
+ * @throws {Error} If writing fails for any other reason, or if a write makes
27
+ * no progress.
28
+ * @internal
29
+ */
30
+ function writeAllSync(fd, bytes, deps = {}) {
31
+ const writeSync$1 = deps.writeSync ?? writeSync;
32
+ const sleep = deps.sleep ?? sleepSync;
33
+ let offset = 0;
34
+ while (offset < bytes.length) {
35
+ let written;
36
+ try {
37
+ written = writeSync$1(fd, bytes, offset, bytes.length - offset);
38
+ } catch (error) {
39
+ const code = getErrorCode(error);
40
+ if (code === "EAGAIN" || code === "EWOULDBLOCK") {
41
+ sleep(1);
42
+ continue;
43
+ }
44
+ if (code === "EINTR") continue;
45
+ if (code === "EPIPE" || code === "ECONNRESET") return;
46
+ throw error;
47
+ }
48
+ if (written <= 0) throw new Error(`Failed to write output to file descriptor ${fd}: no bytes were written.`);
49
+ offset += written;
50
+ }
51
+ }
52
+ /**
53
+ * Reads the encoding a writable stream applies to strings, which
54
+ * `setDefaultEncoding()` changes. Node.js and Bun expose it only through
55
+ * the stream's internal state, so this returns `undefined` when that is not
56
+ * available.
57
+ *
58
+ * @param stream The stream to inspect.
59
+ * @returns The default encoding, or `undefined` if it cannot be read.
60
+ * @internal
61
+ */
62
+ function getDefaultEncoding(stream) {
63
+ if (!("_writableState" in stream)) return void 0;
64
+ const state = stream._writableState;
65
+ return typeof state === "object" && state != null && "defaultEncoding" in state ? state.defaultEncoding : void 0;
66
+ }
67
+ function isUtf8Encoding(encoding) {
68
+ return encoding === void 0 || typeof encoding === "string" && ["utf8", "utf-8"].includes(encoding.toLowerCase());
69
+ }
70
+ /**
71
+ * Decides whether output for the given stream should bypass the stream and
72
+ * be written directly to its file descriptor.
73
+ *
74
+ * TTYs keep going through the stream, since terminal writes are synchronous
75
+ * on POSIX and Windows consoles need the stream's own handling. Windows is
76
+ * excluded altogether. Streams without a file descriptor, such as
77
+ * `process.stdout` in a Node.js worker thread, also keep going through the
78
+ * stream, since writing to fd 1 or 2 would bypass the worker's redirection.
79
+ * Deno is excluded as well: its streams do not lose queued output on exit,
80
+ * and its `fs.writeSync()` can write part of the buffer to a non-blocking
81
+ * pipe and then throw `EAGAIN` without reporting how much it wrote, which
82
+ * would make retrying duplicate output. Finally, streams whose default
83
+ * encoding is not UTF-8 keep going through the stream, since direct writes
84
+ * always encode the text as UTF-8.
85
+ *
86
+ * @param stream The stream to inspect.
87
+ * @param platform The value of `process.platform`.
88
+ * @param deno Whether the current runtime is Deno.
89
+ * @returns `true` if output should be written directly to `stream.fd`.
90
+ * @internal
91
+ */
92
+ function shouldWriteDirectly(stream, platform, deno) {
93
+ const fd = stream.fd;
94
+ return !deno && platform !== "win32" && !stream.isTTY && typeof fd === "number" && Number.isInteger(fd) && fd >= 0 && isUtf8Encoding(stream.defaultEncoding);
95
+ }
96
+ const encoder = new TextEncoder();
97
+ /**
98
+ * Writes text to standard output or standard error such that, when the
99
+ * stream is a pipe or a file, the text has been completely handed to the
100
+ * operating system by the time this returns (unless the reader closed the
101
+ * pipe). Otherwise, it falls back to the stream's `write()` method.
102
+ *
103
+ * Note that output queued in the stream by earlier `write()` calls is not
104
+ * flushed, and that text written directly can overtake it.
105
+ *
106
+ * @param streamName The stream to write to.
107
+ * @param text The text to write.
108
+ * @throws {Error} If writing to the file descriptor fails.
109
+ * @internal
110
+ */
111
+ function writeOutput(streamName, text) {
112
+ const stream = process[streamName];
113
+ const streamLike = {
114
+ isTTY: stream.isTTY,
115
+ fd: stream.fd,
116
+ defaultEncoding: getDefaultEncoding(stream)
117
+ };
118
+ if (shouldWriteDirectly(streamLike, process.platform, "Deno" in globalThis)) writeAllSync(stream.fd, encoder.encode(text));
119
+ else stream.write(text);
120
+ }
121
+
122
+ //#endregion
123
+ export { getDefaultEncoding, shouldWriteDirectly, writeAllSync, writeOutput };
package/dist/print.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const require_output = require('./output.cjs');
2
3
  const node_process = require_rolldown_runtime.__toESM(require("node:process"));
3
4
  const __optique_core_message = require_rolldown_runtime.__toESM(require("@optique/core/message"));
4
5
 
@@ -40,7 +41,7 @@ function printError(message, options = {}) {
40
41
  const stream = options.stream ?? "stderr";
41
42
  const output = node_process.default[stream];
42
43
  const quotes = options.quotes ?? !output.isTTY;
43
- const printer = createPrinter({
44
+ const format = createFormatter({
44
45
  stream,
45
46
  colors: options.colors,
46
47
  quotes,
@@ -50,8 +51,12 @@ function printError(message, options = {}) {
50
51
  type: "text",
51
52
  text: "Error: "
52
53
  }, ...message];
53
- printer(errorMessage);
54
- if (options.exitCode != null) node_process.default.exit(options.exitCode);
54
+ const text = format(errorMessage) + "\n";
55
+ if (options.exitCode == null) output.write(text);
56
+ else {
57
+ require_output.writeOutput(stream, text);
58
+ node_process.default.exit(options.exitCode);
59
+ }
55
60
  }
56
61
  /**
57
62
  * Creates a custom printer function with predefined formatting options.
@@ -82,17 +87,23 @@ function printError(message, options = {}) {
82
87
  */
83
88
  function createPrinter(options = {}) {
84
89
  const stream = options.stream ?? "stdout";
85
- const output = node_process.default[stream];
90
+ const format = createFormatter(options);
91
+ return (message) => {
92
+ node_process.default[stream].write(format(message) + "\n");
93
+ };
94
+ }
95
+ /**
96
+ * Creates a function that formats messages for the given stream, resolving
97
+ * terminal-dependent defaults once, at creation time.
98
+ */
99
+ function createFormatter(options) {
100
+ const output = node_process.default[options.stream ?? "stdout"];
86
101
  const formatOptions = {
87
102
  colors: options.colors ?? output.isTTY,
88
103
  quotes: options.quotes,
89
104
  maxWidth: options.maxWidth ?? output.columns
90
105
  };
91
- return (message) => {
92
- const formatted = (0, __optique_core_message.formatMessage)(message, formatOptions);
93
- if (stream === "stderr") node_process.default.stderr.write(formatted + "\n");
94
- else node_process.default.stdout.write(formatted + "\n");
95
- };
106
+ return (message) => (0, __optique_core_message.formatMessage)(message, formatOptions);
96
107
  }
97
108
 
98
109
  //#endregion
package/dist/print.d.cts CHANGED
@@ -26,6 +26,12 @@ interface PrintErrorOptions extends PrintOptions {
26
26
  /**
27
27
  * Exit code to use when exiting the process.
28
28
  * If specified, the process will exit with this code after printing the error.
29
+ *
30
+ * When the output stream is a pipe or a file on POSIX systems under Node.js
31
+ * or Bun, the error message is written directly to its file descriptor, and
32
+ * the process exits only after the whole message has been written, even if
33
+ * the reader is slow. Output that earlier calls queued in the stream is not
34
+ * flushed.
29
35
  */
30
36
  readonly exitCode?: number;
31
37
  }
@@ -80,6 +86,9 @@ declare function print(message: Message, options?: PrintOptions): void;
80
86
  *
81
87
  * @param message The structured error message to print.
82
88
  * @param options Optional formatting options and exit code.
89
+ * @throws {Error} If `exitCode` is specified and writing the message directly
90
+ * to the output stream's file descriptor fails for a reason other
91
+ * than the reader having gone away (for example, `ENOSPC`).
83
92
  *
84
93
  * @example
85
94
  * ```typescript
package/dist/print.d.ts CHANGED
@@ -26,6 +26,12 @@ interface PrintErrorOptions extends PrintOptions {
26
26
  /**
27
27
  * Exit code to use when exiting the process.
28
28
  * If specified, the process will exit with this code after printing the error.
29
+ *
30
+ * When the output stream is a pipe or a file on POSIX systems under Node.js
31
+ * or Bun, the error message is written directly to its file descriptor, and
32
+ * the process exits only after the whole message has been written, even if
33
+ * the reader is slow. Output that earlier calls queued in the stream is not
34
+ * flushed.
29
35
  */
30
36
  readonly exitCode?: number;
31
37
  }
@@ -80,6 +86,9 @@ declare function print(message: Message, options?: PrintOptions): void;
80
86
  *
81
87
  * @param message The structured error message to print.
82
88
  * @param options Optional formatting options and exit code.
89
+ * @throws {Error} If `exitCode` is specified and writing the message directly
90
+ * to the output stream's file descriptor fails for a reason other
91
+ * than the reader having gone away (for example, `ENOSPC`).
83
92
  *
84
93
  * @example
85
94
  * ```typescript
package/dist/print.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { writeOutput } from "./output.js";
1
2
  import process from "node:process";
2
3
  import { formatMessage } from "@optique/core/message";
3
4
 
@@ -39,7 +40,7 @@ function printError(message$1, options = {}) {
39
40
  const stream = options.stream ?? "stderr";
40
41
  const output = process[stream];
41
42
  const quotes = options.quotes ?? !output.isTTY;
42
- const printer = createPrinter({
43
+ const format = createFormatter({
43
44
  stream,
44
45
  colors: options.colors,
45
46
  quotes,
@@ -49,8 +50,12 @@ function printError(message$1, options = {}) {
49
50
  type: "text",
50
51
  text: "Error: "
51
52
  }, ...message$1];
52
- printer(errorMessage);
53
- if (options.exitCode != null) process.exit(options.exitCode);
53
+ const text$1 = format(errorMessage) + "\n";
54
+ if (options.exitCode == null) output.write(text$1);
55
+ else {
56
+ writeOutput(stream, text$1);
57
+ process.exit(options.exitCode);
58
+ }
54
59
  }
55
60
  /**
56
61
  * Creates a custom printer function with predefined formatting options.
@@ -81,17 +86,23 @@ function printError(message$1, options = {}) {
81
86
  */
82
87
  function createPrinter(options = {}) {
83
88
  const stream = options.stream ?? "stdout";
84
- const output = process[stream];
89
+ const format = createFormatter(options);
90
+ return (message$1) => {
91
+ process[stream].write(format(message$1) + "\n");
92
+ };
93
+ }
94
+ /**
95
+ * Creates a function that formats messages for the given stream, resolving
96
+ * terminal-dependent defaults once, at creation time.
97
+ */
98
+ function createFormatter(options) {
99
+ const output = process[options.stream ?? "stdout"];
85
100
  const formatOptions = {
86
101
  colors: options.colors ?? output.isTTY,
87
102
  quotes: options.quotes,
88
103
  maxWidth: options.maxWidth ?? output.columns
89
104
  };
90
- return (message$1) => {
91
- const formatted = formatMessage(message$1, formatOptions);
92
- if (stream === "stderr") process.stderr.write(formatted + "\n");
93
- else process.stdout.write(formatted + "\n");
94
- };
105
+ return (message$1) => formatMessage(message$1, formatOptions);
95
106
  }
96
107
 
97
108
  //#endregion
package/dist/run.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const require_output = require('./output.cjs');
2
3
  const __optique_core_facade = require_rolldown_runtime.__toESM(require("@optique/core/facade"));
3
- const node_path = require_rolldown_runtime.__toESM(require("node:path"));
4
4
  const node_process = require_rolldown_runtime.__toESM(require("node:process"));
5
+ const node_path = require_rolldown_runtime.__toESM(require("node:path"));
5
6
 
6
7
  //#region src/run.ts
7
8
  function getProgramHelpMetadata(metadata) {
@@ -66,10 +67,10 @@ function buildCoreOptions(options, programMetadata) {
66
67
  const programName = options.programName ?? node_path.default.basename(node_process.default.argv[1] ?? "cli");
67
68
  const args = options.args ?? node_process.default.argv.slice(2);
68
69
  const stdout = options.stdout ?? ((line) => {
69
- node_process.default.stdout.write(`${line}\n`);
70
+ require_output.writeOutput("stdout", `${line}\n`);
70
71
  });
71
72
  const stderr = options.stderr ?? ((line) => {
72
- node_process.default.stderr.write(`${line}\n`);
73
+ require_output.writeOutput("stderr", `${line}\n`);
73
74
  });
74
75
  const onExit = options.onExit ?? ((exitCode) => node_process.default.exit(exitCode));
75
76
  const colors = options.colors ?? node_process.default.stdout.isTTY;
package/dist/run.d.cts CHANGED
@@ -28,6 +28,14 @@ interface RunOptions {
28
28
  * Function used to output help and usage messages. Assumes it prints the
29
29
  * ending newline.
30
30
  *
31
+ * When `process.stdout` is a pipe or a file on POSIX systems under Node.js or
32
+ * Bun, the default writes directly to its file descriptor and returns only
33
+ * after the whole text has been written, even if the reader is slow, so that
34
+ * exiting the process right afterward does not truncate the output.
35
+ *
36
+ * A custom writer using `console.log()` can lose large piped output on
37
+ * affected Bun versions. See the Bun output warning on {@link run}.
38
+ *
31
39
  * @default Writes to `process.stdout` with a trailing newline
32
40
  */
33
41
  readonly stdout?: (text: string) => void;
@@ -35,12 +43,32 @@ interface RunOptions {
35
43
  * Function used to output error messages. Assumes it prints the ending
36
44
  * newline.
37
45
  *
46
+ * When `process.stderr` is a pipe or a file on POSIX systems under Node.js or
47
+ * Bun, the default writes directly to its file descriptor and returns only
48
+ * after the whole text has been written, even if the reader is slow, so that
49
+ * exiting the process right afterward does not truncate the output.
50
+ *
51
+ * A custom writer using `console.error()` can lose large piped output on
52
+ * affected Bun versions. See the Bun output warning on {@link run}.
53
+ *
38
54
  * @default Writes to `process.stderr` with a trailing newline
39
55
  */
40
56
  readonly stderr?: (text: string) => void;
41
57
  /**
42
58
  * Function used to exit the process on help/version display or parse error.
43
59
  *
60
+ * This hook is synchronous and runs immediately after an output handler
61
+ * returns. With the default `process.exit()` handler, custom `stdout` and
62
+ * `stderr` handlers must finish writing synchronously before returning.
63
+ * The default exit handler also does not wait for writes queued by
64
+ * application code.
65
+ *
66
+ * To wait for asynchronous writes, record their completion promises and
67
+ * throw an exception carrying the exit code from `onExit`. Catch that
68
+ * exception outside the runner, await the recorded promises, then call
69
+ * `process.exit()` with that code.
70
+ * An arbitrary delay does not guarantee that output has been flushed.
71
+ *
44
72
  * @default `process.exit`
45
73
  */
46
74
  readonly onExit?: (exitCode: number) => never;
@@ -306,12 +334,34 @@ type AcceptExactOptionalRunOptions<TOptions> = [TOptions] extends [RunOptions |
306
334
  * - Exit the process with appropriate codes on help or error
307
335
  * - Format output according to terminal capabilities
308
336
  *
337
+ * On affected Bun versions, importing `@optique/run` can cause large
338
+ * application `console.log()` output to be silently truncated when piped,
339
+ * even after successful parsing and natural process exit. This is
340
+ * [Bun issue #36419](https://github.com/oven-sh/bun/issues/36419), reported
341
+ * on macOS arm64 with Bun 1.3.14 and 1.4.2. Use `process.stdout.write()`
342
+ * instead of `console.log()` for application data, and `process.stderr.write()`
343
+ * instead of `console.error()` for diagnostics. Both console methods can lose
344
+ * large piped output. Let the process exit naturally. If explicit termination
345
+ * is necessary, wait for the completion callbacks of all pending stdout/stderr
346
+ * writes before calling `process.exit()`; a delay is not a flush guarantee.
347
+ * Waiting after `console.log()` or `console.error()` cannot recover dropped
348
+ * bytes.
349
+ *
350
+ * Setting `colors` and `maxWidth` explicitly does not avoid this Bun bug:
351
+ * importing `node:process`, which this module does, was enough to trigger it
352
+ * in Bun 1.3.14. The upstream fix was merged in
353
+ * [Bun PR #43868](https://github.com/oven-sh/bun/pull/43868); check whether
354
+ * your Bun release includes it.
355
+ *
309
356
  * @template T The parser type being executed.
310
357
  * @param parser The command-line parser to execute.
311
358
  * @param options Configuration options for customizing behavior.
312
359
  * See {@link RunOptions} for available settings.
313
360
  * @returns The parsed result if successful. On help display or parse errors,
314
361
  * the function will call `process.exit()` and not return.
362
+ * @throws {Error} If the default `stdout` or `stderr` writer fails to write
363
+ * to the stream's file descriptor for a reason other than the reader
364
+ * having gone away (for example, `ENOSPC`).
315
365
  *
316
366
  * @example
317
367
  * ```typescript
@@ -386,12 +436,17 @@ declare function run<T extends Parser<Mode, unknown, unknown>>(parser: T, option
386
436
  * Use this when you know your parser is sync-only to get direct return values
387
437
  * without Promise wrappers.
388
438
  *
439
+ * See {@link run} for Bun's piped console output warning and workarounds.
440
+ *
389
441
  * @template T The sync parser type being executed.
390
442
  * @param parser The synchronous command-line parser to execute.
391
443
  * @param options Configuration options for customizing behavior.
392
444
  * @returns The parsed result if successful.
393
445
  * @throws {TypeError} If an async parser (or a {@link Program} wrapping one)
394
446
  * is passed at runtime. Use {@link run} or {@link runAsync} instead.
447
+ * @throws {Error} If the default `stdout` or `stderr` writer fails to write
448
+ * to the stream's file descriptor for a reason other than the reader
449
+ * having gone away (for example, `ENOSPC`).
395
450
  * @since 0.9.0
396
451
  */
397
452
  declare function runSync<T extends Parser<"sync", unknown, unknown>, TContexts extends readonly SourceContext<unknown>[]>(parser: T, options: RunOptions & {
@@ -411,10 +466,15 @@ declare function runSync<T extends Parser<"sync", unknown, unknown>>(parser: T,
411
466
  * Promise. Use this when working with parsers that may contain async
412
467
  * value parsers.
413
468
  *
469
+ * See {@link run} for Bun's piped console output warning and workarounds.
470
+ *
414
471
  * @template T The parser type being executed.
415
472
  * @param parser The command-line parser to execute.
416
473
  * @param options Configuration options for customizing behavior.
417
474
  * @returns A Promise of the parsed result if successful.
475
+ * @throws {Error} If the default `stdout` or `stderr` writer fails to write
476
+ * to the stream's file descriptor for a reason other than the reader
477
+ * having gone away (for example, `ENOSPC`).
418
478
  * @since 0.9.0
419
479
  */
420
480
  declare function runAsync<T extends Parser<Mode, unknown, unknown>, TContexts extends readonly SourceContext<unknown>[]>(parser: T, options: RunOptions & {
package/dist/run.d.ts CHANGED
@@ -28,6 +28,14 @@ interface RunOptions {
28
28
  * Function used to output help and usage messages. Assumes it prints the
29
29
  * ending newline.
30
30
  *
31
+ * When `process.stdout` is a pipe or a file on POSIX systems under Node.js or
32
+ * Bun, the default writes directly to its file descriptor and returns only
33
+ * after the whole text has been written, even if the reader is slow, so that
34
+ * exiting the process right afterward does not truncate the output.
35
+ *
36
+ * A custom writer using `console.log()` can lose large piped output on
37
+ * affected Bun versions. See the Bun output warning on {@link run}.
38
+ *
31
39
  * @default Writes to `process.stdout` with a trailing newline
32
40
  */
33
41
  readonly stdout?: (text: string) => void;
@@ -35,12 +43,32 @@ interface RunOptions {
35
43
  * Function used to output error messages. Assumes it prints the ending
36
44
  * newline.
37
45
  *
46
+ * When `process.stderr` is a pipe or a file on POSIX systems under Node.js or
47
+ * Bun, the default writes directly to its file descriptor and returns only
48
+ * after the whole text has been written, even if the reader is slow, so that
49
+ * exiting the process right afterward does not truncate the output.
50
+ *
51
+ * A custom writer using `console.error()` can lose large piped output on
52
+ * affected Bun versions. See the Bun output warning on {@link run}.
53
+ *
38
54
  * @default Writes to `process.stderr` with a trailing newline
39
55
  */
40
56
  readonly stderr?: (text: string) => void;
41
57
  /**
42
58
  * Function used to exit the process on help/version display or parse error.
43
59
  *
60
+ * This hook is synchronous and runs immediately after an output handler
61
+ * returns. With the default `process.exit()` handler, custom `stdout` and
62
+ * `stderr` handlers must finish writing synchronously before returning.
63
+ * The default exit handler also does not wait for writes queued by
64
+ * application code.
65
+ *
66
+ * To wait for asynchronous writes, record their completion promises and
67
+ * throw an exception carrying the exit code from `onExit`. Catch that
68
+ * exception outside the runner, await the recorded promises, then call
69
+ * `process.exit()` with that code.
70
+ * An arbitrary delay does not guarantee that output has been flushed.
71
+ *
44
72
  * @default `process.exit`
45
73
  */
46
74
  readonly onExit?: (exitCode: number) => never;
@@ -306,12 +334,34 @@ type AcceptExactOptionalRunOptions<TOptions> = [TOptions] extends [RunOptions |
306
334
  * - Exit the process with appropriate codes on help or error
307
335
  * - Format output according to terminal capabilities
308
336
  *
337
+ * On affected Bun versions, importing `@optique/run` can cause large
338
+ * application `console.log()` output to be silently truncated when piped,
339
+ * even after successful parsing and natural process exit. This is
340
+ * [Bun issue #36419](https://github.com/oven-sh/bun/issues/36419), reported
341
+ * on macOS arm64 with Bun 1.3.14 and 1.4.2. Use `process.stdout.write()`
342
+ * instead of `console.log()` for application data, and `process.stderr.write()`
343
+ * instead of `console.error()` for diagnostics. Both console methods can lose
344
+ * large piped output. Let the process exit naturally. If explicit termination
345
+ * is necessary, wait for the completion callbacks of all pending stdout/stderr
346
+ * writes before calling `process.exit()`; a delay is not a flush guarantee.
347
+ * Waiting after `console.log()` or `console.error()` cannot recover dropped
348
+ * bytes.
349
+ *
350
+ * Setting `colors` and `maxWidth` explicitly does not avoid this Bun bug:
351
+ * importing `node:process`, which this module does, was enough to trigger it
352
+ * in Bun 1.3.14. The upstream fix was merged in
353
+ * [Bun PR #43868](https://github.com/oven-sh/bun/pull/43868); check whether
354
+ * your Bun release includes it.
355
+ *
309
356
  * @template T The parser type being executed.
310
357
  * @param parser The command-line parser to execute.
311
358
  * @param options Configuration options for customizing behavior.
312
359
  * See {@link RunOptions} for available settings.
313
360
  * @returns The parsed result if successful. On help display or parse errors,
314
361
  * the function will call `process.exit()` and not return.
362
+ * @throws {Error} If the default `stdout` or `stderr` writer fails to write
363
+ * to the stream's file descriptor for a reason other than the reader
364
+ * having gone away (for example, `ENOSPC`).
315
365
  *
316
366
  * @example
317
367
  * ```typescript
@@ -386,12 +436,17 @@ declare function run<T extends Parser<Mode, unknown, unknown>>(parser: T, option
386
436
  * Use this when you know your parser is sync-only to get direct return values
387
437
  * without Promise wrappers.
388
438
  *
439
+ * See {@link run} for Bun's piped console output warning and workarounds.
440
+ *
389
441
  * @template T The sync parser type being executed.
390
442
  * @param parser The synchronous command-line parser to execute.
391
443
  * @param options Configuration options for customizing behavior.
392
444
  * @returns The parsed result if successful.
393
445
  * @throws {TypeError} If an async parser (or a {@link Program} wrapping one)
394
446
  * is passed at runtime. Use {@link run} or {@link runAsync} instead.
447
+ * @throws {Error} If the default `stdout` or `stderr` writer fails to write
448
+ * to the stream's file descriptor for a reason other than the reader
449
+ * having gone away (for example, `ENOSPC`).
395
450
  * @since 0.9.0
396
451
  */
397
452
  declare function runSync<T extends Parser<"sync", unknown, unknown>, TContexts extends readonly SourceContext<unknown>[]>(parser: T, options: RunOptions & {
@@ -411,10 +466,15 @@ declare function runSync<T extends Parser<"sync", unknown, unknown>>(parser: T,
411
466
  * Promise. Use this when working with parsers that may contain async
412
467
  * value parsers.
413
468
  *
469
+ * See {@link run} for Bun's piped console output warning and workarounds.
470
+ *
414
471
  * @template T The parser type being executed.
415
472
  * @param parser The command-line parser to execute.
416
473
  * @param options Configuration options for customizing behavior.
417
474
  * @returns A Promise of the parsed result if successful.
475
+ * @throws {Error} If the default `stdout` or `stderr` writer fails to write
476
+ * to the stream's file descriptor for a reason other than the reader
477
+ * having gone away (for example, `ENOSPC`).
418
478
  * @since 0.9.0
419
479
  */
420
480
  declare function runAsync<T extends Parser<Mode, unknown, unknown>, TContexts extends readonly SourceContext<unknown>[]>(parser: T, options: RunOptions & {
package/dist/run.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { writeOutput } from "./output.js";
1
2
  import { runParser, runWith, runWithSync } from "@optique/core/facade";
2
- import path from "node:path";
3
3
  import process from "node:process";
4
+ import path from "node:path";
4
5
 
5
6
  //#region src/run.ts
6
7
  function getProgramHelpMetadata(metadata) {
@@ -65,10 +66,10 @@ function buildCoreOptions(options, programMetadata) {
65
66
  const programName = options.programName ?? path.basename(process.argv[1] ?? "cli");
66
67
  const args = options.args ?? process.argv.slice(2);
67
68
  const stdout = options.stdout ?? ((line) => {
68
- process.stdout.write(`${line}\n`);
69
+ writeOutput("stdout", `${line}\n`);
69
70
  });
70
71
  const stderr = options.stderr ?? ((line) => {
71
- process.stderr.write(`${line}\n`);
72
+ writeOutput("stderr", `${line}\n`);
72
73
  });
73
74
  const onExit = options.onExit ?? ((exitCode) => process.exit(exitCode));
74
75
  const colors = options.colors ?? process.stdout.isTTY;
@@ -1,8 +1,8 @@
1
1
  const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const node_fs = require_rolldown_runtime.__toESM(require("node:fs"));
2
3
  const node_path = require_rolldown_runtime.__toESM(require("node:path"));
3
4
  const __optique_core_message = require_rolldown_runtime.__toESM(require("@optique/core/message"));
4
5
  const __optique_core_nonempty = require_rolldown_runtime.__toESM(require("@optique/core/nonempty"));
5
- const node_fs = require_rolldown_runtime.__toESM(require("node:fs"));
6
6
 
7
7
  //#region src/valueparser.ts
8
8
  /**
@@ -1,7 +1,7 @@
1
+ import { existsSync, statSync } from "node:fs";
1
2
  import { basename, dirname, extname } from "node:path";
2
3
  import { message, text } from "@optique/core/message";
3
4
  import { ensureNonEmptyString } from "@optique/core/nonempty";
4
- import { existsSync, statSync } from "node:fs";
5
5
 
6
6
  //#region src/valueparser.ts
7
7
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/run",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -76,11 +76,11 @@
76
76
  },
77
77
  "sideEffects": false,
78
78
  "dependencies": {
79
- "@optique/core": "1.2.9"
79
+ "@optique/core": "1.2.11"
80
80
  },
81
81
  "devDependencies": {
82
- "@optique/config": "1.2.9",
83
- "@optique/env": "1.2.9",
82
+ "@optique/config": "1.2.11",
83
+ "@optique/env": "1.2.11",
84
84
  "@types/node": "^24.0.0",
85
85
  "fast-check": "^4.7.0",
86
86
  "tsdown": "^0.13.0",