Commit 8643029

Anton Golub <antongolub@antongolub.com>
2025-06-18 18:04:31
feat: expose `ProcessOutput` error formatters
1 parent 1c89db0
build/core.cjs
@@ -205,25 +205,18 @@ var ERRNO_CODES = {
 function getErrnoMessage(errno) {
   return ERRNO_CODES[-errno] || "Unknown error";
 }
-function getExitCodeInfo(exitCode) {
-  return EXIT_CODES[exitCode];
-}
+var getExitCodeInfo = (exitCode) => EXIT_CODES[exitCode];
 var formatExitMessage = (code, signal, stderr, from, details = "") => {
-  let message = `exit code: ${code}`;
-  if (code != 0 || signal != null) {
-    message = `${stderr || "\n"}    at ${from}`;
-    message += `
-    exit code: ${code}${getExitCodeInfo(code) ? " (" + getExitCodeInfo(code) + ")" : ""}`;
-    if (signal != null) {
-      message += `
+  if (code == 0 && signal == null) return `exit code: ${code}`;
+  const codeInfo = getExitCodeInfo(code);
+  let message = `${stderr}
+    at ${from}
+    exit code: ${code}${codeInfo ? " (" + codeInfo + ")" : ""}`;
+  if (signal != null) message += `
     signal: ${signal}`;
-    }
-    if (details) {
-      message += `
+  if (details) message += `
     details: 
 ${details}`;
-    }
-  }
   return message;
 };
 var formatErrorMessage = (err, from) => {
@@ -241,12 +234,12 @@ function getCallerLocationFromString(stackString = "unknown") {
   const offset = i < 0 ? i : i + 2;
   return (lines.find((l) => l.includes("file://")) || lines[offset] || stackString).trim();
 }
-function findErrors(lines = []) {
-  if (lines.length < 20) return lines.join("\n");
+var formatErrorDetails = (lines = [], lim = 20) => {
+  if (lines.length < lim) return lines.join("\n");
   let errors = lines.filter((l) => /(fail|error|not ok|exception)/i.test(l));
   if (errors.length === 0) errors = lines;
-  return errors.slice(0, 20).join("\n") + (errors.length > 20 ? "\n..." : "");
-}
+  return errors.slice(0, lim).join("\n") + (errors.length > lim ? "\n..." : "");
+};
 
 // src/core.ts
 var import_vendor_core2 = require("./vendor-core.cjs");
@@ -873,7 +866,13 @@ var _ProcessOutput = class _ProcessOutput extends Error {
       stdall: { get: (0, import_util.once)(() => (0, import_util.bufArrJoin)(dto.store.stdall)) },
       message: {
         get: (0, import_util.once)(
-          () => message || dto.error ? _ProcessOutput.getErrorMessage(dto.error || new Error(message), dto.from) : _ProcessOutput.getExitMessage(dto.code, dto.signal, this.stderr, dto.from, this.stderr.trim() ? "" : findErrors(this.lines()))
+          () => message || dto.error ? _ProcessOutput.getErrorMessage(dto.error || new Error(message), dto.from) : _ProcessOutput.getExitMessage(
+            dto.code,
+            dto.signal,
+            this.stderr,
+            dto.from,
+            this.stderr.trim() ? "" : _ProcessOutput.getErrorDetails(this.lines())
+          )
         )
       }
     });
@@ -931,18 +930,20 @@ var _ProcessOutput = class _ProcessOutput extends Error {
     if (memo[0]) yield memo[0];
   }
   [import_node_util2.inspect.custom]() {
-    const stringify = (s, c) => s.length === 0 ? "''" : c((0, import_node_util2.inspect)(s));
+    const codeInfo = _ProcessOutput.getExitCodeInfo(this.exitCode);
     return `ProcessOutput {
-  stdout: ${stringify(this.stdout, import_vendor_core2.chalk.green)},
-  stderr: ${stringify(this.stderr, import_vendor_core2.chalk.red)},
+  stdout: ${import_vendor_core2.chalk.green((0, import_node_util2.inspect)(this.stdout))},
+  stderr: ${import_vendor_core2.chalk.red((0, import_node_util2.inspect)(this.stderr))},
   signal: ${(0, import_node_util2.inspect)(this.signal)},
-  exitCode: ${(this.exitCode === 0 ? import_vendor_core2.chalk.green : import_vendor_core2.chalk.red)(this.exitCode)}${getExitCodeInfo(this.exitCode) ? import_vendor_core2.chalk.grey(" (" + getExitCodeInfo(this.exitCode) + ")") : ""},
+  exitCode: ${(this.ok ? import_vendor_core2.chalk.green : import_vendor_core2.chalk.red)(this.exitCode)}${codeInfo ? import_vendor_core2.chalk.grey(" (" + codeInfo + ")") : ""},
   duration: ${this.duration}
 }`;
   }
 };
 _ProcessOutput.getExitMessage = formatExitMessage;
 _ProcessOutput.getErrorMessage = formatErrorMessage;
