Commit a69ddd4

Anton Golub <antongolub@antongolub.com>
2024-12-21 16:28:42
refactor: separate error helpers (#1014)
* refactor: separate error helpers closes #964 * chore: handle ts-node issue for ts4
1 parent dda74fc
src/core.ts
@@ -25,6 +25,12 @@ import { type Readable, type Writable } from 'node:stream'
 import { inspect } from 'node:util'
 import { EOL as _EOL } from 'node:os'
 import { EventEmitter } from 'node:events'
+import {
+  formatErrorMessage,
+  formatExitMessage,
+  getCallerLocation,
+  getExitCodeInfo,
+} from './error.js'
 import {
   exec,
   buildCmd,
@@ -39,10 +45,7 @@ import {
 } from './vendor-core.js'
 import {
   type Duration,
-  errnoMessage,
-  exitCodeInfo,
   formatCmd,
-  getCallerLocation,
   isString,
   isStringLiteral,
   noop,
@@ -720,34 +723,9 @@ export class ProcessOutput extends Error {
     return this._duration
   }
 
-  static getExitMessage(
-    code: number | null,
-    signal: NodeJS.Signals | null,
-    stderr: string,
-    from: string
-  ): string {
-    let message = `exit code: ${code}`
-    if (code != 0 || signal != null) {
-      message = `${stderr || '\n'}    at ${from}`
-      message += `\n    exit code: ${code}${
-        exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''
-      }`
-      if (signal != null) {
-        message += `\n    signal: ${signal}`
-      }
-    }
-
-    return message
-  }
+  static getExitMessage = formatExitMessage
 
-  static getErrorMessage(err: NodeJS.ErrnoException, from: string): string {
-    return (
-      `${err.message}\n` +
-      `    errno: ${err.errno} (${errnoMessage(err.errno)})\n` +
-      `    code: ${err.code}\n` +
-      `    at ${from}`
-    )
-  }
+  static getErrorMessage = formatErrorMessage;
 
   [inspect.custom](): string {
     let stringify = (s: string, c: ChalkInstance) =>
@@ -757,8 +735,8 @@ export class ProcessOutput extends Error {
   stderr: ${stringify(this.stderr, chalk.red)},
   signal: ${inspect(this.signal)},
   exitCode: ${(this.exitCode === 0 ? chalk.green : chalk.red)(this.exitCode)}${
-    exitCodeInfo(this.exitCode)
-      ? chalk.grey(' (' + exitCodeInfo(this.exitCode) + ')')
+    getExitCodeInfo(this.exitCode)
+      ? chalk.grey(' (' + getExitCodeInfo(this.exitCode) + ')')
       : ''
   },
   duration: ${this.duration}
src/error.ts
@@ -0,0 +1,226 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const EXIT_CODES = {
+  2: 'Misuse of shell builtins',
+  126: 'Invoked command cannot execute',
+  127: 'Command not found',
+  128: 'Invalid exit argument',
+  129: 'Hangup',
+  130: 'Interrupt',
+  131: 'Quit and dump core',
+  132: 'Illegal instruction',
+  133: 'Trace/breakpoint trap',
+  134: 'Process aborted',
+  135: 'Bus error: "access to undefined portion of memory object"',
+  136: 'Floating point exception: "erroneous arithmetic operation"',
+  137: 'Kill (terminate immediately)',
+  138: 'User-defined 1',
+  139: 'Segmentation violation',
+  140: 'User-defined 2',
+  141: 'Write to pipe with no one reading',
+  142: 'Signal raised by alarm',
+  143: 'Termination (request to terminate)',
+  145: 'Child process terminated, stopped (or continued*)',
+  146: 'Continue if stopped',
+  147: 'Stop executing temporarily',
+  148: 'Terminal stop signal',
+  149: 'Background process attempting to read from tty ("in")',
+  150: 'Background process attempting to write to tty ("out")',
+  151: 'Urgent data available on socket',
+  152: 'CPU time limit exceeded',
+  153: 'File size limit exceeded',
+  154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
+  155: 'Profiling timer expired',
+  157: 'Pollable event',
+  159: 'Bad syscall',
+}
+
+const ERRNO_CODES = {
+  0: 'Success',
+  1: 'Not super-user',
+  2: 'No such file or directory',
+  3: 'No such process',
+  4: 'Interrupted system call',
+  5: 'I/O error',
+  6: 'No such device or address',
+  7: 'Arg list too long',
+  8: 'Exec format error',
+  9: 'Bad file number',
+  10: 'No children',
+  11: 'No more processes',
+  12: 'Not enough core',
+  13: 'Permission denied',
+  14: 'Bad address',
+  15: 'Block device required',
+  16: 'Mount device busy',
+  17: 'File exists',
+  18: 'Cross-device link',
+  19: 'No such device',
+  20: 'Not a directory',
+  21: 'Is a directory',
+  22: 'Invalid argument',
+  23: 'Too many open files in system',
+  24: 'Too many open files',
+  25: 'Not a typewriter',
+  26: 'Text file busy',
+  27: 'File too large',
+  28: 'No space left on device',
+  29: 'Illegal seek',
+  30: 'Read only file system',
+  31: 'Too many links',
+  32: 'Broken pipe',
+  33: 'Math arg out of domain of func',
+  34: 'Math result not representable',
+  35: 'File locking deadlock error',
+  36: 'File or path name too long',
+  37: 'No record locks available',
+  38: 'Function not implemented',
+  39: 'Directory not empty',
+  40: 'Too many symbolic links',
+  42: 'No message of desired type',
+  43: 'Identifier removed',
+  44: 'Channel number out of range',
+  45: 'Level 2 not synchronized',
+  46: 'Level 3 halted',
+  47: 'Level 3 reset',
+  48: 'Link number out of range',
+  49: 'Protocol driver not attached',
+  50: 'No CSI structure available',
+  51: 'Level 2 halted',
+  52: 'Invalid exchange',
+  53: 'Invalid request descriptor',
+  54: 'Exchange full',
+  55: 'No anode',
+  56: 'Invalid request code',
+  57: 'Invalid slot',
+  59: 'Bad font file fmt',
+  60: 'Device not a stream',
+  61: 'No data (for no delay io)',
+  62: 'Timer expired',
+  63: 'Out of streams resources',
+  64: 'Machine is not on the network',
+  65: 'Package not installed',
+  66: 'The object is remote',
+  67: 'The link has been severed',
+  68: 'Advertise error',
+  69: 'Srmount error',
+  70: 'Communication error on send',
+  71: 'Protocol error',
+  72: 'Multihop attempted',
+  73: 'Cross mount point (not really error)',
+  74: 'Trying to read unreadable message',
+  75: 'Value too large for defined data type',
+  76: 'Given log. name not unique',
+  77: 'f.d. invalid for this operation',
+  78: 'Remote address changed',
+  79: 'Can   access a needed shared lib',
+  80: 'Accessing a corrupted shared lib',
+  81: '.lib section in a.out corrupted',
+  82: 'Attempting to link in too many libs',
+  83: 'Attempting to exec a shared library',
+  84: 'Illegal byte sequence',
+  86: 'Streams pipe error',
+  87: 'Too many users',
+  88: 'Socket operation on non-socket',
+  89: 'Destination address required',
+  90: 'Message too long',
+  91: 'Protocol wrong type for socket',
+  92: 'Protocol not available',
+  93: 'Unknown protocol',
+  94: 'Socket type not supported',
+  95: 'Not supported',
+  96: 'Protocol family not supported',
+  97: 'Address family not supported by protocol family',
+  98: 'Address already in use',
+  99: 'Address not available',
+  100: 'Network interface is not configured',
+  101: 'Network is unreachable',
+  102: 'Connection reset by network',
+  103: 'Connection aborted',
+  104: 'Connection reset by peer',
+  105: 'No buffer space available',
+  106: 'Socket is already connected',
+  107: 'Socket is not connected',
+  108: "Can't send after socket shutdown",
+  109: 'Too many references',
+  110: 'Connection timed out',
+  111: 'Connection refused',
+  112: 'Host is down',
+  113: 'Host is unreachable',
+  114: 'Socket already connected',
+  115: 'Connection already in progress',
+  116: 'Stale file handle',
+  122: 'Quota exceeded',
+  123: 'No medium (in tape drive)',
+  125: 'Operation canceled',
+  130: 'Previous owner died',
+  131: 'State not recoverable',
+}
+
+export function getErrnoMessage(errno?: number): string {
+  return (
+    ERRNO_CODES[-(errno as number) as keyof typeof ERRNO_CODES] ||
+    'Unknown error'
+  )
+}
+
+export function getExitCodeInfo(exitCode: number | null): string | undefined {
+  return EXIT_CODES[exitCode as keyof typeof EXIT_CODES]
+}
+
+export const formatExitMessage = (
+  code: number | null,
+  signal: NodeJS.Signals | null,
+  stderr: string,
+  from: 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}`
+    }
+  }
+
+  return message
+}
+
+export const formatErrorMessage = (
+  err: NodeJS.ErrnoException,
+  from: string
+) => {
+  return (
+    `${err.message}\n` +
+    `    errno: ${err.errno} (${getErrnoMessage(err.errno)})\n` +
+    `    code: ${err.code}\n` +
+    `    at ${from}`
+  )
+}
+
+export function getCallerLocation(err = new Error('zx error')) {
+  return getCallerLocationFromString(err.stack)
+}
+
+export function getCallerLocationFromString(stackString = 'unknown') {
+  return (
+    stackString
+      .split(/^\s*(at\s)?/m)
+      .filter((s) => s?.includes(':'))[2]
+      ?.trim() || stackString
+  )
+}
src/util.ts
@@ -121,174 +121,6 @@ export function quotePowerShell(arg: string): string {
   return `'` + arg.replace(/'/g, "''") + `'`
 }
 
-const EXIT_CODES = {
-  2: 'Misuse of shell builtins',
-  126: 'Invoked command cannot execute',
-  127: 'Command not found',
-  128: 'Invalid exit argument',
-  129: 'Hangup',
-  130: 'Interrupt',
-  131: 'Quit and dump core',
-  132: 'Illegal instruction',
-  133: 'Trace/breakpoint trap',
-  134: 'Process aborted',
-  135: 'Bus error: "access to undefined portion of memory object"',
-  136: 'Floating point exception: "erroneous arithmetic operation"',
-  137: 'Kill (terminate immediately)',
-  138: 'User-defined 1',
-  139: 'Segmentation violation',
-  140: 'User-defined 2',
-  141: 'Write to pipe with no one reading',
-  142: 'Signal raised by alarm',
-  143: 'Termination (request to terminate)',
-  145: 'Child process terminated, stopped (or continued*)',
-  146: 'Continue if stopped',
-  147: 'Stop executing temporarily',
-  148: 'Terminal stop signal',
-  149: 'Background process attempting to read from tty ("in")',
-  150: 'Background process attempting to write to tty ("out")',
-  151: 'Urgent data available on socket',
-  152: 'CPU time limit exceeded',
-  153: 'File size limit exceeded',
-  154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
-  155: 'Profiling timer expired',
-  157: 'Pollable event',
-  159: 'Bad syscall',
-}
-
-export function exitCodeInfo(exitCode: number | null): string | undefined {
-  return EXIT_CODES[exitCode as keyof typeof EXIT_CODES]
-}
-
-const ERRNO_CODES = {
-  0: 'Success',
-  1: 'Not super-user',
-  2: 'No such file or directory',
-  3: 'No such process',
-  4: 'Interrupted system call',
-  5: 'I/O error',
-  6: 'No such device or address',
-  7: 'Arg list too long',
-  8: 'Exec format error',
-  9: 'Bad file number',
-  10: 'No children',
-  11: 'No more processes',
-  12: 'Not enough core',
-  13: 'Permission denied',
-  14: 'Bad address',
-  15: 'Block device required',
-  16: 'Mount device busy',
-  17: 'File exists',
-  18: 'Cross-device link',
-  19: 'No such device',
-  20: 'Not a directory',
-  21: 'Is a directory',
-  22: 'Invalid argument',
-  23: 'Too many open files in system',
-  24: 'Too many open files',
-  25: 'Not a typewriter',
-  26: 'Text file busy',
-  27: 'File too large',
-  28: 'No space left on device',
-  29: 'Illegal seek',
-  30: 'Read only file system',
-  31: 'Too many links',
-  32: 'Broken pipe',
-  33: 'Math arg out of domain of func',
-  34: 'Math result not representable',
-  35: 'File locking deadlock error',
-  36: 'File or path name too long',
-  37: 'No record locks available',
-  38: 'Function not implemented',
-  39: 'Directory not empty',
-  40: 'Too many symbolic links',
-  42: 'No message of desired type',
-  43: 'Identifier removed',
-  44: 'Channel number out of range',
-  45: 'Level 2 not synchronized',
-  46: 'Level 3 halted',
-  47: 'Level 3 reset',
-  48: 'Link number out of range',
-  49: 'Protocol driver not attached',
-  50: 'No CSI structure available',
-  51: 'Level 2 halted',
-  52: 'Invalid exchange',
-  53: 'Invalid request descriptor',
-  54: 'Exchange full',
-  55: 'No anode',
-  56: 'Invalid request code',
-  57: 'Invalid slot',
-  59: 'Bad font file fmt',
-  60: 'Device not a stream',
-  61: 'No data (for no delay io)',
-  62: 'Timer expired',
-  63: 'Out of streams resources',
-  64: 'Machine is not on the network',
-  65: 'Package not installed',
-  66: 'The object is remote',
-  67: 'The link has been severed',
-  68: 'Advertise error',
-  69: 'Srmount error',
-  70: 'Communication error on send',
-  71: 'Protocol error',
-  72: 'Multihop attempted',
-  73: 'Cross mount point (not really error)',
-  74: 'Trying to read unreadable message',
-  75: 'Value too large for defined data type',
-  76: 'Given log. name not unique',
-  77: 'f.d. invalid for this operation',
-  78: 'Remote address changed',
-  79: 'Can   access a needed shared lib',
-  80: 'Accessing a corrupted shared lib',
-  81: '.lib section in a.out corrupted',
-  82: 'Attempting to link in too many libs',
-  83: 'Attempting to exec a shared library',
-  84: 'Illegal byte sequence',
-  86: 'Streams pipe error',
-  87: 'Too many users',
-  88: 'Socket operation on non-socket',
-  89: 'Destination address required',
-  90: 'Message too long',
-  91: 'Protocol wrong type for socket',
-  92: 'Protocol not available',
-  93: 'Unknown protocol',
-  94: 'Socket type not supported',
-  95: 'Not supported',
-  96: 'Protocol family not supported',
-  97: 'Address family not supported by protocol family',
-  98: 'Address already in use',
-  99: 'Address not available',
-  100: 'Network interface is not configured',
-  101: 'Network is unreachable',
-  102: 'Connection reset by network',
-  103: 'Connection aborted',
-  104: 'Connection reset by peer',
-  105: 'No buffer space available',
-  106: 'Socket is already connected',
-  107: 'Socket is not connected',
-  108: "Can't send after socket shutdown",
-  109: 'Too many references',
-  110: 'Connection timed out',
-  111: 'Connection refused',
-  112: 'Host is down',
-  113: 'Host is unreachable',
-  114: 'Socket already connected',
-  115: 'Connection already in progress',
-  116: 'Stale file handle',
-  122: 'Quota exceeded',
-  123: 'No medium (in tape drive)',
-  125: 'Operation canceled',
-  130: 'Previous owner died',
-  131: 'State not recoverable',
-}
-
-export function errnoMessage(errno?: number): string {
-  return (
-    ERRNO_CODES[-(errno as number) as keyof typeof ERRNO_CODES] ||
-    'Unknown error'
-  )
-}
-
 export type Duration = number | `${number}m` | `${number}s` | `${number}ms`
 
 export function parseDuration(d: Duration) {
@@ -427,19 +259,6 @@ const RESERVED_WORDS = new Set([
   'in',
 ])
 
-export function getCallerLocation(err = new Error()) {
-  return getCallerLocationFromString(err.stack)
-}
-
-export function getCallerLocationFromString(stackString = 'unknown') {
-  return (
-    stackString
-      .split(/^\s*(at\s)?/m)
-      .filter((s) => s?.includes(':'))[2]
-      ?.trim() || stackString
-  )
-}
-
 export const once = <T extends (...args: any[]) => any>(fn: T) => {
   let called = false
   let result: ReturnType<T>
test/all.test.js
@@ -15,6 +15,7 @@
 import './cli.test.js'
 import './core.test.js'
 import './deps.test.js'
+import './error.test.ts'
 import './global.test.js'
 import './goods.test.js'
 import './index.test.js'
test/core.test.js
@@ -997,6 +997,27 @@ describe('core', () => {
       assert.throws(() => o.blob(), /Blob is not supported/)
       globalThis.Blob = Blob
     })
+
+    describe('static', () => {
+      test('getExitMessage()', () => {
+        assert.match(
+          ProcessOutput.getExitMessage(2, null, '', ''),
+          /Misuse of shell builtins/
+        )
+      })
+
+      test('getErrorMessage()', () => {
+        assert.match(
+          ProcessOutput.getErrorMessage({ errno: -2 }, ''),
+          /No such file or directory/
+        )
+        assert.match(
+          ProcessOutput.getErrorMessage({ errno: -1e9 }, ''),
+          /Unknown error/
+        )
+        assert.match(ProcessOutput.getErrorMessage({}, ''), /Unknown error/)
+      })
+    })
   })
 
   describe('cd()', () => {
test/error.test.ts
@@ -0,0 +1,112 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import assert from 'node:assert'
+import { test, describe } from 'node:test'
+import {
+  getErrnoMessage,
+  getExitCodeInfo,
+  getCallerLocation,
+  getCallerLocationFromString,
+  formatExitMessage,
+  formatErrorMessage,
+} from '../src/error.ts'
+
+describe('error', () => {
+  test('getExitCodeInfo()', () => {
+    assert.equal(getExitCodeInfo(2), 'Misuse of shell builtins')
+  })
+
+  test('getErrnoMessage()', () => {
+    assert.equal(getErrnoMessage(-2), 'No such file or directory')
+    assert.equal(getErrnoMessage(1e9), 'Unknown error')
+    assert.equal(getErrnoMessage(undefined), 'Unknown error')
+  })
+
+  describe('getCallerLocation()', () => {
+    assert.match(getCallerLocation(new Error('Foo')), /Suite\.runInAsyncScope/)
+  })
+
+  describe('getCallerLocationFromString()', () => {
+    test('empty', () => {
+      assert.equal(getCallerLocationFromString(), 'unknown')
+    })
+
+    test('no-match', () => {
+      assert.equal(
+        getCallerLocationFromString('stack\nstring'),
+        'stack\nstring'
+      )
+    })
+
+    test(`getCallerLocationFromString-v8`, () => {
+      const stack = `
+    Error
+      at getCallerLocation (/Users/user/test.js:22:17)
+      at e (/Users/user/test.js:34:13)
+      at d (/Users/user/test.js:11:5)
+      at c (/Users/user/test.js:8:5)
+      at b (/Users/user/test.js:5:5)
+      at a (/Users/user/test.js:2:5)
+      at Object.<anonymous> (/Users/user/test.js:37:1)
+      at Module._compile (node:internal/modules/cjs/loader:1254:14)
+      at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
+      at Module.load (node:internal/modules/cjs/loader:1117:32)
+      at Module._load (node:internal/modules/cjs/loader:958:12)
+    `
+      assert.match(getCallerLocationFromString(stack), /^.*:11:5.*$/)
+    })
+
+    test(`getCallerLocationFromString-JSC`, () => {
+      const stack = `
+    getCallerLocation@/Users/user/test.js:22:17
+    e@/Users/user/test.js:34:13
+    d@/Users/user/test.js:11:5
+    c@/Users/user/test.js:8:5
+    b@/Users/user/test.js:5:5
+    a@/Users/user/test.js:2:5
+    module code@/Users/user/test.js:37:1
+    evaluate@[native code]
+    moduleEvaluation@[native code]
+    moduleEvaluation@[native code]
+    @[native code]
+    asyncFunctionResume@[native code]
+    promiseReactionJobWithoutPromise@[native code]
+    promiseReactionJob@[native code]
+    d@/Users/user/test.js:11:5
+  `
+      assert.match(getCallerLocationFromString(stack), /^.*:11:5.*$/)
+    })
+  })
+
+  test('getExitMessage()', () => {
+    assert.match(formatExitMessage(2, null, '', ''), /Misuse of shell builtins/)
+    assert.match(formatExitMessage(1, 'SIGKILL', '', ''), /SIGKILL/)
+  })
+
+  test('getErrorMessage()', () => {
+    assert.match(
+      formatErrorMessage({ errno: -2 } as NodeJS.ErrnoException, ''),
+      /No such file or directory/
+    )
+    assert.match(
+      formatErrorMessage({ errno: -1e9 } as NodeJS.ErrnoException, ''),
+      /Unknown error/
+    )
+    assert.match(
+      formatErrorMessage({} as NodeJS.ErrnoException, ''),
+      /Unknown error/
+    )
+  })
+})
test/util.test.js
@@ -16,8 +16,6 @@ import assert from 'node:assert'
 import fs from 'node:fs'
 import { test, describe } from 'node:test'
 import {
-  exitCodeInfo,
-  errnoMessage,
   formatCmd,
   isString,
   isStringLiteral,
@@ -27,7 +25,6 @@ import {
   quotePowerShell,
   randomId,
   // normalizeMultilinePieces,
-  getCallerLocationFromString,
   tempdir,
   tempfile,
   preferLocalBin,
@@ -36,16 +33,6 @@ import {
 } from '../build/util.js'
 
 describe('util', () => {
-  test('exitCodeInfo()', () => {
-    assert.equal(exitCodeInfo(2), 'Misuse of shell builtins')
-  })
-
-  test('errnoMessage()', () => {
-    assert.equal(errnoMessage(-2), 'No such file or directory')
-    assert.equal(errnoMessage(1e9), 'Unknown error')
-    assert.equal(errnoMessage(undefined), 'Unknown error')
-  })
-
   test('randomId()', () => {
     assert.ok(/^[a-z0-9]+$/.test(randomId()))
     assert.ok(
@@ -120,90 +107,43 @@ describe('util', () => {
   //     ' a ,b c d, e'
   //   )
   // })
-})
-
-test('getCallerLocation: empty', () => {
-  assert.equal(getCallerLocationFromString(), 'unknown')
-})
-
-test('getCallerLocation: no-match', () => {
-  assert.equal(getCallerLocationFromString('stack\nstring'), 'stack\nstring')
-})
-
-test(`getCallerLocationFromString-v8`, () => {
-  const stack = `
-    Error
-      at getCallerLocation (/Users/user/test.js:22:17)
-      at e (/Users/user/test.js:34:13)
-      at d (/Users/user/test.js:11:5)
-      at c (/Users/user/test.js:8:5)
-      at b (/Users/user/test.js:5:5)
-      at a (/Users/user/test.js:2:5)
-      at Object.<anonymous> (/Users/user/test.js:37:1)
-      at Module._compile (node:internal/modules/cjs/loader:1254:14)
-      at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
-      at Module.load (node:internal/modules/cjs/loader:1117:32)
-      at Module._load (node:internal/modules/cjs/loader:958:12)
-    `
-  assert.match(getCallerLocationFromString(stack), /^.*:11:5.*$/)
-})
-
-test(`getCallerLocationFromString-JSC`, () => {
-  const stack = `
-    getCallerLocation@/Users/user/test.js:22:17
-    e@/Users/user/test.js:34:13
-    d@/Users/user/test.js:11:5
-    c@/Users/user/test.js:8:5
-    b@/Users/user/test.js:5:5
-    a@/Users/user/test.js:2:5
-    module code@/Users/user/test.js:37:1
-    evaluate@[native code]
-    moduleEvaluation@[native code]
-    moduleEvaluation@[native code]
-    @[native code]
-    asyncFunctionResume@[native code]
-    promiseReactionJobWithoutPromise@[native code]
-    promiseReactionJob@[native code]
-    d@/Users/user/test.js:11:5
-  `
-  assert.match(getCallerLocationFromString(stack), /^.*:11:5.*$/)
-})
 
-test('tempdir() creates temporary folders', () => {
-  assert.match(tempdir(), /\/zx-/)
-  assert.match(tempdir('foo'), /\/foo$/)
-})
+  test('tempdir() creates temporary folders', () => {
+    assert.match(tempdir(), /\/zx-/)
+    assert.match(tempdir('foo'), /\/foo$/)
+  })
 
-test('tempfile() creates temporary files', () => {
-  assert.match(tempfile(), /\/zx-.+/)
-  assert.match(tempfile('foo.txt'), /\/zx-.+\/foo\.txt$/)
+  test('tempfile() creates temporary files', () => {
+    assert.match(tempfile(), /\/zx-.+/)
+    assert.match(tempfile('foo.txt'), /\/zx-.+\/foo\.txt$/)
 
-  const tf = tempfile('bar.txt', 'bar')
-  assert.match(tf, /\/zx-.+\/bar\.txt$/)
-  assert.equal(fs.readFileSync(tf, 'utf-8'), 'bar')
-})
+    const tf = tempfile('bar.txt', 'bar')
+    assert.match(tf, /\/zx-.+\/bar\.txt$/)
+    assert.equal(fs.readFileSync(tf, 'utf-8'), 'bar')
+  })
 
-test('preferLocalBin()', () => {
-  const env = {
-    PATH: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/sbin',
-  }
-  const _env = preferLocalBin(env, process.cwd())
-  assert.equal(
-    _env.PATH,
-    `${process.cwd()}/node_modules/.bin:${process.cwd()}:${env.PATH}`
-  )
-})
+  test('preferLocalBin()', () => {
+    const env = {
+      PATH: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/sbin',
+    }
+    const _env = preferLocalBin(env, process.cwd())
+    assert.equal(
+      _env.PATH,
+      `${process.cwd()}/node_modules/.bin:${process.cwd()}:${env.PATH}`
+    )
+  })
 
-test('camelToSnake()', () => {
-  assert.equal(camelToSnake('verbose'), 'VERBOSE')
-  assert.equal(camelToSnake('nothrow'), 'NOTHROW')
-  assert.equal(camelToSnake('preferLocal'), 'PREFER_LOCAL')
-  assert.equal(camelToSnake('someMoreBigStr'), 'SOME_MORE_BIG_STR')
-})
+  test('camelToSnake()', () => {
+    assert.equal(camelToSnake('verbose'), 'VERBOSE')
+    assert.equal(camelToSnake('nothrow'), 'NOTHROW')
+    assert.equal(camelToSnake('preferLocal'), 'PREFER_LOCAL')
+    assert.equal(camelToSnake('someMoreBigStr'), 'SOME_MORE_BIG_STR')
+  })
 
-test('snakeToCamel()', () => {
-  assert.equal(snakeToCamel('VERBOSE'), 'verbose')
-  assert.equal(snakeToCamel('NOTHROW'), 'nothrow')
-  assert.equal(snakeToCamel('PREFER_LOCAL'), 'preferLocal')
-  assert.equal(snakeToCamel('SOME_MORE_BIG_STR'), 'someMoreBigStr')
+  test('snakeToCamel()', () => {
+    assert.equal(snakeToCamel('VERBOSE'), 'verbose')
+    assert.equal(snakeToCamel('NOTHROW'), 'nothrow')
+    assert.equal(snakeToCamel('PREFER_LOCAL'), 'preferLocal')
+    assert.equal(snakeToCamel('SOME_MORE_BIG_STR'), 'someMoreBigStr')
+  })
 })
package.json
@@ -63,10 +63,10 @@
     "fmt": "prettier --write .",
     "fmt:check": "prettier --check .",
     "build": "npm run build:js && npm run build:dts && npm run build:tests",
-    "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts && npm run build:vendor",
+    "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts:!src/error.ts && npm run build:vendor",
     "build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor-*.ts --bundle=all",
     "build:tests": "node scripts/build-tests.mjs",
-    "build:dts": "tsc --project tsconfig.prod.json && node scripts/build-dts.mjs",
+    "build:dts": "tsc --project tsconfig.prod.json && rm build/error.d.ts && node scripts/build-dts.mjs",
     "docs:dev": "vitepress dev docs",
     "docs:build": "vitepress build docs",
     "docs:preview": "vitepress preview docs",
@@ -74,7 +74,7 @@
     "test": "npm run test:size && npm run fmt:check && npm run test:unit && npm run test:types && npm run test:license",
     "test:it": "node ./test/it/build.test.js",
     "test:jsr": "node ./test/it/build-jsr.test.js",
-    "test:unit": "node ./test/all.test.js",
+    "test:unit": "node --experimental-strip-types ./test/all.test.js",
     "test:coverage": "c8 -x build/deno.js -x build/vendor-extra.cjs -x build/vendor-core.cjs -x build/esblib.cjs -x 'test/**' -x scripts --check-coverage npm run test:unit",
     "test:circular": "madge --circular src/*",
     "test:types": "tsd",