Skip to content

Commit 3ba7475

Browse files
committed
inline serialize-error package
1 parent 4dc5ae3 commit 3ba7475

File tree

1 file changed

+300
-0
lines changed

1 file changed

+300
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/**
2+
* `serialize-error` package wrapped as a single file for compatibility
3+
* with both CJS and ESM.
4+
*
5+
* @see https://github.com/sindresorhus/serialize-error
6+
*/
7+
const list = [
8+
// Native ES errors https://262.ecma-international.org/12.0/#sec-well-known-intrinsic-objects
9+
Error,
10+
EvalError,
11+
RangeError,
12+
ReferenceError,
13+
SyntaxError,
14+
TypeError,
15+
URIError,
16+
AggregateError,
17+
18+
// Built-in errors
19+
globalThis.DOMException,
20+
21+
// Node-specific errors
22+
// https://nodejs.org/api/errors.html
23+
(globalThis as any).AssertionError,
24+
(globalThis as any).SystemError,
25+
]
26+
// Non-native Errors are used with `globalThis` because they might be missing. This filter drops them when undefined.
27+
.filter(Boolean)
28+
.map((constructor) => [constructor.name, constructor]);
29+
30+
export type ErrorObject = {
31+
name?: string;
32+
message?: string;
33+
stack?: string;
34+
cause?: unknown;
35+
code?: string;
36+
} & Record<string, unknown>;
37+
38+
export const errorConstructors = new Map(list as any);
39+
40+
export function addKnownErrorConstructor(constructor: any) {
41+
const { name } = constructor;
42+
if (errorConstructors.has(name)) {
43+
throw new Error(`The error constructor "${name}" is already known.`);
44+
}
45+
46+
try {
47+
// eslint-disable-next-line no-new -- It just needs to be verified
48+
new constructor();
49+
} catch (error) {
50+
throw new Error(`The error constructor "${name}" is not compatible`, {
51+
cause: error,
52+
});
53+
}
54+
55+
errorConstructors.set(name, constructor);
56+
}
57+
58+
export class NonError extends Error {
59+
override name = 'NonError';
60+
61+
constructor(message: any) {
62+
super(NonError._prepareSuperMessage(message));
63+
}
64+
65+
static _prepareSuperMessage(message: any) {
66+
try {
67+
return JSON.stringify(message);
68+
} catch {
69+
return String(message);
70+
}
71+
}
72+
}
73+
74+
const errorProperties = [
75+
{
76+
property: 'name',
77+
enumerable: false,
78+
},
79+
{
80+
property: 'message',
81+
enumerable: false,
82+
},
83+
{
84+
property: 'stack',
85+
enumerable: false,
86+
},
87+
{
88+
property: 'code',
89+
enumerable: true,
90+
},
91+
{
92+
property: 'cause',
93+
enumerable: false,
94+
},
95+
{
96+
property: 'errors',
97+
enumerable: false,
98+
},
99+
];
100+
101+
const toJsonWasCalled = new WeakSet();
102+
103+
const toJSON = (from: any) => {
104+
toJsonWasCalled.add(from);
105+
const json = from.toJSON();
106+
toJsonWasCalled.delete(from);
107+
return json;
108+
};
109+
110+
const newError = (name: any) => {
111+
const ErrorConstructor = errorConstructors.get(name) ?? (Error as any);
112+
return ErrorConstructor === AggregateError
113+
? new ErrorConstructor([])
114+
: new ErrorConstructor();
115+
};
116+
117+
// eslint-disable-next-line complexity
118+
const destroyCircular = ({
119+
from,
120+
seen,
121+
to,
122+
forceEnumerable,
123+
maxDepth,
124+
depth,
125+
useToJSON,
126+
serialize,
127+
}: {
128+
from?: any;
129+
seen: any[];
130+
to?: any;
131+
forceEnumerable: boolean;
132+
maxDepth: number;
133+
depth: number;
134+
useToJSON: boolean;
135+
serialize: boolean;
136+
}) => {
137+
if (!to) {
138+
if (Array.isArray(from)) {
139+
to = [];
140+
} else if (!serialize && isErrorLike(from)) {
141+
to = newError(from.name);
142+
} else {
143+
to = {};
144+
}
145+
}
146+
147+
seen.push(from);
148+
149+
if (depth >= maxDepth) {
150+
return to;
151+
}
152+
153+
if (
154+
useToJSON &&
155+
typeof from.toJSON === 'function' &&
156+
!toJsonWasCalled.has(from)
157+
) {
158+
return toJSON(from);
159+
}
160+
161+
const continueDestroyCircular = (value: any) =>
162+
destroyCircular({
163+
from: value,
164+
seen: [...seen],
165+
forceEnumerable,
166+
maxDepth,
167+
depth,
168+
useToJSON,
169+
serialize,
170+
});
171+
172+
for (const [key, value] of Object.entries(from)) {
173+
if (
174+
value &&
175+
value instanceof Uint8Array &&
176+
value.constructor.name === 'Buffer'
177+
) {
178+
to[key] = '[object Buffer]';
179+
continue;
180+
}
181+
182+
// TODO: Use `stream.isReadable()` when targeting Node.js 18.
183+
if (
184+
value !== null &&
185+
typeof value === 'object' &&
186+
typeof (value as any).pipe === 'function'
187+
) {
188+
to[key] = '[object Stream]';
189+
continue;
190+
}
191+
192+
if (typeof value === 'function') {
193+
continue;
194+
}
195+
196+
if (!value || typeof value !== 'object') {
197+
// Gracefully handle non-configurable errors like `DOMException`.
198+
try {
199+
to[key] = value;
200+
} catch {}
201+
202+
continue;
203+
}
204+
205+
if (!seen.includes(from[key])) {
206+
depth++;
207+
to[key] = continueDestroyCircular(from[key]);
208+
209+
continue;
210+
}
211+
212+
to[key] = '[Circular]';
213+
}
214+
215+
if (serialize || to instanceof Error) {
216+
for (const { property, enumerable } of errorProperties) {
217+
if (from[property] !== undefined && from[property] !== null) {
218+
Object.defineProperty(to, property, {
219+
value:
220+
isErrorLike(from[property]) ||
221+
Array.isArray(from[property])
222+
? continueDestroyCircular(from[property])
223+
: from[property],
224+
enumerable: forceEnumerable ? true : enumerable,
225+
configurable: true,
226+
writable: true,
227+
});
228+
}
229+
}
230+
}
231+
232+
return to;
233+
};
234+
235+
export function serializeError(value: any, options: any = {}) {
236+
const { maxDepth = Number.POSITIVE_INFINITY, useToJSON = true } = options;
237+
238+
if (typeof value === 'object' && value !== null) {
239+
return destroyCircular({
240+
from: value,
241+
seen: [],
242+
forceEnumerable: true,
243+
maxDepth,
244+
depth: 0,
245+
useToJSON,
246+
serialize: true,
247+
});
248+
}
249+
250+
// People sometimes throw things besides Error objects…
251+
if (typeof value === 'function') {
252+
// `JSON.stringify()` discards functions. We do too, unless a function is thrown directly.
253+
// We intentionally use `||` because `.name` is an empty string for anonymous functions.
254+
return `[Function: ${value.name || 'anonymous'}]`;
255+
}
256+
257+
return value;
258+
}
259+
260+
export function deserializeError(value: any, options: any = {}) {
261+
const { maxDepth = Number.POSITIVE_INFINITY } = options;
262+
263+
if (value instanceof Error) {
264+
return value;
265+
}
266+
267+
if (isMinimumViableSerializedError(value)) {
268+
return destroyCircular({
269+
from: value,
270+
seen: [],
271+
to: newError(value.name),
272+
maxDepth,
273+
depth: 0,
274+
serialize: false,
275+
} as any);
276+
}
277+
278+
return new NonError(value);
279+
}
280+
281+
export function isErrorLike(value: any) {
282+
return (
283+
Boolean(value) &&
284+
typeof value === 'object' &&
285+
typeof value.name === 'string' &&
286+
typeof value.message === 'string' &&
287+
typeof value.stack === 'string'
288+
);
289+
}
290+
291+
// Used as a weak check for immediately-passed objects, whereas `isErrorLike` is used for nested values to avoid bad detection
292+
function isMinimumViableSerializedError(value: any) {
293+
// @ts-ignore
294+
return (
295+
Boolean(value) &&
296+
typeof value === 'object' &&
297+
typeof value.message === 'string' &&
298+
!Array.isArray(value)
299+
);
300+
}

0 commit comments

Comments
 (0)