+_ProcessOutput.getErrorDetails = formatErrorDetails;
+_ProcessOutput.getExitCodeInfo = getExitCodeInfo;
 var ProcessOutput = _ProcessOutput;
 function usePowerShell() {
   $.shell = import_vendor_core2.which.sync("powershell.exe");
build/core.d.ts
@@ -175,9 +175,11 @@ export declare class ProcessOutput extends Error {
     lines(delimiter?: string | RegExp): string[];
     valueOf(): string;
     [Symbol.iterator](): Iterator<string>;
+    [inspect.custom](): string;
     static getExitMessage: (code: number | null, signal: NodeJS.Signals | null, stderr: string, from: string, details?: string) => string;
     static getErrorMessage: (err: NodeJS.ErrnoException, from: string) => string;
-    [inspect.custom](): string;
+    static getErrorDetails: (lines?: string[], lim?: number) => string;
+    static getExitCodeInfo: (exitCode: number | null) => string | undefined;
 }
 export declare function usePowerShell(): void;
 export declare function usePwsh(): void;
src/core.ts
@@ -29,7 +29,7 @@ import { EventEmitter } from 'node:events'
 import { Buffer } from 'node:buffer'
 import process from 'node:process'
 import {
-  findErrors,
+  formatErrorDetails,
   formatErrorMessage,
   formatExitMessage,
   getCallerLocation,
@@ -746,7 +746,13 @@ export class ProcessOutput extends Error {
       message: { get: once(() =>
           message || dto.error
             ? ProcessOutput.getErrorMessage(dto.error || new Error(message), dto.from)
-            : ProcessOutput.getExitMessage(dto.code, dto.signal, this.stderr, dto.from, this.stderr.trim() ? '' : findErrors(this.lines()))
+            : ProcessOutput.getExitMessage(
+              dto.code,
+              dto.signal,
+              this.stderr,
+              dto.from,
+              this.stderr.trim() ? '' : ProcessOutput.getErrorDetails(this.lines())
+            )
         ),
       },
     })
@@ -823,25 +829,27 @@ export class ProcessOutput extends Error {
     if (memo[0]) yield memo[0]
   }
 
-  static getExitMessage = formatExitMessage
-
-  static getErrorMessage = formatErrorMessage;
-
   [inspect.custom](): string {
-    const stringify = (s: string, c: ChalkInstance) =>
-      s.length === 0 ? "''" : c(inspect(s))
+    const codeInfo = ProcessOutput.getExitCodeInfo(this.exitCode)
+
     return `ProcessOutput {
-  stdout: ${stringify(this.stdout, chalk.green)},
-  stderr: ${stringify(this.stderr, chalk.red)},
+  stdout: ${chalk.green(inspect(this.stdout))},
+  stderr: ${chalk.red(inspect(this.stderr))},
   signal: ${inspect(this.signal)},
-  exitCode: ${(this.exitCode === 0 ? chalk.green : chalk.red)(this.exitCode)}${
-    getExitCodeInfo(this.exitCode)
-      ? chalk.grey(' (' + getExitCodeInfo(this.exitCode) + ')')
-      : ''
+  exitCode: ${(this.ok ? chalk.green : chalk.red)(this.exitCode)}${
+    codeInfo ? chalk.grey(' (' + codeInfo + ')') : ''
   },
   duration: ${this.duration}
 }`
   }
+
+  static getExitMessage = formatExitMessage
+
+  static getErrorMessage = formatErrorMessage
+
+  static getErrorDetails = formatErrorDetails
+
+  static getExitCodeInfo = getExitCodeInfo
 }
 
 export function usePowerShell() {
src/error.ts
@@ -176,9 +176,8 @@ export function getErrnoMessage(errno?: number): string {
   )
 }
 
