unknown-error 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +189 -2
- package/WHY-UNKNOWN-ERROR.md +91 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/lib/safe.d.ts +56 -0
- package/dist/lib/safe.js +88 -0
- package/dist/lib/to-error.d.ts +43 -0
- package/dist/lib/to-error.js +59 -0
- package/dist/lib/walk.d.ts +61 -0
- package/dist/lib/walk.js +88 -0
- package/dist/to-error.d.ts +2 -0
- package/dist/to-error.js +3 -0
- package/dist/walk.d.ts +3 -0
- package/dist/walk.js +3 -0
- package/package.json +61 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jacob Ley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,190 @@
|
|
|
1
|
-
|
|
1
|
+
<div style="text-align:center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# unknown-error
|
|
4
|
+
Turn any caught `unknown` into a real Error without throwing, even for hostile values, other realms and deep or circular cause chains.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/unknown-error)
|
|
7
|
+
[](https://github.com/JacobLey/leyman/blob/main/tools/unknown-error/LICENSE)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
- **Never throws**: Safe on Proxies that throw from every trap, throwing getters, and `toJSON` or `toString` that throw. Handling an error can't raise a new one inside your `catch`.
|
|
12
|
+
- **Any realm, any runtime**: Recognizes Errors from `vm`, iframes and workers, where `instanceof Error` fails. Runs in Node.js, Deno, Bun and browsers, using `Error.isError` where it exists.
|
|
13
|
+
- **Keeps what it can**: Rebuilds error-shaped objects (from JSON, RPC or `postMessage`) with their name, stack, cause and `code`. Describes anything else readably, including circular objects and BigInts, instead of `[object Object]`.
|
|
14
|
+
- **Whole error trees**: Walks `cause` chains and `AggregateError` entries without recursion, so circular or 100,000-deep chains end instead of overflowing the stack.
|
|
15
|
+
- **Doesn't modify your errors**: Errors are returned as they are, never patched in place.
|
|
16
|
+
|
|
17
|
+
| | unknown-error | [normalize-exception](https://www.npmjs.com/package/normalize-exception) | [ensure-error](https://www.npmjs.com/package/ensure-error) | [serialize-error](https://www.npmjs.com/package/serialize-error) | [pony-cause](https://www.npmjs.com/package/pony-cause) |
|
|
18
|
+
|---|---|---|---|---|---|
|
|
19
|
+
| Never throws on hostile values | ✅ | ✅ | ❌ | ❌ | — |
|
|
20
|
+
| Errors from other realms | ✅ | ✅ | ❌ | ✅ | ❌ |
|
|
21
|
+
| Readable circular and BigInt values | ✅ | ❌ | ❌ | ❌ | — |
|
|
22
|
+
| Leaves your errors unmodified | ✅ | ❌ | ❌ | ✅ | — |
|
|
23
|
+
| Causes and `AggregateError` | ✅ | ✅ | ✅ | — | Causes only |
|
|
24
|
+
| 10,000-deep cause chains | ✅ | ❌ | ❌ | — | ❌ |
|
|
25
|
+
|
|
26
|
+
For the problems with hand-written conversions, the full comparison and design decisions, see [WHY-UNKNOWN-ERROR.md](https://github.com/JacobLey/leyman/blob/main/tools/unknown-error/WHY-UNKNOWN-ERROR.md).
|
|
27
|
+
|
|
28
|
+
## Contents
|
|
29
|
+
- [Install](#install)
|
|
30
|
+
- [Example](#example)
|
|
31
|
+
- [Usage](#usage)
|
|
32
|
+
- [API](#api)
|
|
33
|
+
- [`toError(value)`](#toerrorvalue)
|
|
34
|
+
- [`getErrorMessage(value)`](#geterrormessagevalue)
|
|
35
|
+
- [`hasCode(value, code)`](#hascodevalue-code)
|
|
36
|
+
- [`NonError`](#nonerror)
|
|
37
|
+
- [`walkErrors(value, options?)`](#walkerrorsvalue-options)
|
|
38
|
+
- [`formatError(value, options?)`](#formaterrorvalue-options)
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm i unknown-error
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Example
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { formatError, getErrorMessage, hasCode, toError } from 'unknown-error';
|
|
50
|
+
|
|
51
|
+
export const loadConfig = async (): Promise<Config> => {
|
|
52
|
+
try {
|
|
53
|
+
return await readConfig();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (hasCode(err, 'ENOENT')) {
|
|
56
|
+
// `err` is `Error & { readonly code: 'ENOENT' }`
|
|
57
|
+
return defaultConfig;
|
|
58
|
+
}
|
|
59
|
+
console.error(formatError(err));
|
|
60
|
+
throw new Error(`Failed to read config: ${getErrorMessage(err)}`, { cause: toError(err) });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
ESM only. Runs in Node.js 24 or later, Deno, Bun and browsers, with no Node.js APIs. From CommonJS, load it with a dynamic import: `const { toError } = await import('unknown-error');`
|
|
68
|
+
|
|
69
|
+
Errors are recognized with [`Error.isError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError) where the runtime has it: Node.js 24, Chrome 134, Firefox 138, Safari 18.4 and later. In older browsers, they are recognized by `instanceof Error` or their `[object Error]` tag instead, which works across realms too, but also lets through a Proxy around an Error and an object with a forged `Symbol.toStringTag`.
|
|
70
|
+
|
|
71
|
+
In Safari and in Node.js before 24.3, `Error.isError` returns `false` for a `DOMException`, so there a `DOMException` is rebuilt as a plain Error with the same name and message rather than returned as-is.
|
|
72
|
+
|
|
73
|
+
Every function takes `unknown`, so pass the `catch` variable straight in. None of them throw, whatever the value.
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `toError(value)`
|
|
78
|
+
|
|
79
|
+
Converts any caught value to an Error.
|
|
80
|
+
|
|
81
|
+
| `value` | Result |
|
|
82
|
+
|---------|--------|
|
|
83
|
+
| An Error, including one from another realm | The same Error, unmodified |
|
|
84
|
+
| An object with a string `message` | A new `Error` with the object's `message`, `name`, `stack` and `cause`, and its other own properties (like `code`) copied over |
|
|
85
|
+
| Anything else | A new [`NonError`](#nonerror) describing it |
|
|
86
|
+
|
|
87
|
+
**Returns** `Error`.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
toError(new TypeError('x')); // the same TypeError
|
|
91
|
+
toError({ name: 'HttpError', message: 'Not found', status: 404 });
|
|
92
|
+
// HttpError: Not found, with `status: 404`
|
|
93
|
+
toError('boom'); // NonError: boom
|
|
94
|
+
toError({ count: 1n }); // NonError: {"count":"1n"}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
An error-like object without a `stack` gets a stack pointing at the `toError` call, since the original location is unknown.
|
|
98
|
+
|
|
99
|
+
### `getErrorMessage(value)`
|
|
100
|
+
|
|
101
|
+
The message of `toError(value)`.
|
|
102
|
+
|
|
103
|
+
**Returns** `string`. A `message` that isn't a string is stringified, and one whose getter throws gives `'[unreadable message]'`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
getErrorMessage(new Error('boom')); // 'boom'
|
|
107
|
+
getErrorMessage(404); // '404'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `hasCode(value, code)`
|
|
111
|
+
|
|
112
|
+
Whether `value` is an Error whose `code` property equals `code`. A type guard, so it narrows `value` to `Error & { readonly code: typeof code }`.
|
|
113
|
+
|
|
114
|
+
| Parameter | Type | Default | Description |
|
|
115
|
+
|-----------|------|---------|-------------|
|
|
116
|
+
| `value` | `unknown` | — | A caught value. |
|
|
117
|
+
| `code` | `string \| number` | — | The code to match, like `'ENOENT'`. |
|
|
118
|
+
|
|
119
|
+
**Returns** `boolean`.
|
|
120
|
+
|
|
121
|
+
Only real Errors match. To match error-like objects too, convert them first: `hasCode(toError(err), 'E_TIMEOUT')`.
|
|
122
|
+
|
|
123
|
+
### `NonError`
|
|
124
|
+
|
|
125
|
+
`class NonError extends Error`. Wraps a thrown value that was neither an Error nor shaped like one.
|
|
126
|
+
|
|
127
|
+
#### `new NonError(thrown, options?)`
|
|
128
|
+
|
|
129
|
+
| Parameter | Type | Default | Description |
|
|
130
|
+
|-----------|------|---------|-------------|
|
|
131
|
+
| `thrown` | `unknown` | — | The value that was thrown. |
|
|
132
|
+
| `options` | `ErrorOptions` | — | Standard Error options, such as `cause`. |
|
|
133
|
+
|
|
134
|
+
#### Properties
|
|
135
|
+
|
|
136
|
+
| Property | Type | Description |
|
|
137
|
+
|----------|------|-------------|
|
|
138
|
+
| `name` | `'NonError'` | |
|
|
139
|
+
| `message` | `string` | A description of `thrown`: strings as-is, objects as JSON (with circular references and BigInts handled), functions as `[Function name]`. Cut to 1,000 characters. |
|
|
140
|
+
| `thrown` | `unknown` | The original value. |
|
|
141
|
+
|
|
142
|
+
### `walkErrors(value, options?)`
|
|
143
|
+
|
|
144
|
+
Visits `value`, its `cause` chain and the `errors` of any `AggregateError`, depth first: an error, then its cause, then its `errors` in order.
|
|
145
|
+
|
|
146
|
+
Every value is converted with `toError`, so a cause that isn't an Error still appears. A value already visited is skipped, so circular chains end.
|
|
147
|
+
|
|
148
|
+
| Parameter | Type | Default | Description |
|
|
149
|
+
|-----------|------|---------|-------------|
|
|
150
|
+
| `value` | `unknown` | — | A caught value. |
|
|
151
|
+
| `options.maxDepth` | `number` | `32` | Deepest level to visit. The root is level `0`. Pass `Infinity` for no limit. |
|
|
152
|
+
|
|
153
|
+
**Returns** `Generator<ErrorNode>`.
|
|
154
|
+
|
|
155
|
+
#### `ErrorNode`
|
|
156
|
+
|
|
157
|
+
| Property | Type | Description |
|
|
158
|
+
|----------|------|-------------|
|
|
159
|
+
| `error` | `Error` | The value, converted with `toError`. |
|
|
160
|
+
| `depth` | `number` | Distance from the root, which is `0`. |
|
|
161
|
+
| `relation` | `'root' \| 'cause' \| 'aggregate'` | How it relates to its parent: the value passed in, its parent's `cause`, or an entry in its parent's `errors`. |
|
|
162
|
+
| `truncated` | `boolean` | `true` when `maxDepth` stopped the walk here, so this error's own cause and errors were skipped. |
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const error = new AggregateError([new Error('a'), 'b'], 'all', { cause: new Error('why') });
|
|
166
|
+
|
|
167
|
+
for (const { error: node, depth, relation } of walkErrors(error)) {
|
|
168
|
+
console.log(depth, relation, node.message);
|
|
169
|
+
}
|
|
170
|
+
// 0 root all
|
|
171
|
+
// 1 cause why
|
|
172
|
+
// 1 aggregate a
|
|
173
|
+
// 1 aggregate b
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### `formatError(value, options?)`
|
|
177
|
+
|
|
178
|
+
Formats an error with every cause and `AggregateError` entry, each indented under its parent. Takes the same options as [`walkErrors`](#walkerrorsvalue-options).
|
|
179
|
+
|
|
180
|
+
**Returns** `string`.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
console.log(formatError(new Error('request failed', { cause: new Error('ECONNREFUSED') })));
|
|
184
|
+
// Error: request failed
|
|
185
|
+
// at ...
|
|
186
|
+
// Caused by: Error: ECONNREFUSED
|
|
187
|
+
// at ...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Causes are labelled `Caused by:`, and `AggregateError` entries `-`. Each error is printed as its `stack`, with a `Name: message` line added first when the stack doesn't start with one (as in Firefox and Safari). Where `maxDepth` cut the walk short, a `… more causes omitted` line says so.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<div style="text-align:center">
|
|
2
|
+
|
|
3
|
+
# Why unknown-error?
|
|
4
|
+
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
In TypeScript's `strict` mode, a `catch` variable is `unknown`, and with reason: JavaScript can throw anything. To log it, report it or wrap it, you need an Error, so nearly every codebase ends up with something like this:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const toError = (value: unknown): Error =>
|
|
13
|
+
value instanceof Error ? value : new Error(String(value));
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
It handles the cases that come to mind first: an Error, or a thrown string. It falls apart on the rest, and it fails inside the `catch` block that was meant to handle the failure.
|
|
17
|
+
|
|
18
|
+
### Errors from another realm aren't `instanceof Error`
|
|
19
|
+
|
|
20
|
+
Each realm (a `vm` context, an iframe, some test runners) has its own `Error` class. An Error from another realm fails `instanceof Error`, so the code above turns a perfectly good `TypeError` into `new Error("TypeError: x")`, losing its name, stack, cause and properties.
|
|
21
|
+
|
|
22
|
+
### Errors that crossed a boundary are plain objects
|
|
23
|
+
|
|
24
|
+
An error sent through JSON, `postMessage` or an RPC framework arrives as `{ name, message, stack, code }`. `String()` turns it into `Error: [object Object]`.
|
|
25
|
+
|
|
26
|
+
### Thrown objects become `[object Object]`
|
|
27
|
+
|
|
28
|
+
`throw { status: 404, reason: 'missing' }` is bad practice, but libraries do it. `String()` loses everything, and `JSON.stringify` throws on circular references and BigInts, which is why most conversions don't use it.
|
|
29
|
+
|
|
30
|
+
### The handler itself can throw
|
|
31
|
+
|
|
32
|
+
The thrown value controls what runs when you inspect it. A Proxy can throw from every trap, a `message` getter can throw, and so can `toString` and `toJSON`. `String(value)`, `value.message` and even `value instanceof Error` (which reads the prototype) run that code, so the conversion throws inside your `catch` and replaces the original error with a new one.
|
|
33
|
+
|
|
34
|
+
### Cause chains and AggregateError are trees
|
|
35
|
+
|
|
36
|
+
`cause` can be anything, including a non-Error or another realm's Error, and it can be circular. `AggregateError` adds a list of errors at each level. Code that follows `cause` with a `while` loop hangs on a cycle, and code that recurses overflows the call stack on a deep enough chain.
|
|
37
|
+
|
|
38
|
+
## Why Existing Solutions Fall Short
|
|
39
|
+
|
|
40
|
+
Every claim below is checked by the [comparison suite](https://github.com/JacobLey/leyman/blob/main/e2e/unknown-error-comparison/test/integration/comparison.spec.ts), which runs each library through the same cases.
|
|
41
|
+
|
|
42
|
+
- **[`normalize-exception`](https://www.npmjs.com/package/normalize-exception)** comes closest. It handles other realms, error-like objects and hostile values, and normalizes causes and `AggregateError` entries. But it modifies the error you pass in (replacing a non-Error `cause` in place), describes circular objects and objects containing BigInts as `[object Object]`, and recurses, so a 10,000-deep cause chain overflows the stack.
|
|
43
|
+
- **[`ensure-error`](https://www.npmjs.com/package/ensure-error)** uses `instanceof`, so it wraps another realm's Error in a `NonError` whose message is `{}`. It doesn't recognize error-like objects, throws on a Proxy or a throwing `message` getter, and also patches your error in place. It recurses too, and builds a new stack at every level, which gets slow on deep chains when `Error.stackTraceLimit` is raised (as Mocha does).
|
|
44
|
+
- **[`serialize-error`](https://www.npmjs.com/package/serialize-error)** is built to turn errors into JSON and back. Its `deserializeError` rebuilds error-like objects well, but prefixes thrown strings (`Non-error value: boom`), throws on hostile values, and doesn't walk causes.
|
|
45
|
+
- **[`pony-cause`](https://www.npmjs.com/package/pony-cause)** only formats `cause` chains of real Errors: it drops causes that aren't Errors (including other realms' Errors), ignores `AggregateError`, and recurses.
|
|
46
|
+
- **Hand-written conversions** like the one above fail most of these cases, and throw on three of them.
|
|
47
|
+
|
|
48
|
+
## How unknown-error Is Different
|
|
49
|
+
|
|
50
|
+
- **Every read is guarded.** Property reads, key listing and stringifying all catch errors from getters and Proxy traps, with fallbacks down to a fixed `[unprintable value]`. No function in the package throws, whatever it's given.
|
|
51
|
+
- **`Error.isError` where it exists.** It recognizes Errors from any realm, and can't be fooled by a Proxy or run any of its traps. Where it's missing (Chrome before 134, Firefox before 138, Safari before 18.4), a guarded `instanceof` and object-tag check takes over.
|
|
52
|
+
- **Your errors are never modified.** An Error is returned as it is. Error-like objects are copied into a new Error, and everything else is wrapped in a `NonError` that keeps the original value as `thrown`.
|
|
53
|
+
- **Readable descriptions.** Non-Errors are described as JSON with circular references marked `[Circular]` and BigInts kept as `1n`, falling back to `toString` and then the object's tag.
|
|
54
|
+
- **A walk, not recursion.** `walkErrors` uses an explicit stack, skips anything it has already visited, and stops at `maxDepth` (32 by default), marking where it stopped. `formatError` is built on it.
|
|
55
|
+
|
|
56
|
+
## Comparison
|
|
57
|
+
|
|
58
|
+
From the [comparison suite](https://github.com/JacobLey/leyman/blob/main/e2e/unknown-error-comparison/test/integration/comparison.spec.ts). ✅ handles it, ❌ doesn't, 💥 throws, — has no API for it.
|
|
59
|
+
|
|
60
|
+
| Case | unknown-error | normalize-exception | ensure-error | serialize-error | pony-cause | Hand-written |
|
|
61
|
+
|---|---|---|---|---|---|---|
|
|
62
|
+
| Uses a thrown string as the message, unchanged | ✅ | ✅ | ❌ quotes it | ❌ prefixes it | — | ✅ |
|
|
63
|
+
| Converts a thrown `null` | ✅ | ✅ | ✅ | ✅ | — | ✅ |
|
|
64
|
+
| Keeps an Error from another realm | ✅ | ✅ | ❌ | ✅ | — | ❌ |
|
|
65
|
+
| Rebuilds an error-like object with its name, message, code and stack | ✅ | ✅ | ❌ | ✅ | — | ❌ |
|
|
66
|
+
| Describes a circular object | ✅ | ❌ | ❌ | ❌ | — | ❌ |
|
|
67
|
+
| Describes an object containing a BigInt | ✅ | ❌ | ❌ | ❌ | — | ❌ |
|
|
68
|
+
| Converts an object whose `toJSON` and `toString` throw | ✅ | ✅ | ✅ | ✅ | — | 💥 |
|
|
69
|
+
| Converts a Proxy that throws from every trap | ✅ | ✅ | 💥 | 💥 | — | 💥 |
|
|
70
|
+
| Reads the message of an Error whose `message` getter throws | ✅ | ✅ | 💥 | 💥 | — | 💥 |
|
|
71
|
+
| Leaves the Error passed in unmodified | ✅ | ❌ | ❌ | ✅ | — | ✅ |
|
|
72
|
+
| Includes a cause that is not an Error | ✅ | ✅ | ✅ | — | ❌ | — |
|
|
73
|
+
| Includes a cause from another realm | ✅ | ✅ | ❌ | — | ❌ | — |
|
|
74
|
+
| Ends a circular cause chain | ✅ | ✅ | ✅ | — | ✅ | — |
|
|
75
|
+
| Includes nested `AggregateError` entries | ✅ | ✅ | ✅ | — | ❌ | — |
|
|
76
|
+
| Handles a 10,000-deep cause chain | ✅ | 💥 | 💥 | — | 💥 | — |
|
|
77
|
+
|
|
78
|
+
## Design Decisions
|
|
79
|
+
|
|
80
|
+
- **Rebuild error-like objects, don't wrap them.** An object with a string `message` is almost always an error that lost its prototype on the way (through JSON or `postMessage`), so it becomes a plain `Error` with its name, stack, cause and other properties. Wrapping it in a `NonError` would bury its message in JSON.
|
|
81
|
+
- **Only real Errors match `hasCode`.** It narrows to `Error & { code }`, which would be a lie for a plain object. Convert first, with `hasCode(toError(err), code)`, to match error-like objects.
|
|
82
|
+
- **A depth limit by default.** A cause chain dozens of levels deep is almost always a bug (like an error wrapped on every retry), and printing all of it buries the useful part. `maxDepth` is 32, the walk says where it stopped, and `Infinity` turns the limit off.
|
|
83
|
+
- **A fallback for `Error.isError`, not a requirement.** It is the only way to recognize an Error from another realm without running a Proxy's traps, but browsers only shipped it in 2025 (Chrome 134, Firefox 138, Safari 18.4), so older ones lack it. Without it, `instanceof Error` and the `[object Error]` tag still recognize Errors from any realm, inside a `try` so a Proxy can't make them throw. The cost is that a Proxy around an Error, or an object with a forged `Symbol.toStringTag`, also passes. It is read on every call, so a polyfill loaded later is used.
|
|
84
|
+
|
|
85
|
+
## Non-Goals
|
|
86
|
+
|
|
87
|
+
- **Serializing errors.** To send an error over JSON, use [`serialize-error`](https://www.npmjs.com/package/serialize-error). unknown-error rebuilds what it receives on the other side.
|
|
88
|
+
- **Error class hierarchies.** It converts and inspects errors, and has no opinion on how you define them.
|
|
89
|
+
- **Recovering the throw location of non-Errors.** A thrown string has no stack. A `NonError`'s stack points at the `toError` call, the closest place that is known.
|
|
90
|
+
|
|
91
|
+
Back to the [README](https://www.npmjs.com/package/unknown-error).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers that inspect a thrown value without ever throwing themselves.
|
|
3
|
+
*
|
|
4
|
+
* Anything can be thrown, including objects with throwing getters, a throwing `toString` and Proxies
|
|
5
|
+
* that throw from every trap. Every read of a caught value goes through these helpers, so that
|
|
6
|
+
* handling an error can't raise a new one from inside a `catch`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Whether `value` can have properties: objects, arrays and functions.
|
|
10
|
+
*
|
|
11
|
+
* @param value - any value
|
|
12
|
+
* @returns `true` for non-null objects and functions
|
|
13
|
+
*/
|
|
14
|
+
export declare const isObject: (value: unknown) => value is object;
|
|
15
|
+
/**
|
|
16
|
+
* Whether `value` is an Error, including one from another realm (`vm`, iframes). Never throws.
|
|
17
|
+
*
|
|
18
|
+
* Uses `Error.isError` where the runtime has it, which recognizes every realm's Errors without
|
|
19
|
+
* running any Proxy trap. It is read on each call rather than once, so a polyfill loaded later is
|
|
20
|
+
* still used.
|
|
21
|
+
*
|
|
22
|
+
* @param value - any value
|
|
23
|
+
* @returns whether `value` is an Error
|
|
24
|
+
*/
|
|
25
|
+
export declare const isError: (value: unknown) => value is Error;
|
|
26
|
+
/**
|
|
27
|
+
* Reads a property, returning `undefined` when a getter or Proxy trap throws.
|
|
28
|
+
*
|
|
29
|
+
* @param value - object to read from
|
|
30
|
+
* @param key - property to read
|
|
31
|
+
* @returns the property's value, or `undefined` if reading it threw
|
|
32
|
+
*/
|
|
33
|
+
export declare const safeGet: (value: object, key: PropertyKey) => unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Checks for a property (own or inherited), returning `false` when a Proxy `has` trap throws.
|
|
36
|
+
*
|
|
37
|
+
* @param value - object to check
|
|
38
|
+
* @param key - property to look for
|
|
39
|
+
* @returns whether the property exists
|
|
40
|
+
*/
|
|
41
|
+
export declare const hasProperty: (value: object, key: PropertyKey) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Lists own enumerable string keys, returning none when a Proxy `ownKeys` trap throws.
|
|
44
|
+
*
|
|
45
|
+
* @param value - object to list
|
|
46
|
+
* @returns its keys
|
|
47
|
+
*/
|
|
48
|
+
export declare const safeKeys: (value: object) => string[];
|
|
49
|
+
/**
|
|
50
|
+
* Describes any value as a string, without throwing.
|
|
51
|
+
*
|
|
52
|
+
* @param value - any value
|
|
53
|
+
* @returns a readable description
|
|
54
|
+
*/
|
|
55
|
+
export declare const safeStringify: (value: unknown) => string;
|
|
56
|
+
//# sourceMappingURL=safe.d.ts.map
|
package/dist/lib/safe.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const isObject = (value)=>typeof value === 'object' && value !== null || typeof value === 'function';
|
|
2
|
+
const isErrorFallback = (value)=>{
|
|
3
|
+
if (!isObject(value)) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
return value instanceof Error || Object.prototype.toString.call(value) === '[object Error]';
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export const isError = (value)=>{
|
|
13
|
+
const native = Error.isError;
|
|
14
|
+
return typeof native === 'function' ? native(value) : isErrorFallback(value);
|
|
15
|
+
};
|
|
16
|
+
export const safeGet = (value, key)=>{
|
|
17
|
+
try {
|
|
18
|
+
return Reflect.get(value, key);
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const hasProperty = (value, key)=>{
|
|
24
|
+
try {
|
|
25
|
+
return Reflect.has(value, key);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export const safeKeys = (value)=>{
|
|
31
|
+
try {
|
|
32
|
+
return Object.keys(value);
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const toJson = (value)=>{
|
|
38
|
+
const ancestors = [];
|
|
39
|
+
return JSON.stringify(value, function(_key, nested) {
|
|
40
|
+
if (typeof nested === 'bigint') {
|
|
41
|
+
return `${nested}n`;
|
|
42
|
+
}
|
|
43
|
+
if (!isObject(nested)) {
|
|
44
|
+
return nested;
|
|
45
|
+
}
|
|
46
|
+
while(ancestors.length > 0 && ancestors.at(-1) !== this){
|
|
47
|
+
ancestors.pop();
|
|
48
|
+
}
|
|
49
|
+
if (ancestors.includes(nested)) {
|
|
50
|
+
return '[Circular]';
|
|
51
|
+
}
|
|
52
|
+
ancestors.push(nested);
|
|
53
|
+
return nested;
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const stringifyObject = (value)=>{
|
|
57
|
+
try {
|
|
58
|
+
const json = toJson(value);
|
|
59
|
+
if (json !== undefined) {
|
|
60
|
+
return json;
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
try {
|
|
64
|
+
return String(value);
|
|
65
|
+
} catch {}
|
|
66
|
+
try {
|
|
67
|
+
return Object.prototype.toString.call(value);
|
|
68
|
+
} catch {}
|
|
69
|
+
return '[unprintable value]';
|
|
70
|
+
};
|
|
71
|
+
export const safeStringify = (value)=>{
|
|
72
|
+
if (typeof value === 'string') {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'bigint') {
|
|
76
|
+
return `${value}n`;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'function') {
|
|
79
|
+
const name = safeGet(value, 'name');
|
|
80
|
+
return typeof name === 'string' && name ? `[Function ${name}]` : '[Function (anonymous)]';
|
|
81
|
+
}
|
|
82
|
+
if (isObject(value)) {
|
|
83
|
+
return stringifyObject(value);
|
|
84
|
+
}
|
|
85
|
+
return String(value);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=safe.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a thrown value that was neither an Error nor shaped like one.
|
|
3
|
+
*
|
|
4
|
+
* The message describes the value, and the value itself is kept as `thrown`.
|
|
5
|
+
*/
|
|
6
|
+
export declare class NonError extends Error {
|
|
7
|
+
readonly thrown: unknown;
|
|
8
|
+
/**
|
|
9
|
+
* @param thrown - the value that was thrown
|
|
10
|
+
* @param options - standard Error options, such as `cause`
|
|
11
|
+
*/
|
|
12
|
+
constructor(thrown: unknown, options?: ErrorOptions);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Converts any caught value to an Error. Never throws.
|
|
16
|
+
*
|
|
17
|
+
* - Errors, including those from another realm (`vm`, iframes), are returned as-is.
|
|
18
|
+
* - Objects with a string `message` are rebuilt as an Error, keeping their name, stack, cause and
|
|
19
|
+
* other properties such as `code`.
|
|
20
|
+
* - Anything else is wrapped in a {@link NonError}.
|
|
21
|
+
*
|
|
22
|
+
* @param value - a caught value
|
|
23
|
+
* @returns `value` when it is already an Error, otherwise a new Error describing it
|
|
24
|
+
*/
|
|
25
|
+
export declare const toError: (value: unknown) => Error;
|
|
26
|
+
/**
|
|
27
|
+
* Reads the message of any caught value. Never throws.
|
|
28
|
+
*
|
|
29
|
+
* @param value - a caught value
|
|
30
|
+
* @returns the message of `toError(value)`
|
|
31
|
+
*/
|
|
32
|
+
export declare const getErrorMessage: (value: unknown) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether `value` is an Error with the given `code`, such as Node's `ENOENT`.
|
|
35
|
+
*
|
|
36
|
+
* @param value - a caught value
|
|
37
|
+
* @param code - code to match
|
|
38
|
+
* @returns `true` when `value` is an Error whose `code` is `code`, narrowing its type
|
|
39
|
+
*/
|
|
40
|
+
export declare const hasCode: <const Code extends number | string>(value: unknown, code: Code) => value is Error & {
|
|
41
|
+
readonly code: Code;
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=to-error.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { hasProperty, isError, isObject, safeGet, safeKeys, safeStringify } from '#safe';
|
|
2
|
+
const MAX_MESSAGE_LENGTH = 1000;
|
|
3
|
+
export class NonError extends Error {
|
|
4
|
+
thrown;
|
|
5
|
+
constructor(thrown, options){
|
|
6
|
+
const message = safeStringify(thrown);
|
|
7
|
+
super(message.length > MAX_MESSAGE_LENGTH ? `${message.slice(0, MAX_MESSAGE_LENGTH)}…` : message, options);
|
|
8
|
+
this.name = 'NonError';
|
|
9
|
+
this.thrown = thrown;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const fromErrorLike = (value, message)=>{
|
|
13
|
+
const error = hasProperty(value, 'cause') ? new Error(message, {
|
|
14
|
+
cause: safeGet(value, 'cause')
|
|
15
|
+
}) : new Error(message);
|
|
16
|
+
const name = safeGet(value, 'name');
|
|
17
|
+
if (typeof name === 'string') {
|
|
18
|
+
error.name = name;
|
|
19
|
+
}
|
|
20
|
+
const stack = safeGet(value, 'stack');
|
|
21
|
+
if (typeof stack === 'string') {
|
|
22
|
+
error.stack = stack;
|
|
23
|
+
}
|
|
24
|
+
for (const key of safeKeys(value)){
|
|
25
|
+
if (!Reflect.has(error, key)) {
|
|
26
|
+
Object.defineProperty(error, key, {
|
|
27
|
+
configurable: true,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
value: safeGet(value, key),
|
|
30
|
+
writable: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return error;
|
|
35
|
+
};
|
|
36
|
+
export const toError = (value)=>{
|
|
37
|
+
if (isError(value)) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (isObject(value)) {
|
|
41
|
+
const message = safeGet(value, 'message');
|
|
42
|
+
if (typeof message === 'string') {
|
|
43
|
+
return fromErrorLike(value, message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return new NonError(value);
|
|
47
|
+
};
|
|
48
|
+
export const getErrorMessage = (value)=>{
|
|
49
|
+
const error = toError(value);
|
|
50
|
+
try {
|
|
51
|
+
const { message } = error;
|
|
52
|
+
return typeof message === 'string' ? message : safeStringify(message);
|
|
53
|
+
} catch {
|
|
54
|
+
return '[unreadable message]';
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const hasCode = (value, code)=>isError(value) && safeGet(value, 'code') === code;
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=to-error.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an error relates to the one above it in the tree.
|
|
3
|
+
*/
|
|
4
|
+
export type ErrorRelation = 'aggregate' | 'cause' | 'root';
|
|
5
|
+
/**
|
|
6
|
+
* One error in the tree that `walkErrors` visits.
|
|
7
|
+
*/
|
|
8
|
+
export interface ErrorNode {
|
|
9
|
+
/**
|
|
10
|
+
* The value, converted with `toError`.
|
|
11
|
+
*/
|
|
12
|
+
readonly error: Error;
|
|
13
|
+
/**
|
|
14
|
+
* Distance from the root, which is `0`.
|
|
15
|
+
*/
|
|
16
|
+
readonly depth: number;
|
|
17
|
+
/**
|
|
18
|
+
* `root` for the value passed in, `cause` for an error's `cause`, `aggregate` for an entry in an
|
|
19
|
+
* error's `errors` (as on `AggregateError`).
|
|
20
|
+
*/
|
|
21
|
+
readonly relation: ErrorRelation;
|
|
22
|
+
/**
|
|
23
|
+
* `true` when `maxDepth` stopped the walk here, so this error's own cause and errors were skipped.
|
|
24
|
+
*/
|
|
25
|
+
readonly truncated: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for `walkErrors` and `formatError`.
|
|
29
|
+
*/
|
|
30
|
+
export interface WalkOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Deepest level to visit. The root is level `0`.
|
|
33
|
+
*
|
|
34
|
+
* @default 32
|
|
35
|
+
*/
|
|
36
|
+
readonly maxDepth?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Visits an error, its `cause` chain and the `errors` of any `AggregateError`, depth first. Never
|
|
40
|
+
* throws.
|
|
41
|
+
*
|
|
42
|
+
* Every value is converted with `toError`, so a cause that isn't an Error still appears. A value
|
|
43
|
+
* already visited is skipped, so a circular chain ends.
|
|
44
|
+
*
|
|
45
|
+
* @param value - a caught value
|
|
46
|
+
* @param options - {@link WalkOptions}
|
|
47
|
+
* @yields {ErrorNode} each error in the tree, starting with `value` itself
|
|
48
|
+
*/
|
|
49
|
+
export declare const walkErrors: (value: unknown, options?: WalkOptions) => Generator<ErrorNode, void, undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* Formats an error with every cause and `AggregateError` entry, indented by depth. Never throws.
|
|
52
|
+
*
|
|
53
|
+
* Each error is printed as its stack, with causes labelled `Caused by:` and `AggregateError` entries
|
|
54
|
+
* labelled `-`.
|
|
55
|
+
*
|
|
56
|
+
* @param value - a caught value
|
|
57
|
+
* @param options - {@link WalkOptions}
|
|
58
|
+
* @returns a multi-line report
|
|
59
|
+
*/
|
|
60
|
+
export declare const formatError: (value: unknown, options?: WalkOptions) => string;
|
|
61
|
+
//# sourceMappingURL=walk.d.ts.map
|
package/dist/lib/walk.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { hasProperty, isObject, safeGet, safeStringify } from '#safe';
|
|
2
|
+
import { getErrorMessage, toError } from '#to-error';
|
|
3
|
+
const DEFAULT_MAX_DEPTH = 32;
|
|
4
|
+
const INDENT_WIDTH = 4;
|
|
5
|
+
const childrenOf = (error, depth)=>{
|
|
6
|
+
const children = [];
|
|
7
|
+
if (hasProperty(error, 'cause')) {
|
|
8
|
+
children.push({
|
|
9
|
+
depth,
|
|
10
|
+
relation: 'cause',
|
|
11
|
+
value: safeGet(error, 'cause')
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const errors = safeGet(error, 'errors');
|
|
15
|
+
try {
|
|
16
|
+
if (Array.isArray(errors)) {
|
|
17
|
+
for (const value of errors){
|
|
18
|
+
children.push({
|
|
19
|
+
depth,
|
|
20
|
+
value,
|
|
21
|
+
relation: 'aggregate'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
return children;
|
|
27
|
+
};
|
|
28
|
+
export const walkErrors = function*(value, options = {}) {
|
|
29
|
+
const { maxDepth = DEFAULT_MAX_DEPTH } = options;
|
|
30
|
+
const seen = new WeakSet();
|
|
31
|
+
const pending = [
|
|
32
|
+
{
|
|
33
|
+
depth: 0,
|
|
34
|
+
relation: 'root',
|
|
35
|
+
value
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
for(let next = pending.pop(); next; next = pending.pop()){
|
|
39
|
+
if (isObject(next.value)) {
|
|
40
|
+
if (seen.has(next.value)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
seen.add(next.value);
|
|
44
|
+
}
|
|
45
|
+
const error = toError(next.value);
|
|
46
|
+
const children = childrenOf(error, next.depth + 1);
|
|
47
|
+
const isTruncated = next.depth >= maxDepth && children.length > 0;
|
|
48
|
+
yield {
|
|
49
|
+
error,
|
|
50
|
+
depth: next.depth,
|
|
51
|
+
relation: next.relation,
|
|
52
|
+
truncated: isTruncated
|
|
53
|
+
};
|
|
54
|
+
if (!isTruncated) {
|
|
55
|
+
pending.push(...children.toReversed());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const labels = {
|
|
60
|
+
aggregate: '- ',
|
|
61
|
+
cause: 'Caused by: ',
|
|
62
|
+
root: ''
|
|
63
|
+
};
|
|
64
|
+
const describe = (error)=>{
|
|
65
|
+
const name = safeGet(error, 'name');
|
|
66
|
+
const message = getErrorMessage(error);
|
|
67
|
+
const nameText = typeof name === 'string' ? name : safeStringify(name);
|
|
68
|
+
const header = message ? `${nameText}: ${message}` : nameText;
|
|
69
|
+
const stack = safeGet(error, 'stack');
|
|
70
|
+
if (typeof stack !== 'string' || stack === '') {
|
|
71
|
+
return header;
|
|
72
|
+
}
|
|
73
|
+
return stack.startsWith(header) ? stack : `${header}\n${stack}`;
|
|
74
|
+
};
|
|
75
|
+
export const formatError = (value, options = {})=>{
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (const { error, depth, relation, truncated } of walkErrors(value, options)){
|
|
78
|
+
const indent = ' '.repeat(INDENT_WIDTH * depth);
|
|
79
|
+
const [first = '', ...rest] = describe(error).split('\n');
|
|
80
|
+
lines.push(`${indent}${labels[relation]}${first}`, ...rest.map((line)=>indent + line));
|
|
81
|
+
if (truncated) {
|
|
82
|
+
lines.push(`${indent} … more causes omitted (maxDepth: ${depth})`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=walk.js.map
|
package/dist/to-error.js
ADDED
package/dist/walk.d.ts
ADDED
package/dist/walk.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unknown-error",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Turn any caught `unknown` into a real Error without throwing, even for hostile values, other realms and deep or circular cause chains",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=24"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"author": "JacobLey",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/JacobLey/leyman.git",
|
|
14
|
+
"directory": "tools/unknown-error"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/JacobLey/leyman/tree/main/tools/unknown-error#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/JacobLey/leyman/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"error",
|
|
22
|
+
"unknown",
|
|
23
|
+
"catch",
|
|
24
|
+
"normalize",
|
|
25
|
+
"to-error",
|
|
26
|
+
"error-message",
|
|
27
|
+
"cause",
|
|
28
|
+
"aggregate-error",
|
|
29
|
+
"typescript"
|
|
30
|
+
],
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"types": "./dist/index.js",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"!dist/**/*.map",
|
|
36
|
+
"!dist/tsconfig.tsbuildinfo",
|
|
37
|
+
"WHY-UNKNOWN-ERROR.md"
|
|
38
|
+
],
|
|
39
|
+
"exports": {
|
|
40
|
+
".": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"imports": {
|
|
43
|
+
"#safe": "./dist/lib/safe.js",
|
|
44
|
+
"#to-error": "./dist/lib/to-error.js",
|
|
45
|
+
"#walk": "./dist/lib/walk.js"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "^2.5.14",
|
|
49
|
+
"@leyman/eslint-config": "^0.0.1",
|
|
50
|
+
"@leyman/expect": "^0.0.1",
|
|
51
|
+
"@swc/cli": "^0.8.1",
|
|
52
|
+
"@swc/core": "^1.16.2",
|
|
53
|
+
"@types/mocha": "^10.0.10",
|
|
54
|
+
"@types/node": "^24.19.0",
|
|
55
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
56
|
+
"c8": "^12.0.0",
|
|
57
|
+
"eslint": "^10.12.0",
|
|
58
|
+
"expect-type": "^1.4.0",
|
|
59
|
+
"mocha": "^12.0.2",
|
|
60
|
+
"mocha-chain": "^1.0.1",
|
|
61
|
+
"npm-barrelify": "npm:barrelify@^1.3.3",
|
|
62
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
63
|
+
}
|
|
6
64
|
}
|