printable-shell-command 5.0.7 → 5.1.0
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.
- package/dist/lib/printable-shell-command/index.js +30 -8
- package/dist/lib/printable-shell-command/index.js.map +2 -2
- package/dist/lib/printable-shell-command/trimTrailingNewlines.d.ts +4 -2
- package/package.json +1 -1
- package/src/PrintableShellCommand.test.ts +52 -1
- package/src/PrintableShellCommand.ts +46 -11
- package/src/trimTrailingNewlines.ts +9 -4
|
@@ -34,12 +34,15 @@ async function handleTrailingNewlines(text, options) {
|
|
|
34
34
|
function wrapHandleTrailingNewlines(fn) {
|
|
35
35
|
return (options) => handleTrailingNewlines(fn(), options);
|
|
36
36
|
}
|
|
37
|
-
function
|
|
38
|
-
const
|
|
37
|
+
function wrapHandleTrailingNewlinesForResponder(v) {
|
|
38
|
+
const response = v.response();
|
|
39
|
+
const originalTextFn = response.text.bind(response);
|
|
39
40
|
return wrapHandleTrailingNewlines(originalTextFn);
|
|
40
41
|
}
|
|
41
42
|
function wrapHandleTrailingNewlinesForResponse(response) {
|
|
42
|
-
const textFn =
|
|
43
|
+
const textFn = wrapHandleTrailingNewlinesForResponder({
|
|
44
|
+
response: () => response
|
|
45
|
+
});
|
|
43
46
|
Object.defineProperty(response, "text", {
|
|
44
47
|
get: () => {
|
|
45
48
|
return textFn;
|
|
@@ -76,6 +79,10 @@ var ARGUMENT_LINE_WRAPPING_VALUES = [
|
|
|
76
79
|
"by-argument",
|
|
77
80
|
"inline"
|
|
78
81
|
];
|
|
82
|
+
function NO_COLOR() {
|
|
83
|
+
const { env } = globalThis.process.getBuiltinModule("node:process");
|
|
84
|
+
return !!env["NO_COLOR"];
|
|
85
|
+
}
|
|
79
86
|
var SPECIAL_SHELL_CHARACTERS = /* @__PURE__ */ new Set([
|
|
80
87
|
" ",
|
|
81
88
|
'"',
|
|
@@ -231,13 +238,27 @@ var PrintableShellCommand = class {
|
|
|
231
238
|
*/
|
|
232
239
|
print(options) {
|
|
233
240
|
const stream = options?.stream ?? stderr;
|
|
234
|
-
const optionsCopy = {
|
|
235
|
-
|
|
241
|
+
const optionsCopy = {
|
|
242
|
+
...options,
|
|
243
|
+
style: this.#styleFromOptions(stream, options)
|
|
244
|
+
};
|
|
236
245
|
const writable = stream instanceof Writable ? stream : Writable.fromWeb(stream);
|
|
237
246
|
writable.write(this.getPrintableCommand(optionsCopy));
|
|
238
247
|
writable.write("\n");
|
|
239
248
|
return this;
|
|
240
249
|
}
|
|
250
|
+
#styleFromOptions(stream, options) {
|
|
251
|
+
if (options?.autoStyle === "never") {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!stream.isTTY) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (NO_COLOR()) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
return TTY_AUTO_STYLE;
|
|
261
|
+
}
|
|
241
262
|
#stdinSource;
|
|
242
263
|
/**
|
|
243
264
|
* Send data to `stdin` of the subprocess.
|
|
@@ -275,9 +296,10 @@ var PrintableShellCommand = class {
|
|
|
275
296
|
...options,
|
|
276
297
|
cwd
|
|
277
298
|
});
|
|
299
|
+
let cachedSuccess;
|
|
278
300
|
Object.defineProperty(subprocess, "success", {
|
|
279
301
|
get() {
|
|
280
|
-
return new Promise((resolve, reject) => {
|
|
302
|
+
return cachedSuccess ??= new Promise((resolve, reject) => {
|
|
281
303
|
function handle(exitCode) {
|
|
282
304
|
if (exitCode === 0) {
|
|
283
305
|
resolve();
|
|
@@ -309,7 +331,7 @@ var PrintableShellCommand = class {
|
|
|
309
331
|
new Response(Readable.from(this.#generator(s.stdout, s.success)))
|
|
310
332
|
)
|
|
311
333
|
);
|
|
312
|
-
s.stdout.text =
|
|
334
|
+
s.stdout.text = wrapHandleTrailingNewlinesForResponder(s.stdout);
|
|
313
335
|
const thisCached = this;
|
|
314
336
|
s.stdout.text0 = async function* () {
|
|
315
337
|
yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));
|
|
@@ -325,7 +347,7 @@ var PrintableShellCommand = class {
|
|
|
325
347
|
new Response(Readable.from(this.#generator(s.stderr, s.success)))
|
|
326
348
|
)
|
|
327
349
|
);
|
|
328
|
-
s.stderr.text =
|
|
350
|
+
s.stderr.text = wrapHandleTrailingNewlinesForResponder(s.stderr);
|
|
329
351
|
const thisCached = this;
|
|
330
352
|
s.stderr.text0 = async function* () {
|
|
331
353
|
yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/PrintableShellCommand.ts", "../../../src/trimTrailingNewlines.ts"],
|
|
4
|
-
"sourcesContent": ["import assert from \"node:assert\";\nimport type {\n ChildProcessByStdio,\n ChildProcess as NodeChildProcess,\n SpawnOptions as NodeSpawnOptions,\n} from \"node:child_process\";\nimport { createReadStream } from \"node:fs\";\nimport { stderr } from \"node:process\";\nimport { Readable, Writable } from \"node:stream\";\nimport type { WriteStream } from \"node:tty\";\nimport { styleText } from \"node:util\";\nimport { Path, stringifyIfPath } from \"path-class\";\nimport type {\n NodeWithCwd,\n spawnType,\n WithStderrResponse,\n WithStdoutResponse,\n WithSuccess,\n} from \"./spawn\";\nimport {\n handleTrailingNewlines,\n type TrailingNewlineOptions,\n wrapHandleTrailingNewlinesForResponse,\n wrapHandleTrailingNewlinesForText,\n} from \"./trimTrailingNewlines\";\n\n// TODO: does this import work?\n/**\n * @import { stdout } from \"node:process\"\n */\n\nconst DEFAULT_MAIN_INDENTATION = \"\";\nconst DEFAULT_ARG_INDENTATION = \" \";\nconst DEFAULT_ARGUMENT_LINE_WRAPPING = \"by-entry\";\n\nconst INLINE_SEPARATOR = \" \";\nconst LINE_WRAP_LINE_END = \" \\\\\\n\";\n\ntype StyleTextFormat = Parameters<typeof styleText>[0];\n\nconst TTY_AUTO_STYLE: StyleTextFormat = [\"gray\", \"bold\"];\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type nere.\nfunction isString(s: any): s is string {\n return typeof s === \"string\";\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type here.\nfunction isValidArgsEntryArray(entries: any[]): entries is SingleArgument[] {\n for (const entry of entries) {\n if (isString(entry)) {\n continue;\n }\n if (entry instanceof Path) {\n continue;\n }\n return false;\n }\n return true;\n}\n\n// TODO: allow `.toString()`ables?\ntype SingleArgument = string | Path;\ntype ArgsEntry = SingleArgument | SingleArgument[];\ntype Args = ArgsEntry[];\n\nconst ARGUMENT_LINE_WRAPPING_VALUES = [\n \"by-entry\",\n \"nested-by-entry\",\n \"by-argument\",\n \"inline\",\n] as const;\ntype ArgumentLineWrapping = (typeof ARGUMENT_LINE_WRAPPING_VALUES)[number];\n\nexport interface PrintOptions {\n /** Defaults to \"\" */\n mainIndentation?: string;\n /** Defaults to \" \" */\n argIndentation?: string;\n /**\n * - `\"auto\"`: Quote only arguments that need it for safety. This tries to be\n * portable and safe across shells, but true safety and portability is hard\n * to guarantee.\n * - `\"extra-safe\"`: Quote all arguments, even ones that don't need it. This is\n * more likely to be safe under all circumstances.\n */\n quoting?: \"auto\" | \"extra-safe\";\n /** Line wrapping to use between arguments. Defaults to `\"by-entry\"`. */\n argumentLineWrapping?: ArgumentLineWrapping;\n /** Include the first arg (or first arg group) on the same line as the command, regardless of the `argumentLineWrapping` setting. */\n skipLineWrapBeforeFirstArg?: true | false;\n /**\n * Style text using `node`'s {@link styleText | `styleText(\u2026)`}.\n *\n * Example usage:\n *\n * ```\n * new PrintableShellCommand(\"echo\", [\"hi\"]).print({\n * style: [\"green\", \"underline\"],\n * });\n * */\n style?: StyleTextFormat;\n}\n\nexport interface StreamPrintOptions extends PrintOptions {\n /**\n * Auto-style the text when:\n *\n * - the output stream is detected to be a TTY\n * - `styleTextFormat` is not specified.\n *\n * The current auto style is: `[\"gray\", \"bold\"]`\n */\n autoStyle?: \"tty\" | \"never\";\n // This would be a `WritableStream` (open web standard), but `WriteStream` allows us to query `.isTTY`.\n stream?: WriteStream | Writable;\n}\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS = new Set([\n \" \",\n '\"',\n \"'\",\n \"`\",\n \"|\",\n \"$\",\n \"*\",\n \"?\",\n \">\",\n \"<\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"{\",\n \"}\",\n \"&\",\n \"\\\\\",\n \";\",\n \"#\",\n]);\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND =\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (SPECIAL_SHELL_CHARACTERS as unknown as any).union(new Set([\"=\"]));\n\n// TODO: Is there an idiomatic ways to check that all potential fields of\n// `StdinSource` satisfy `(typeof STDIN_SOURCE_KEYS)[number]`, without adding\n// extra indirection for type wrangling?\nconst STDIN_SOURCE_KEYS = [\"text\", \"json\", \"path\", \"stream\"] as const;\nexport type StdinSource =\n | { text: string }\n // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON data.\n | { json: any }\n | { path: string | Path }\n | { stream: Readable | ReadableStream };\n\nexport class PrintableShellCommand {\n #commandName: string | Path;\n constructor(\n commandName: string | Path,\n private args: Args = [],\n ) {\n if (!isString(commandName) && !(commandName instanceof Path)) {\n // biome-ignore lint/suspicious/noExplicitAny: We want to print this, no matter what it is.\n throw new Error(\"Command name is not a string:\", commandName as any);\n }\n this.#commandName = commandName;\n if (typeof args === \"undefined\") {\n return;\n }\n if (!Array.isArray(args)) {\n throw new Error(\"Command arguments are not an array\");\n }\n for (let i = 0; i < args.length; i++) {\n const argEntry = args[i];\n if (typeof argEntry === \"string\") {\n continue;\n }\n if (argEntry instanceof Path) {\n continue;\n }\n if (Array.isArray(argEntry) && isValidArgsEntryArray(argEntry)) {\n continue;\n }\n throw new Error(`Invalid arg entry at index: ${i}`);\n }\n }\n\n get commandName(): string {\n return stringifyIfPath(this.#commandName);\n }\n\n /**\n * For use with `node:child_process`\n *\n * Usage example:\n *\n * ```\n * import { PrintableShellCommand } from \"printable-shell-command\";\n * import { spawn } from \"node:child_process\";\n *\n * const command = new PrintableShellCommand( \u2026 );\n * const child_process = spawn(...command.toCommandWithFlatArgs()); // Note the `...`\n * ```\n *\n */\n public toCommandWithFlatArgs(): [string, string[]] {\n return [this.commandName, this.args.flat().map(stringifyIfPath)];\n }\n\n #mainIndentation(options: PrintOptions): string {\n return options?.mainIndentation ?? DEFAULT_MAIN_INDENTATION;\n }\n\n #argIndentation(options: PrintOptions): string {\n return (\n this.#mainIndentation(options) +\n (options?.argIndentation ?? DEFAULT_ARG_INDENTATION)\n );\n }\n\n #lineWrapSeparator(options: PrintOptions): string {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n\n #argPairSeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\": {\n return INLINE_SEPARATOR;\n }\n case \"nested-by-entry\": {\n return this.#lineWrapSeparator(options) + this.#argIndentation(options);\n }\n case \"by-argument\": {\n return this.#lineWrapSeparator(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #intraEntrySeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\":\n case \"nested-by-entry\":\n case \"by-argument\": {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #separatorAfterCommand(\n options: PrintOptions,\n numFollowingEntries: number,\n ): string {\n if (numFollowingEntries === 0) {\n return \"\";\n }\n if (options.skipLineWrapBeforeFirstArg ?? false) {\n return INLINE_SEPARATOR;\n }\n return this.#intraEntrySeparator(options);\n }\n\n public getPrintableCommand(options?: PrintOptions): string {\n // TODO: Why in the world does TypeScript not give the `options` arg the type of `PrintOptions | undefined`???\n options ??= {};\n const serializedEntries: string[] = [];\n\n for (let i = 0; i < this.args.length; i++) {\n const argsEntry = stringifyIfPath(this.args[i]);\n\n if (isString(argsEntry)) {\n serializedEntries.push(escapeArg(argsEntry, false, options));\n } else {\n serializedEntries.push(\n argsEntry\n .map((part) => escapeArg(stringifyIfPath(part), false, options))\n .join(this.#argPairSeparator(options)),\n );\n }\n }\n\n let text =\n this.#mainIndentation(options) +\n escapeArg(this.commandName, true, options) +\n this.#separatorAfterCommand(options, serializedEntries.length) +\n serializedEntries.join(this.#intraEntrySeparator(options));\n if (options?.style) {\n text = styleText(options.style, text);\n }\n return text;\n }\n\n /**\n * Print the shell command to {@link stderr} (default) or a specified stream.\n *\n * By default, this will be auto-styled (as bold gray) when `.isTTY` is true\n * for the stream. `.isTTY` is populated for the {@link stderr} and\n * {@link stdout} objects. Pass `\"autoStyle\": \"never\"` or an explicit\n * `style` to disable this.\n *\n */\n public print(options?: StreamPrintOptions): PrintableShellCommand {\n const stream = options?.stream ?? stderr;\n // Note: we only need to modify top-level fields, so `structuredClone(\u2026)`\n // would be overkill and can only cause performance issues.\n const optionsCopy = { ...options };\n optionsCopy.style ??=\n options?.autoStyle !== \"never\" &&\n (stream as { isTTY?: boolean }).isTTY === true\n ? TTY_AUTO_STYLE\n : undefined;\n const writable =\n stream instanceof Writable ? stream : Writable.fromWeb(stream);\n writable.write(this.getPrintableCommand(optionsCopy));\n writable.write(\"\\n\");\n return this;\n }\n\n #stdinSource: StdinSource | undefined;\n /**\n * Send data to `stdin` of the subprocess.\n *\n * Note that this will overwrite:\n *\n * - Any previous value set using {@link PrintableShellCommand.stdin | `.stdin(\u2026)`}.\n * - Any value set for `stdin` using the `\"stdio\"` field of {@link PrintableShellCommand.spawn | `.spawn(\u2026)`}.\n */\n stdin(source: StdinSource): PrintableShellCommand {\n const [key, ...moreKeys] = Object.keys(source);\n assert.equal(moreKeys.length, 0);\n // TODO: validate values?\n assert((STDIN_SOURCE_KEYS as unknown as string[]).includes(key));\n\n this.#stdinSource = source;\n return this;\n }\n\n /**\n * The returned child process includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313\n */\n public spawn: typeof spawnType = ((\n options?: Parameters<typeof spawnType>[0],\n ) => {\n const { spawn } = process.getBuiltinModule(\"node:child_process\");\n const cwd = stringifyIfPath(options?.cwd);\n options = { ...options };\n if (this.#stdinSource) {\n options ??= {};\n if (typeof options.stdio === \"undefined\") {\n options.stdio = \"pipe\";\n }\n if (typeof options.stdio === \"string\") {\n options.stdio = new Array(3).fill(options.stdio);\n }\n options.stdio = [\"pipe\", ...options.stdio.slice(1)];\n }\n // biome-ignore lint/suspicious/noTsIgnore: We don't want linting to depend on *broken* type checking.\n // @ts-ignore: The TypeScript checker has trouble reconciling the optional (i.e. potentially `undefined`) `options` with the third argument.\n const subprocess = spawn(...this.toCommandWithFlatArgs(), {\n ...(options as object),\n cwd,\n }) as NodeChildProcess & {\n success: Promise<void>;\n };\n // TODO: define properties on prototypes instead.\n Object.defineProperty(subprocess, \"success\", {\n get() {\n return new Promise<void>((resolve, reject) => {\n function handle(exitCode: number) {\n if (exitCode === 0) {\n resolve();\n } else {\n reject(`Command failed with non-zero exit code: ${exitCode}`);\n }\n }\n if (subprocess.exitCode !== null) {\n handle(subprocess.exitCode);\n }\n this.addListener(\n \"exit\",\n /* we only use the first arg */\n handle,\n );\n // biome-ignore lint/suspicious/noExplicitAny: We don't have the type available.\n this.addListener(\"error\", (err: any) => {\n reject(err);\n });\n });\n },\n enumerable: false,\n });\n if (subprocess.stdout) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStdoutResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stdout.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#generator(s.stdout, s.success))),\n ));\n s.stdout.text = wrapHandleTrailingNewlinesForText(s.stdout.response());\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stdout.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));\n };\n s.stdout.json = <T>() => s.stdout.response().json() as Promise<T>;\n }\n if (subprocess.stderr) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStderrResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stderr.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#generator(s.stderr, s.success))),\n ));\n s.stderr.text = wrapHandleTrailingNewlinesForText(s.stderr.response());\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stderr.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));\n };\n s.stderr.json = <T>() => s.stderr.response().json() as Promise<T>;\n }\n if (this.#stdinSource) {\n const { stdin } = subprocess;\n assert(stdin);\n if (\"text\" in this.#stdinSource) {\n stdin.write(this.#stdinSource.text);\n stdin.end();\n } else if (\"json\" in this.#stdinSource) {\n stdin.write(JSON.stringify(this.#stdinSource.json));\n stdin.end();\n } else if (\"path\" in this.#stdinSource) {\n createReadStream(stringifyIfPath(this.#stdinSource.path)).pipe(stdin);\n } else if (\"stream\" in this.#stdinSource) {\n const stream = (() => {\n const { stream } = this.#stdinSource;\n return stream instanceof Readable ? stream : Readable.fromWeb(stream);\n })();\n stream.pipe(stdin);\n } else {\n throw new Error(\"Invalid `.stdin(\u2026)` source?\");\n }\n }\n return subprocess;\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling\n }) as any;\n\n /** A wrapper for `.spawn(\u2026)` that sets stdio to `\"inherit\"` (common for\n * invoking commands from scripts whose output and interaction should be\n * surfaced to the user).\n *\n * If there is no other interaction with the shell from the calling process,\n * then it acts \"transparent\" and allows user to interact with the subprocess\n * in its stead.\n */\n public spawnTransparently(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): ChildProcessByStdio<null, null, null> & WithSuccess {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling.\n return this.spawn({ ...options, stdio: \"inherit\" }) as any;\n }\n\n /**\n * A wrapper for {@link PrintableShellCommand.spawn | `.spawn(\u2026)`} that:\n *\n * - sets `detached` to `true`,\n * - sets stdio to `\"inherit\"`,\n * - calls `.unref()`, and\n * - does not wait for the process to exit.\n *\n * This is similar to starting a command in the background and disowning it (in a shell).\n *\n */\n public spawnDetached(\n options?: NodeWithCwd<Omit<Omit<NodeSpawnOptions, \"stdio\">, \"detached\">>,\n ): void {\n if (options) {\n for (const field of [\"stdio\", \"detached\"]) {\n if (field in options) {\n throw new Error(`Unexpected \\`${field}\\` field.`);\n }\n }\n }\n const childProcess = this.spawn({\n stdio: \"ignore\",\n ...options,\n detached: true,\n });\n childProcess.unref();\n }\n\n #generator(\n readable: Readable,\n successPromise: Promise<void>,\n ): AsyncGenerator<string> {\n // TODO: we'd make this a `ReadableStream`, but `ReadableStream.from(\u2026)` is\n // not implemented in `bun`: https://github.com/oven-sh/bun/issues/3700\n return (async function* () {\n for await (const chunk of readable) {\n yield chunk;\n }\n await successPromise;\n })();\n }\n\n #stdoutSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"pipe\", \"inherit\"],\n });\n return this.#generator(subprocess.stdout, subprocess.success);\n }\n\n public stdout(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response & {\n text: (options?: TrailingNewlineOptions) => Promise<string>;\n } {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#stdoutSpawnGenerator(options))),\n );\n }\n\n #stderrSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"inherit\", \"pipe\"],\n });\n return this.#generator(subprocess.stderr, subprocess.success);\n }\n\n public stderr(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response & {\n text: (options?: TrailingNewlineOptions) => Promise<string>;\n } {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#stderrSpawnGenerator(options))),\n );\n }\n\n async *#split0(generator: AsyncGenerator<string>): AsyncGenerator<string> {\n let pending = \"\";\n for await (const chunk of generator) {\n pending += chunk;\n const newChunks = pending.split(\"\\x00\");\n pending = newChunks.splice(-1)[0];\n yield* newChunks;\n }\n if (pending !== \"\") {\n throw new Error(\n \"Missing a trailing NUL character at the end of a NUL-delimited stream.\",\n );\n }\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).text()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public async text(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">> & {\n trimTrailingNewlines: \"single-required\" | \"single-if-present\" | \"never\";\n },\n ): Promise<string> {\n const {\n trimTrailingNewlines: trimTrailingNewlinesOption,\n ...spawnOptions\n } = options ?? {};\n return handleTrailingNewlines(this.stdout(spawnOptions).text(), {\n trimTrailingNewlines: trimTrailingNewlinesOption,\n });\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).json()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public json<T>(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Promise<T> {\n return this.stdout(options).json() as Promise<T>;\n }\n\n /**\n * Parse `stdout` into a generator of string values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *text0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n yield* this.#split0(this.#stdoutSpawnGenerator(options));\n }\n\n /**\n * Parse `stdout` into a generator of JSON values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *json0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON\n AsyncGenerator<any> {\n for await (const part of this.#split0(\n this.#stdoutSpawnGenerator(options),\n )) {\n yield JSON.parse(part);\n }\n }\n\n /** Equivalent to:\n *\n * ```\n * await this.print(\u2026).spawnTransparently(\u2026).success;\n * ```\n */\n public async shellOut(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">> & {\n print?: StreamPrintOptions | ArgumentLineWrapping | boolean;\n },\n ): Promise<void> {\n const { print: printOptions, ...spawnOptions } = options ?? {};\n\n if (typeof printOptions === \"string\") {\n assert(ARGUMENT_LINE_WRAPPING_VALUES.includes(printOptions));\n this.print({ argumentLineWrapping: printOptions });\n } else if (printOptions === true) {\n this.print();\n } else if (printOptions === false) {\n // no-op\n } else {\n this.print(printOptions);\n }\n await this.spawnTransparently(spawnOptions).success;\n }\n}\n\nexport function escapeArg(\n arg: string,\n isMainCommand: boolean,\n options: PrintOptions,\n): string {\n const argCharacters = new Set(arg);\n const specialShellCharacters = isMainCommand\n ? SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND\n : SPECIAL_SHELL_CHARACTERS;\n if (\n options?.quoting === \"extra-safe\" ||\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (argCharacters as unknown as any).intersection(specialShellCharacters)\n .size > 0\n ) {\n // Use single quote to reduce the need to escape (and therefore reduce the chance for bugs/security issues).\n const escaped = arg.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"'\", \"\\\\'\");\n return `'${escaped}'`;\n }\n return arg;\n}\n", "const DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR = \"never\";\nexport interface TrailingNewlineOptions {\n trimTrailingNewlines?: \"single-required\" | \"single-if-present\" | \"never\";\n}\n\n// TODO: Handle carriage return?\nexport async function handleTrailingNewlines(\n text: Promise<string>,\n options?: TrailingNewlineOptions,\n): Promise<string> {\n const awaitedText = await text;\n switch (\n options?.trimTrailingNewlines ??\n DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR\n ) {\n case \"single-required\": {\n if (!awaitedText.endsWith(\"\\n\")) {\n throw new Error(\"Trailing newline required, but not present.\");\n }\n return awaitedText.slice(0, -1);\n }\n case \"single-if-present\": {\n if (awaitedText.endsWith(\"\\n\")) {\n return awaitedText.slice(0, -1);\n } else {\n return awaitedText;\n }\n }\n case \"never\": {\n return awaitedText;\n }\n default:\n throw new Error(\"Invalid value for `trimTrailingNewlines`.\");\n }\n}\n\nfunction wrapHandleTrailingNewlines(\n fn: () => Promise<string>,\n): (options?: TrailingNewlineOptions) => Promise<string> {\n return (options?: TrailingNewlineOptions) =>\n handleTrailingNewlines(fn(), options);\n}\n\nexport function wrapHandleTrailingNewlinesForText(v: {\n text: () => Promise<string>;\n}): (options?: TrailingNewlineOptions) => Promise<string> {\n const originalTextFn = v.text.bind(v);\n return wrapHandleTrailingNewlines(originalTextFn);\n}\n\nexport function wrapHandleTrailingNewlinesForResponse(\n response: Response,\n): Response & { text: (options?: TrailingNewlineOptions) => Promise<string> } {\n const textFn = wrapHandleTrailingNewlinesForText(response);\n Object.defineProperty(response, \"text\", {\n get: () => {\n return textFn;\n },\n });\n return response;\n}\n"],
|
|
5
|
-
"mappings": ";AAAA,OAAO,YAAY;AAMnB,SAAS,wBAAwB;AACjC,SAAS,cAAc;AACvB,SAAS,UAAU,gBAAgB;AAEnC,SAAS,iBAAiB;AAC1B,SAAS,MAAM,uBAAuB;;;ACXtC,IAAM,0CAA0C;AAMhD,eAAsB,uBACpB,MACA,SACiB;AACjB,QAAM,cAAc,MAAM;AAC1B,UACE,SAAS,wBACT,yCACA;AAAA,IACA,KAAK,mBAAmB;AACtB,UAAI,CAAC,YAAY,SAAS,IAAI,GAAG;AAC/B,cAAM,IAAI,MAAM,6CAA6C;AAAA,MAC/D;AACA,aAAO,YAAY,MAAM,GAAG,EAAE;AAAA,IAChC;AAAA,IACA,KAAK,qBAAqB;AACxB,UAAI,YAAY,SAAS,IAAI,GAAG;AAC9B,eAAO,YAAY,MAAM,GAAG,EAAE;AAAA,MAChC,OAAO;AACL,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,aAAO;AAAA,IACT;AAAA,IACA;AACE,YAAM,IAAI,MAAM,2CAA2C;AAAA,EAC/D;AACF;AAEA,SAAS,2BACP,IACuD;AACvD,SAAO,CAAC,YACN,uBAAuB,GAAG,GAAG,OAAO;AACxC;AAEO,SAAS,
|
|
4
|
+
"sourcesContent": ["import assert from \"node:assert\";\nimport type {\n ChildProcessByStdio,\n ChildProcess as NodeChildProcess,\n SpawnOptions as NodeSpawnOptions,\n} from \"node:child_process\";\nimport { createReadStream } from \"node:fs\";\nimport { stderr } from \"node:process\";\nimport { Readable, Writable } from \"node:stream\";\nimport type { WriteStream } from \"node:tty\";\nimport { styleText } from \"node:util\";\nimport { Path, stringifyIfPath } from \"path-class\";\nimport type {\n NodeWithCwd,\n spawnType,\n WithStderrResponse,\n WithStdoutResponse,\n WithSuccess,\n} from \"./spawn\";\nimport {\n handleTrailingNewlines,\n type TrailingNewlineOptions,\n wrapHandleTrailingNewlinesForResponder,\n wrapHandleTrailingNewlinesForResponse,\n} from \"./trimTrailingNewlines\";\n\n// TODO: does this import work?\n/**\n * @import { stdout } from \"node:process\"\n */\n\nconst DEFAULT_MAIN_INDENTATION = \"\";\nconst DEFAULT_ARG_INDENTATION = \" \";\nconst DEFAULT_ARGUMENT_LINE_WRAPPING = \"by-entry\";\n\nconst INLINE_SEPARATOR = \" \";\nconst LINE_WRAP_LINE_END = \" \\\\\\n\";\n\ntype StyleTextFormat = Parameters<typeof styleText>[0];\n\nconst TTY_AUTO_STYLE: StyleTextFormat = [\"gray\", \"bold\"];\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type nere.\nfunction isString(s: any): s is string {\n return typeof s === \"string\";\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: This is the correct type here.\nfunction isValidArgsEntryArray(entries: any[]): entries is SingleArgument[] {\n for (const entry of entries) {\n if (isString(entry)) {\n continue;\n }\n if (entry instanceof Path) {\n continue;\n }\n return false;\n }\n return true;\n}\n\n// TODO: allow `.toString()`ables?\ntype SingleArgument = string | Path;\ntype ArgsEntry = SingleArgument | SingleArgument[];\ntype Args = ArgsEntry[];\n\nconst ARGUMENT_LINE_WRAPPING_VALUES = [\n \"by-entry\",\n \"nested-by-entry\",\n \"by-argument\",\n \"inline\",\n] as const;\ntype ArgumentLineWrapping = (typeof ARGUMENT_LINE_WRAPPING_VALUES)[number];\n\nexport interface PrintOptions {\n /** Defaults to \"\" */\n mainIndentation?: string;\n /** Defaults to \" \" */\n argIndentation?: string;\n /**\n * - `\"auto\"`: Quote only arguments that need it for safety. This tries to be\n * portable and safe across shells, but true safety and portability is hard\n * to guarantee.\n * - `\"extra-safe\"`: Quote all arguments, even ones that don't need it. This is\n * more likely to be safe under all circumstances.\n */\n quoting?: \"auto\" | \"extra-safe\";\n /** Line wrapping to use between arguments. Defaults to `\"by-entry\"`. */\n argumentLineWrapping?: ArgumentLineWrapping;\n /** Include the first arg (or first arg group) on the same line as the command, regardless of the `argumentLineWrapping` setting. */\n skipLineWrapBeforeFirstArg?: true | false;\n /**\n * Style text using `node`'s {@link styleText | `styleText(\u2026)`}.\n *\n * Example usage:\n *\n * ```\n * new PrintableShellCommand(\"echo\", [\"hi\"]).print({\n * style: [\"green\", \"underline\"],\n * });\n * */\n style?: StyleTextFormat;\n}\n\n/**\n * https://no-color.org/\n *\n * > Command-line software which adds ANSI color to its output by default should\n * > check for a NO_COLOR environment variable that, when present and not an\n * > empty string (regardless of its value), prevents the addition of ANSI\n * > color.\n *\n * I think it's a bit silly that `NO_COLOR=false` and `NO_COLOR=0` count as \"no\n * color please\", but \uD83E\uDD37\n *\n */\nfunction NO_COLOR(): boolean {\n const { env } = globalThis.process.getBuiltinModule(\"node:process\");\n // The empty string is falsy, so we can just use `!!`.\n // biome-ignore lint/complexity/useLiteralKeys: TODO: https://github.com/biomejs/biome/discussions/7572\n return !!env[\"NO_COLOR\"];\n}\n\nexport interface StreamPrintOptions extends PrintOptions {\n /**\n * Auto-style the text when:\n *\n * - the output stream is detected to be a TTY\n * - `styleTextFormat` is not specified.\n *\n * The current auto style is: `[\"gray\", \"bold\"]`\n */\n autoStyle?: \"tty\" | \"never\";\n // This would be a `WritableStream` (open web standard), but `WriteStream` allows us to query `.isTTY`.\n stream?: WriteStream | Writable;\n}\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS = new Set([\n \" \",\n '\"',\n \"'\",\n \"`\",\n \"|\",\n \"$\",\n \"*\",\n \"?\",\n \">\",\n \"<\",\n \"(\",\n \")\",\n \"[\",\n \"]\",\n \"{\",\n \"}\",\n \"&\",\n \"\\\\\",\n \";\",\n \"#\",\n]);\n\n// https://mywiki.wooledge.org/BashGuide/SpecialCharacters\nconst SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND =\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (SPECIAL_SHELL_CHARACTERS as unknown as any).union(new Set([\"=\"]));\n\n// TODO: Is there an idiomatic ways to check that all potential fields of\n// `StdinSource` satisfy `(typeof STDIN_SOURCE_KEYS)[number]`, without adding\n// extra indirection for type wrangling?\nconst STDIN_SOURCE_KEYS = [\"text\", \"json\", \"path\", \"stream\"] as const;\nexport type StdinSource =\n | { text: string }\n // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON data.\n | { json: any }\n | { path: string | Path }\n | { stream: Readable | ReadableStream };\n\nexport class PrintableShellCommand {\n #commandName: string | Path;\n constructor(\n commandName: string | Path,\n private args: Args = [],\n ) {\n if (!isString(commandName) && !(commandName instanceof Path)) {\n // biome-ignore lint/suspicious/noExplicitAny: We want to print this, no matter what it is.\n throw new Error(\"Command name is not a string:\", commandName as any);\n }\n this.#commandName = commandName;\n if (typeof args === \"undefined\") {\n return;\n }\n if (!Array.isArray(args)) {\n throw new Error(\"Command arguments are not an array\");\n }\n for (let i = 0; i < args.length; i++) {\n const argEntry = args[i];\n if (typeof argEntry === \"string\") {\n continue;\n }\n if (argEntry instanceof Path) {\n continue;\n }\n if (Array.isArray(argEntry) && isValidArgsEntryArray(argEntry)) {\n continue;\n }\n throw new Error(`Invalid arg entry at index: ${i}`);\n }\n }\n\n get commandName(): string {\n return stringifyIfPath(this.#commandName);\n }\n\n /**\n * For use with `node:child_process`\n *\n * Usage example:\n *\n * ```\n * import { PrintableShellCommand } from \"printable-shell-command\";\n * import { spawn } from \"node:child_process\";\n *\n * const command = new PrintableShellCommand( \u2026 );\n * const child_process = spawn(...command.toCommandWithFlatArgs()); // Note the `...`\n * ```\n *\n */\n public toCommandWithFlatArgs(): [string, string[]] {\n return [this.commandName, this.args.flat().map(stringifyIfPath)];\n }\n\n #mainIndentation(options: PrintOptions): string {\n return options?.mainIndentation ?? DEFAULT_MAIN_INDENTATION;\n }\n\n #argIndentation(options: PrintOptions): string {\n return (\n this.#mainIndentation(options) +\n (options?.argIndentation ?? DEFAULT_ARG_INDENTATION)\n );\n }\n\n #lineWrapSeparator(options: PrintOptions): string {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n\n #argPairSeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\": {\n return INLINE_SEPARATOR;\n }\n case \"nested-by-entry\": {\n return this.#lineWrapSeparator(options) + this.#argIndentation(options);\n }\n case \"by-argument\": {\n return this.#lineWrapSeparator(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #intraEntrySeparator(options: PrintOptions): string {\n switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {\n case \"by-entry\":\n case \"nested-by-entry\":\n case \"by-argument\": {\n return LINE_WRAP_LINE_END + this.#argIndentation(options);\n }\n case \"inline\": {\n return INLINE_SEPARATOR;\n }\n default:\n throw new Error(\"Invalid argument line wrapping argument.\");\n }\n }\n\n #separatorAfterCommand(\n options: PrintOptions,\n numFollowingEntries: number,\n ): string {\n if (numFollowingEntries === 0) {\n return \"\";\n }\n if (options.skipLineWrapBeforeFirstArg ?? false) {\n return INLINE_SEPARATOR;\n }\n return this.#intraEntrySeparator(options);\n }\n\n public getPrintableCommand(options?: PrintOptions): string {\n // TODO: Why in the world does TypeScript not give the `options` arg the type of `PrintOptions | undefined`???\n options ??= {};\n const serializedEntries: string[] = [];\n\n for (let i = 0; i < this.args.length; i++) {\n const argsEntry = stringifyIfPath(this.args[i]);\n\n if (isString(argsEntry)) {\n serializedEntries.push(escapeArg(argsEntry, false, options));\n } else {\n serializedEntries.push(\n argsEntry\n .map((part) => escapeArg(stringifyIfPath(part), false, options))\n .join(this.#argPairSeparator(options)),\n );\n }\n }\n\n let text =\n this.#mainIndentation(options) +\n escapeArg(this.commandName, true, options) +\n this.#separatorAfterCommand(options, serializedEntries.length) +\n serializedEntries.join(this.#intraEntrySeparator(options));\n if (options?.style) {\n text = styleText(options.style, text);\n }\n return text;\n }\n\n /**\n * Print the shell command to {@link stderr} (default) or a specified stream.\n *\n * By default, this will be auto-styled (as bold gray) when `.isTTY` is true\n * for the stream. `.isTTY` is populated for the {@link stderr} and\n * {@link stdout} objects. Pass `\"autoStyle\": \"never\"` or an explicit\n * `style` to disable this.\n *\n */\n public print(options?: StreamPrintOptions): PrintableShellCommand {\n const stream = options?.stream ?? stderr;\n // Note: we only need to modify top-level fields, so `structuredClone(\u2026)`\n // would be overkill and can only cause performance issues.\n const optionsCopy = {\n ...options,\n style: this.#styleFromOptions(stream, options),\n };\n const writable =\n stream instanceof Writable ? stream : Writable.fromWeb(stream);\n writable.write(this.getPrintableCommand(optionsCopy));\n writable.write(\"\\n\");\n return this;\n }\n\n #styleFromOptions(\n stream: WriteStream | Writable,\n options?: StreamPrintOptions,\n ): StyleTextFormat | undefined {\n if (options?.autoStyle === \"never\") {\n return;\n }\n if (!(stream as { isTTY?: boolean }).isTTY) {\n return;\n }\n if (NO_COLOR()) {\n return;\n }\n return TTY_AUTO_STYLE;\n }\n\n #stdinSource: StdinSource | undefined;\n /**\n * Send data to `stdin` of the subprocess.\n *\n * Note that this will overwrite:\n *\n * - Any previous value set using {@link PrintableShellCommand.stdin | `.stdin(\u2026)`}.\n * - Any value set for `stdin` using the `\"stdio\"` field of {@link PrintableShellCommand.spawn | `.spawn(\u2026)`}.\n */\n stdin(source: StdinSource): PrintableShellCommand {\n const [key, ...moreKeys] = Object.keys(source);\n assert.equal(moreKeys.length, 0);\n // TODO: validate values?\n assert((STDIN_SOURCE_KEYS as unknown as string[]).includes(key));\n\n this.#stdinSource = source;\n return this;\n }\n\n /**\n * The returned child process includes a `.success` `Promise` field, per https://github.com/oven-sh/bun/issues/8313\n */\n public spawn: typeof spawnType = ((\n options?: Parameters<typeof spawnType>[0],\n ) => {\n const { spawn } = process.getBuiltinModule(\"node:child_process\");\n const cwd = stringifyIfPath(options?.cwd);\n options = { ...options };\n if (this.#stdinSource) {\n options ??= {};\n if (typeof options.stdio === \"undefined\") {\n options.stdio = \"pipe\";\n }\n if (typeof options.stdio === \"string\") {\n options.stdio = new Array(3).fill(options.stdio);\n }\n options.stdio = [\"pipe\", ...options.stdio.slice(1)];\n }\n // biome-ignore lint/suspicious/noTsIgnore: We don't want linting to depend on *broken* type checking.\n // @ts-ignore: The TypeScript checker has trouble reconciling the optional (i.e. potentially `undefined`) `options` with the third argument.\n const subprocess = spawn(...this.toCommandWithFlatArgs(), {\n ...(options as object),\n cwd,\n }) as NodeChildProcess & {\n success: Promise<void>;\n };\n let cachedSuccess: Promise<void> | undefined;\n // TODO: define properties on prototypes instead.\n Object.defineProperty(subprocess, \"success\", {\n get() {\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n return (cachedSuccess ??= new Promise<void>((resolve, reject) => {\n function handle(exitCode: number) {\n if (exitCode === 0) {\n resolve();\n } else {\n reject(`Command failed with non-zero exit code: ${exitCode}`);\n }\n }\n if (subprocess.exitCode !== null) {\n handle(subprocess.exitCode);\n }\n this.addListener(\n \"exit\",\n /* we only use the first arg */\n handle,\n );\n // biome-ignore lint/suspicious/noExplicitAny: We don't have the type available.\n this.addListener(\"error\", (err: any) => {\n reject(err);\n });\n }));\n },\n enumerable: false,\n });\n if (subprocess.stdout) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStdoutResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stdout.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#generator(s.stdout, s.success))),\n ));\n s.stdout.text = wrapHandleTrailingNewlinesForResponder(s.stdout);\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stdout.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));\n };\n s.stdout.json = <T>() => s.stdout.response().json() as Promise<T>;\n }\n if (subprocess.stderr) {\n // TODO: dedupe\n const s = subprocess as unknown as Readable &\n WithStderrResponse &\n WithSuccess;\n let cachedResponse: Response | undefined;\n s.stderr.response = () =>\n // biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592\n (cachedResponse ??= wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#generator(s.stderr, s.success))),\n ));\n s.stderr.text = wrapHandleTrailingNewlinesForResponder(s.stderr);\n const thisCached = this; // TODO: make this type-check using `.bind(\u2026)`\n s.stderr.text0 = async function* () {\n yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));\n };\n s.stderr.json = <T>() => s.stderr.response().json() as Promise<T>;\n }\n if (this.#stdinSource) {\n const { stdin } = subprocess;\n assert(stdin);\n if (\"text\" in this.#stdinSource) {\n stdin.write(this.#stdinSource.text);\n stdin.end();\n } else if (\"json\" in this.#stdinSource) {\n stdin.write(JSON.stringify(this.#stdinSource.json));\n stdin.end();\n } else if (\"path\" in this.#stdinSource) {\n createReadStream(stringifyIfPath(this.#stdinSource.path)).pipe(stdin);\n } else if (\"stream\" in this.#stdinSource) {\n const stream = (() => {\n const { stream } = this.#stdinSource;\n return stream instanceof Readable ? stream : Readable.fromWeb(stream);\n })();\n stream.pipe(stdin);\n } else {\n throw new Error(\"Invalid `.stdin(\u2026)` source?\");\n }\n }\n return subprocess;\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling\n }) as any;\n\n /** A wrapper for `.spawn(\u2026)` that sets stdio to `\"inherit\"` (common for\n * invoking commands from scripts whose output and interaction should be\n * surfaced to the user).\n *\n * If there is no other interaction with the shell from the calling process,\n * then it acts \"transparent\" and allows user to interact with the subprocess\n * in its stead.\n */\n public spawnTransparently(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): ChildProcessByStdio<null, null, null> & WithSuccess {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type wrangling.\n return this.spawn({ ...options, stdio: \"inherit\" }) as any;\n }\n\n /**\n * A wrapper for {@link PrintableShellCommand.spawn | `.spawn(\u2026)`} that:\n *\n * - sets `detached` to `true`,\n * - sets stdio to `\"inherit\"`,\n * - calls `.unref()`, and\n * - does not wait for the process to exit.\n *\n * This is similar to starting a command in the background and disowning it (in a shell).\n *\n */\n public spawnDetached(\n options?: NodeWithCwd<Omit<Omit<NodeSpawnOptions, \"stdio\">, \"detached\">>,\n ): void {\n if (options) {\n for (const field of [\"stdio\", \"detached\"]) {\n if (field in options) {\n throw new Error(`Unexpected \\`${field}\\` field.`);\n }\n }\n }\n const childProcess = this.spawn({\n stdio: \"ignore\",\n ...options,\n detached: true,\n });\n childProcess.unref();\n }\n\n #generator(\n readable: Readable,\n successPromise: Promise<void>,\n ): AsyncGenerator<string> {\n // TODO: we'd make this a `ReadableStream`, but `ReadableStream.from(\u2026)` is\n // not implemented in `bun`: https://github.com/oven-sh/bun/issues/3700\n return (async function* () {\n for await (const chunk of readable) {\n yield chunk;\n }\n await successPromise;\n })();\n }\n\n #stdoutSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"pipe\", \"inherit\"],\n });\n return this.#generator(subprocess.stdout, subprocess.success);\n }\n\n public stdout(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response & {\n text: (options?: TrailingNewlineOptions) => Promise<string>;\n } {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#stdoutSpawnGenerator(options))),\n );\n }\n\n #stderrSpawnGenerator(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n if (options && \"stdio\" in options) {\n throw new Error(\"Unexpected `stdio` field.\");\n }\n const subprocess = this.spawn({\n ...options,\n stdio: [\"ignore\", \"inherit\", \"pipe\"],\n });\n return this.#generator(subprocess.stderr, subprocess.success);\n }\n\n public stderr(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Response & {\n text: (options?: TrailingNewlineOptions) => Promise<string>;\n } {\n // TODO: Use `ReadableStream.from(\u2026)` once `bun` implements it: https://github.com/oven-sh/bun/pull/21269\n return wrapHandleTrailingNewlinesForResponse(\n new Response(Readable.from(this.#stderrSpawnGenerator(options))),\n );\n }\n\n async *#split0(generator: AsyncGenerator<string>): AsyncGenerator<string> {\n let pending = \"\";\n for await (const chunk of generator) {\n pending += chunk;\n const newChunks = pending.split(\"\\x00\");\n pending = newChunks.splice(-1)[0];\n yield* newChunks;\n }\n if (pending !== \"\") {\n throw new Error(\n \"Missing a trailing NUL character at the end of a NUL-delimited stream.\",\n );\n }\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).text()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public async text(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">> & {\n trimTrailingNewlines: \"single-required\" | \"single-if-present\" | \"never\";\n },\n ): Promise<string> {\n const {\n trimTrailingNewlines: trimTrailingNewlinesOption,\n ...spawnOptions\n } = options ?? {};\n return handleTrailingNewlines(this.stdout(spawnOptions).text(), {\n trimTrailingNewlines: trimTrailingNewlinesOption,\n });\n }\n\n /**\n * Convenience function for:\n *\n * .stdout(options).json()\n *\n * This can make some simple invocations easier to read and/or fit on a single line.\n */\n public json<T>(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): Promise<T> {\n return this.stdout(options).json() as Promise<T>;\n }\n\n /**\n * Parse `stdout` into a generator of string values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *text0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): AsyncGenerator<string> {\n yield* this.#split0(this.#stdoutSpawnGenerator(options));\n }\n\n /**\n * Parse `stdout` into a generator of JSON values using a NULL delimiter.\n *\n * A trailing NULL delimiter from `stdout` is required and removed.\n */\n public async *json0(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">>,\n ): // biome-ignore lint/suspicious/noExplicitAny: `any` is the correct type for JSON\n AsyncGenerator<any> {\n for await (const part of this.#split0(\n this.#stdoutSpawnGenerator(options),\n )) {\n yield JSON.parse(part);\n }\n }\n\n /** Equivalent to:\n *\n * ```\n * await this.print(\u2026).spawnTransparently(\u2026).success;\n * ```\n */\n public async shellOut(\n options?: NodeWithCwd<Omit<NodeSpawnOptions, \"stdio\">> & {\n print?: StreamPrintOptions | ArgumentLineWrapping | boolean;\n },\n ): Promise<void> {\n const { print: printOptions, ...spawnOptions } = options ?? {};\n\n if (typeof printOptions === \"string\") {\n assert(ARGUMENT_LINE_WRAPPING_VALUES.includes(printOptions));\n this.print({ argumentLineWrapping: printOptions });\n } else if (printOptions === true) {\n this.print();\n } else if (printOptions === false) {\n // no-op\n } else {\n this.print(printOptions);\n }\n await this.spawnTransparently(spawnOptions).success;\n }\n}\n\nexport function escapeArg(\n arg: string,\n isMainCommand: boolean,\n options: PrintOptions,\n): string {\n const argCharacters = new Set(arg);\n const specialShellCharacters = isMainCommand\n ? SPECIAL_SHELL_CHARACTERS_FOR_MAIN_COMMAND\n : SPECIAL_SHELL_CHARACTERS;\n if (\n options?.quoting === \"extra-safe\" ||\n // biome-ignore lint/suspicious/noExplicitAny: Workaround to make this package easier to use in a project that otherwise only uses ES2022.)\n (argCharacters as unknown as any).intersection(specialShellCharacters)\n .size > 0\n ) {\n // Use single quote to reduce the need to escape (and therefore reduce the chance for bugs/security issues).\n const escaped = arg.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"'\", \"\\\\'\");\n return `'${escaped}'`;\n }\n return arg;\n}\n", "const DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR = \"never\";\nexport interface TrailingNewlineOptions {\n trimTrailingNewlines?: \"single-required\" | \"single-if-present\" | \"never\";\n}\n\n// TODO: Handle carriage return?\nexport async function handleTrailingNewlines(\n text: Promise<string>,\n options?: TrailingNewlineOptions,\n): Promise<string> {\n const awaitedText = await text;\n switch (\n options?.trimTrailingNewlines ??\n DEFAULT_TRIM_TRAILING_NEWLINE_BEHAVIOUR\n ) {\n case \"single-required\": {\n if (!awaitedText.endsWith(\"\\n\")) {\n throw new Error(\"Trailing newline required, but not present.\");\n }\n return awaitedText.slice(0, -1);\n }\n case \"single-if-present\": {\n if (awaitedText.endsWith(\"\\n\")) {\n return awaitedText.slice(0, -1);\n } else {\n return awaitedText;\n }\n }\n case \"never\": {\n return awaitedText;\n }\n default:\n throw new Error(\"Invalid value for `trimTrailingNewlines`.\");\n }\n}\n\nfunction wrapHandleTrailingNewlines(\n fn: () => Promise<string>,\n): (options?: TrailingNewlineOptions) => Promise<string> {\n return (options?: TrailingNewlineOptions) =>\n handleTrailingNewlines(fn(), options);\n}\n\nexport function wrapHandleTrailingNewlinesForResponder(v: {\n response(): {\n text: () => Promise<string>;\n };\n}): (options?: TrailingNewlineOptions) => Promise<string> {\n const response = v.response();\n const originalTextFn = response.text.bind(response);\n return wrapHandleTrailingNewlines(originalTextFn);\n}\n\nexport function wrapHandleTrailingNewlinesForResponse(\n response: Response,\n): Response & { text: (options?: TrailingNewlineOptions) => Promise<string> } {\n const textFn = wrapHandleTrailingNewlinesForResponder({\n response: () => response,\n });\n Object.defineProperty(response, \"text\", {\n get: () => {\n return textFn;\n },\n });\n return response;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["stream"]
|
|
7
7
|
}
|
|
@@ -2,8 +2,10 @@ export interface TrailingNewlineOptions {
|
|
|
2
2
|
trimTrailingNewlines?: "single-required" | "single-if-present" | "never";
|
|
3
3
|
}
|
|
4
4
|
export declare function handleTrailingNewlines(text: Promise<string>, options?: TrailingNewlineOptions): Promise<string>;
|
|
5
|
-
export declare function
|
|
6
|
-
|
|
5
|
+
export declare function wrapHandleTrailingNewlinesForResponder(v: {
|
|
6
|
+
response(): {
|
|
7
|
+
text: () => Promise<string>;
|
|
8
|
+
};
|
|
7
9
|
}): (options?: TrailingNewlineOptions) => Promise<string>;
|
|
8
10
|
export declare function wrapHandleTrailingNewlinesForResponse(response: Response): Response & {
|
|
9
11
|
text: (options?: TrailingNewlineOptions) => Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect, spyOn, test } from "bun:test";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { createWriteStream } from "node:fs";
|
|
4
|
-
import { stderr, stdout } from "node:process";
|
|
4
|
+
import { env, stderr, stdout } from "node:process";
|
|
5
5
|
import { Path } from "path-class";
|
|
6
6
|
import { PrintableShellCommand } from ".";
|
|
7
7
|
|
|
@@ -606,6 +606,8 @@ function resetMocks() {
|
|
|
606
606
|
spyStderr.mockReset();
|
|
607
607
|
globalThis.process.stdout.isTTY = false;
|
|
608
608
|
globalThis.process.stderr.isTTY = false;
|
|
609
|
+
// biome-ignore lint/complexity/useLiteralKeys: TODO: https://github.com/biomejs/biome/discussions/7404
|
|
610
|
+
delete env["NO_COLOR"];
|
|
609
611
|
}
|
|
610
612
|
|
|
611
613
|
const PLAIN_ECHO: [string][] = [["echo \\\n hi"], ["\n"]];
|
|
@@ -644,6 +646,26 @@ test.serial("tty (stderr)", async () => {
|
|
|
644
646
|
expect(spyStderr.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
645
647
|
});
|
|
646
648
|
|
|
649
|
+
test.serial("NO_COLOR (stderr)", async () => {
|
|
650
|
+
resetMocks();
|
|
651
|
+
|
|
652
|
+
globalThis.process.stderr.isTTY = true;
|
|
653
|
+
new PrintableShellCommand("echo", ["hi"]).print();
|
|
654
|
+
expect(spyStderr.mock.calls.slice(-2)).toEqual(BOLD_GRAY_ECHO);
|
|
655
|
+
|
|
656
|
+
// biome-ignore lint/complexity/useLiteralKeys: TODO: https://github.com/biomejs/biome/discussions/7404
|
|
657
|
+
env["NO_COLOR"] = "true";
|
|
658
|
+
|
|
659
|
+
new PrintableShellCommand("echo", ["hi"]).print();
|
|
660
|
+
expect(spyStderr.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
661
|
+
new PrintableShellCommand("echo", ["hi"]).print({ autoStyle: "never" });
|
|
662
|
+
expect(spyStderr.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
663
|
+
new PrintableShellCommand("echo", ["hi"]).print({ autoStyle: "tty" });
|
|
664
|
+
expect(spyStderr.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
665
|
+
new PrintableShellCommand("echo", ["hi"]).print({ style: ["gray", "bold"] });
|
|
666
|
+
expect(spyStderr.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
667
|
+
});
|
|
668
|
+
|
|
647
669
|
test.serial("tty (stdout)", async () => {
|
|
648
670
|
resetMocks();
|
|
649
671
|
|
|
@@ -679,6 +701,35 @@ test.serial("tty (stdout)", async () => {
|
|
|
679
701
|
expect(spyStdout.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
680
702
|
});
|
|
681
703
|
|
|
704
|
+
test.serial("NO_COLOR (stdout)", async () => {
|
|
705
|
+
resetMocks();
|
|
706
|
+
|
|
707
|
+
globalThis.process.stdout.isTTY = true;
|
|
708
|
+
new PrintableShellCommand("echo", ["hi"]).print({ stream: stdout });
|
|
709
|
+
expect(spyStdout.mock.calls.slice(-2)).toEqual(BOLD_GRAY_ECHO);
|
|
710
|
+
|
|
711
|
+
// biome-ignore lint/complexity/useLiteralKeys: TODO: https://github.com/biomejs/biome/discussions/7404
|
|
712
|
+
env["NO_COLOR"] = "true";
|
|
713
|
+
|
|
714
|
+
new PrintableShellCommand("echo", ["hi"]).print({ stream: stdout });
|
|
715
|
+
expect(spyStdout.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
716
|
+
new PrintableShellCommand("echo", ["hi"]).print({
|
|
717
|
+
stream: stdout,
|
|
718
|
+
autoStyle: "never",
|
|
719
|
+
});
|
|
720
|
+
expect(spyStdout.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
721
|
+
new PrintableShellCommand("echo", ["hi"]).print({
|
|
722
|
+
stream: stdout,
|
|
723
|
+
autoStyle: "tty",
|
|
724
|
+
});
|
|
725
|
+
expect(spyStdout.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
726
|
+
new PrintableShellCommand("echo", ["hi"]).print({
|
|
727
|
+
stream: stdout,
|
|
728
|
+
style: ["gray", "bold"],
|
|
729
|
+
});
|
|
730
|
+
expect(spyStdout.mock.calls.slice(-2)).toEqual(PLAIN_ECHO);
|
|
731
|
+
});
|
|
732
|
+
|
|
682
733
|
test.serial(".shellOut()", async () => {
|
|
683
734
|
resetMocks();
|
|
684
735
|
|
|
@@ -20,8 +20,8 @@ import type {
|
|
|
20
20
|
import {
|
|
21
21
|
handleTrailingNewlines,
|
|
22
22
|
type TrailingNewlineOptions,
|
|
23
|
+
wrapHandleTrailingNewlinesForResponder,
|
|
23
24
|
wrapHandleTrailingNewlinesForResponse,
|
|
24
|
-
wrapHandleTrailingNewlinesForText,
|
|
25
25
|
} from "./trimTrailingNewlines";
|
|
26
26
|
|
|
27
27
|
// TODO: does this import work?
|
|
@@ -102,6 +102,25 @@ export interface PrintOptions {
|
|
|
102
102
|
style?: StyleTextFormat;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* https://no-color.org/
|
|
107
|
+
*
|
|
108
|
+
* > Command-line software which adds ANSI color to its output by default should
|
|
109
|
+
* > check for a NO_COLOR environment variable that, when present and not an
|
|
110
|
+
* > empty string (regardless of its value), prevents the addition of ANSI
|
|
111
|
+
* > color.
|
|
112
|
+
*
|
|
113
|
+
* I think it's a bit silly that `NO_COLOR=false` and `NO_COLOR=0` count as "no
|
|
114
|
+
* color please", but 🤷
|
|
115
|
+
*
|
|
116
|
+
*/
|
|
117
|
+
function NO_COLOR(): boolean {
|
|
118
|
+
const { env } = globalThis.process.getBuiltinModule("node:process");
|
|
119
|
+
// The empty string is falsy, so we can just use `!!`.
|
|
120
|
+
// biome-ignore lint/complexity/useLiteralKeys: TODO: https://github.com/biomejs/biome/discussions/7572
|
|
121
|
+
return !!env["NO_COLOR"];
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
export interface StreamPrintOptions extends PrintOptions {
|
|
106
125
|
/**
|
|
107
126
|
* Auto-style the text when:
|
|
@@ -315,12 +334,10 @@ export class PrintableShellCommand {
|
|
|
315
334
|
const stream = options?.stream ?? stderr;
|
|
316
335
|
// Note: we only need to modify top-level fields, so `structuredClone(…)`
|
|
317
336
|
// would be overkill and can only cause performance issues.
|
|
318
|
-
const optionsCopy = {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
? TTY_AUTO_STYLE
|
|
323
|
-
: undefined;
|
|
337
|
+
const optionsCopy = {
|
|
338
|
+
...options,
|
|
339
|
+
style: this.#styleFromOptions(stream, options),
|
|
340
|
+
};
|
|
324
341
|
const writable =
|
|
325
342
|
stream instanceof Writable ? stream : Writable.fromWeb(stream);
|
|
326
343
|
writable.write(this.getPrintableCommand(optionsCopy));
|
|
@@ -328,6 +345,22 @@ export class PrintableShellCommand {
|
|
|
328
345
|
return this;
|
|
329
346
|
}
|
|
330
347
|
|
|
348
|
+
#styleFromOptions(
|
|
349
|
+
stream: WriteStream | Writable,
|
|
350
|
+
options?: StreamPrintOptions,
|
|
351
|
+
): StyleTextFormat | undefined {
|
|
352
|
+
if (options?.autoStyle === "never") {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!(stream as { isTTY?: boolean }).isTTY) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (NO_COLOR()) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
return TTY_AUTO_STYLE;
|
|
362
|
+
}
|
|
363
|
+
|
|
331
364
|
#stdinSource: StdinSource | undefined;
|
|
332
365
|
/**
|
|
333
366
|
* Send data to `stdin` of the subprocess.
|
|
@@ -374,10 +407,12 @@ export class PrintableShellCommand {
|
|
|
374
407
|
}) as NodeChildProcess & {
|
|
375
408
|
success: Promise<void>;
|
|
376
409
|
};
|
|
410
|
+
let cachedSuccess: Promise<void> | undefined;
|
|
377
411
|
// TODO: define properties on prototypes instead.
|
|
378
412
|
Object.defineProperty(subprocess, "success", {
|
|
379
413
|
get() {
|
|
380
|
-
|
|
414
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: TODO: https://github.com/biomejs/biome/discussions/7592
|
|
415
|
+
return (cachedSuccess ??= new Promise<void>((resolve, reject) => {
|
|
381
416
|
function handle(exitCode: number) {
|
|
382
417
|
if (exitCode === 0) {
|
|
383
418
|
resolve();
|
|
@@ -397,7 +432,7 @@ export class PrintableShellCommand {
|
|
|
397
432
|
this.addListener("error", (err: any) => {
|
|
398
433
|
reject(err);
|
|
399
434
|
});
|
|
400
|
-
});
|
|
435
|
+
}));
|
|
401
436
|
},
|
|
402
437
|
enumerable: false,
|
|
403
438
|
});
|
|
@@ -412,7 +447,7 @@ export class PrintableShellCommand {
|
|
|
412
447
|
(cachedResponse ??= wrapHandleTrailingNewlinesForResponse(
|
|
413
448
|
new Response(Readable.from(this.#generator(s.stdout, s.success))),
|
|
414
449
|
));
|
|
415
|
-
s.stdout.text =
|
|
450
|
+
s.stdout.text = wrapHandleTrailingNewlinesForResponder(s.stdout);
|
|
416
451
|
const thisCached = this; // TODO: make this type-check using `.bind(…)`
|
|
417
452
|
s.stdout.text0 = async function* () {
|
|
418
453
|
yield* thisCached.#split0(thisCached.#generator(s.stdout, s.success));
|
|
@@ -430,7 +465,7 @@ export class PrintableShellCommand {
|
|
|
430
465
|
(cachedResponse ??= wrapHandleTrailingNewlinesForResponse(
|
|
431
466
|
new Response(Readable.from(this.#generator(s.stderr, s.success))),
|
|
432
467
|
));
|
|
433
|
-
s.stderr.text =
|
|
468
|
+
s.stderr.text = wrapHandleTrailingNewlinesForResponder(s.stderr);
|
|
434
469
|
const thisCached = this; // TODO: make this type-check using `.bind(…)`
|
|
435
470
|
s.stderr.text0 = async function* () {
|
|
436
471
|
yield* thisCached.#split0(thisCached.#generator(s.stderr, s.success));
|
|
@@ -41,17 +41,22 @@ function wrapHandleTrailingNewlines(
|
|
|
41
41
|
handleTrailingNewlines(fn(), options);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function
|
|
45
|
-
|
|
44
|
+
export function wrapHandleTrailingNewlinesForResponder(v: {
|
|
45
|
+
response(): {
|
|
46
|
+
text: () => Promise<string>;
|
|
47
|
+
};
|
|
46
48
|
}): (options?: TrailingNewlineOptions) => Promise<string> {
|
|
47
|
-
const
|
|
49
|
+
const response = v.response();
|
|
50
|
+
const originalTextFn = response.text.bind(response);
|
|
48
51
|
return wrapHandleTrailingNewlines(originalTextFn);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export function wrapHandleTrailingNewlinesForResponse(
|
|
52
55
|
response: Response,
|
|
53
56
|
): Response & { text: (options?: TrailingNewlineOptions) => Promise<string> } {
|
|
54
|
-
const textFn =
|
|
57
|
+
const textFn = wrapHandleTrailingNewlinesForResponder({
|
|
58
|
+
response: () => response,
|
|
59
|
+
});
|
|
55
60
|
Object.defineProperty(response, "text", {
|
|
56
61
|
get: () => {
|
|
57
62
|
return textFn;
|