-export function getExitCodeInfo(exitCode: number | null): string | undefined {
-  return EXIT_CODES[exitCode as keyof typeof EXIT_CODES]
-}
+export const getExitCodeInfo = (exitCode: number | null): string | undefined =>
+  EXIT_CODES[exitCode as keyof typeof EXIT_CODES]
 
 export const formatExitMessage = (
   code: number | null,
@@ -187,19 +186,16 @@ export const formatExitMessage = (
   from: string,
   details: string = ''
 ): string => {
-  let message = `exit code: ${code}`
-  if (code != 0 || signal != null) {
-    message = `${stderr || '\n'}    at ${from}`
-    message += `\n    exit code: ${code}${
-      getExitCodeInfo(code) ? ' (' + getExitCodeInfo(code) + ')' : ''
-    }`
-    if (signal != null) {
-      message += `\n    signal: ${signal}`
-    }
-    if (details) {
-      message += `\n    details: \n${details}`
-    }
-  }
+  if (code == 0 && signal == null) return `exit code: ${code}`
+
+  const codeInfo = getExitCodeInfo(code)
+  let message = `${stderr}
+    at ${from}
+    exit code: ${code}${codeInfo ? ' (' + codeInfo + ')' : ''}`
+
+  if (signal != null) message += `\n    signal: ${signal}`
+
+  if (details) message += `\n    details: \n${details}`
 
   return message
 }
@@ -208,12 +204,10 @@ export const formatErrorMessage = (
   err: NodeJS.ErrnoException,
   from: string
 ): string => {
-  return (
-    `${err.message}\n` +
-    `    errno: ${err.errno} (${getErrnoMessage(err.errno)})\n` +
-    `    code: ${err.code}\n` +
-    `    at ${from}`
-  )
+  return `${err.message}
+    errno: ${err.errno} (${getErrnoMessage(err.errno)})
+    code: ${err.code}
+    at ${from}`
 }
 
 export function getCallerLocation(err = new Error('zx error')): string {
@@ -234,10 +228,10 @@ export function getCallerLocationFromString(stackString = 'unknown'): string {
   ).trim()
 }
 
-export function findErrors(lines: string[] = []): string {
-  if (lines.length < 20) return lines.join('\n')
+export const formatErrorDetails = (lines: string[] = [], lim = 20): string => {
+  if (lines.length < lim) return lines.join('\n')
 
   let errors = lines.filter((l) => /(fail|error|not ok|exception)/i.test(l))
   if (errors.length === 0) errors = lines
-  return errors.slice(0, 20).join('\n') + (errors.length > 20 ? '\n...' : '')
+  return errors.slice(0, lim).join('\n') + (errors.length > lim ? '\n...' : '')
 }
test/error.test.ts
@@ -21,7 +21,7 @@ import {
   getCallerLocationFromString,
   formatExitMessage,
   formatErrorMessage,
-  findErrors,
+  formatErrorDetails,
 } from '../src/error.ts'
 
 describe('error', () => {
@@ -119,15 +119,19 @@ describe('error', () => {
   test('findErrors()', () => {
     const lines = [...Array(40).keys()].map((v) => v + '')
 
-    assert.equal(findErrors([]), '', 'empty returns empty')
-    assert.equal(findErrors(['foo', 'bar']), 'foo\nbar', 'squashes a few')
+    assert.equal(formatErrorDetails([]), '', 'empty returns empty')
     assert.equal(
-      findErrors(['failure: foo', 'NOT OK smth', ...lines]),
+      formatErrorDetails(['foo', 'bar']),
+      'foo\nbar',
+      'squashes a few'
+    )
+    assert.equal(
+      formatErrorDetails(['failure: foo', 'NOT OK smth', ...lines]),
       'failure: foo\nNOT OK smth',
       'extracts errors'
     )
     assert.equal(
-      findErrors(lines),
+      formatErrorDetails(lines),
       '0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n...',
       'shows a sample'
     )
test/export.test.js
@@ -24,7 +24,9 @@ describe('core', () => {
   test('exports', () => {
     assert.equal(typeof core.$, 'function', 'core.$')
     assert.equal(typeof core.ProcessOutput, 'function', 'core.ProcessOutput')
+    assert.equal(typeof core.ProcessOutput.getErrorDetails, 'function', 'core.ProcessOutput.getErrorDetails')
     assert.equal(typeof core.ProcessOutput.getErrorMessage, 'function', 'core.ProcessOutput.getErrorMessage')
+    assert.equal(typeof core.ProcessOutput.getExitCodeInfo, 'function', 'core.ProcessOutput.getExitCodeInfo')
     assert.equal(typeof core.ProcessOutput.getExitMessage, 'function', 'core.ProcessOutput.getExitMessage')
     assert.equal(typeof core.ProcessPromise, 'function', 'core.ProcessPromise')
     assert.equal(typeof core.cd, 'function', 'core.cd')
@@ -144,7 +146,9 @@ describe('index', () => {
   test('exports', () => {
     assert.equal(typeof index.$, 'function', 'index.$')
     assert.equal(typeof index.ProcessOutput, 'function', 'index.ProcessOutput')
+    assert.equal(typeof index.ProcessOutput.getErrorDetails, 'function', 'index.ProcessOutput.getErrorDetails')
     assert.equal(typeof index.ProcessOutput.getErrorMessage, 'function', 'index.ProcessOutput.getErrorMessage')
+    assert.equal(typeof index.ProcessOutput.getExitCodeInfo, 'function', 'index.ProcessOutput.getExitCodeInfo')
     assert.equal(typeof index.ProcessOutput.getExitMessage, 'function', 'index.ProcessOutput.getExitMessage')
     assert.equal(typeof index.ProcessPromise, 'function', 'index.ProcessPromise')
     assert.equal(typeof index.VERSION, 'string', 'index.VERSION')
.size-limit.json
@@ -15,7 +15,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "120.55 kB",
+    "limit": "120.80 kB",
     "brotli": false,
     "gzip": false
   },
@@ -29,14 +29,14 @@
       "build/globals.js",
       "build/deno.js"
     ],
-    "limit": "812.05 kB",
+    "limit": "812.15 kB",
     "brotli": false,
     "gzip": false
   },
   {
     "name": "libdefs",
     "path": "build/*.d.ts",
-    "limit": "39.00 kB",
+    "limit": "39.15 kB",
     "brotli": false,
     "gzip": false
   },
@@ -62,7 +62,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "867.40 kB",
+    "limit": "867.65 kB",
     "brotli": false,
     "gzip": false
   }