222 lines
4.5 KiB
JavaScript
222 lines
4.5 KiB
JavaScript
|
|
import {errorConstructors} from './error-constructors.js';
|
||
|
|
|
||
|
|
export class NonError extends Error {
|
||
|
|
name = 'NonError';
|
||
|
|
|
||
|
|
constructor(message) {
|
||
|
|
super(NonError._prepareSuperMessage(message));
|
||
|
|
}
|
||
|
|
|
||
|
|
static _prepareSuperMessage(message) {
|
||
|
|
try {
|
||
|
|
return JSON.stringify(message);
|
||
|
|
} catch {
|
||
|
|
return String(message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const errorProperties = [
|
||
|
|
{
|
||
|
|
property: 'name',
|
||
|
|
enumerable: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
property: 'message',
|
||
|
|
enumerable: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
property: 'stack',
|
||
|
|
enumerable: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
property: 'code',
|
||
|
|
enumerable: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
property: 'cause',
|
||
|
|
enumerable: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
property: 'errors',
|
||
|
|
enumerable: false,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const toJsonWasCalled = new WeakSet();
|
||
|
|
|
||
|
|
const toJSON = from => {
|
||
|
|
toJsonWasCalled.add(from);
|
||
|
|
const json = from.toJSON();
|
||
|
|
toJsonWasCalled.delete(from);
|
||
|
|
return json;
|
||
|
|
};
|
||
|
|
|
||
|
|
const newError = name => {
|
||
|
|
const ErrorConstructor = errorConstructors.get(name) ?? Error;
|
||
|
|
return ErrorConstructor === AggregateError
|
||
|
|
? new ErrorConstructor([])
|
||
|
|
: new ErrorConstructor();
|
||
|
|
};
|
||
|
|
|
||
|
|
// eslint-disable-next-line complexity
|
||
|
|
const destroyCircular = ({
|
||
|
|
from,
|
||
|
|
seen,
|
||
|
|
to,
|
||
|
|
forceEnumerable,
|
||
|
|
maxDepth,
|
||
|
|
depth,
|
||
|
|
useToJSON,
|
||
|
|
serialize,
|
||
|
|
}) => {
|
||
|
|
if (!to) {
|
||
|
|
if (Array.isArray(from)) {
|
||
|
|
to = [];
|
||
|
|
} else if (!serialize && isErrorLike(from)) {
|
||
|
|
to = newError(from.name);
|
||
|
|
} else {
|
||
|
|
to = {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
seen.push(from);
|
||
|
|
|
||
|
|
if (depth >= maxDepth) {
|
||
|
|
return to;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (useToJSON && typeof from.toJSON === 'function' && !toJsonWasCalled.has(from)) {
|
||
|
|
return toJSON(from);
|
||
|
|
}
|
||
|
|
|
||
|
|
const continueDestroyCircular = value => destroyCircular({
|
||
|
|
from: value,
|
||
|
|
seen: [...seen],
|
||
|
|
forceEnumerable,
|
||
|
|
maxDepth,
|
||
|
|
depth,
|
||
|
|
useToJSON,
|
||
|
|
serialize,
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const [key, value] of Object.entries(from)) {
|
||
|
|
if (value && value instanceof Uint8Array && value.constructor.name === 'Buffer') {
|
||
|
|
to[key] = '[object Buffer]';
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO: Use `stream.isReadable()` when targeting Node.js 18.
|
||
|
|
if (value !== null && typeof value === 'object' && typeof value.pipe === 'function') {
|
||
|
|
to[key] = '[object Stream]';
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof value === 'function') {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!value || typeof value !== 'object') {
|
||
|
|
// Gracefully handle non-configurable errors like `DOMException`.
|
||
|
|
try {
|
||
|
|
to[key] = value;
|
||
|
|
} catch {}
|
||
|
|
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!seen.includes(from[key])) {
|
||
|
|
depth++;
|
||
|
|
to[key] = continueDestroyCircular(from[key]);
|
||
|
|
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
to[key] = '[Circular]';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (serialize || to instanceof Error) {
|
||
|
|
for (const {property, enumerable} of errorProperties) {
|
||
|
|
if (from[property] !== undefined && from[property] !== null) {
|
||
|
|
Object.defineProperty(to, property, {
|
||
|
|
value: isErrorLike(from[property]) || Array.isArray(from[property])
|
||
|
|
? continueDestroyCircular(from[property])
|
||
|
|
: from[property],
|
||
|
|
enumerable: forceEnumerable ? true : enumerable,
|
||
|
|
configurable: true,
|
||
|
|
writable: true,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return to;
|
||
|
|
};
|
||
|
|
|
||
|
|
export function serializeError(value, options = {}) {
|
||
|
|
const {
|
||
|
|
maxDepth = Number.POSITIVE_INFINITY,
|
||
|
|
useToJSON = true,
|
||
|
|
} = options;
|
||
|
|
|
||
|
|
if (typeof value === 'object' && value !== null) {
|
||
|
|
return destroyCircular({
|
||
|
|
from: value,
|
||
|
|
seen: [],
|
||
|
|
forceEnumerable: true,
|
||
|
|
maxDepth,
|
||
|
|
depth: 0,
|
||
|
|
useToJSON,
|
||
|
|
serialize: true,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// People sometimes throw things besides Error objects…
|
||
|
|
if (typeof value === 'function') {
|
||
|
|
// `JSON.stringify()` discards functions. We do too, unless a function is thrown directly.
|
||
|
|
// We intentionally use `||` because `.name` is an empty string for anonymous functions.
|
||
|
|
return `[Function: ${value.name || 'anonymous'}]`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function deserializeError(value, options = {}) {
|
||
|
|
const {maxDepth = Number.POSITIVE_INFINITY} = options;
|
||
|
|
|
||
|
|
if (value instanceof Error) {
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isMinimumViableSerializedError(value)) {
|
||
|
|
return destroyCircular({
|
||
|
|
from: value,
|
||
|
|
seen: [],
|
||
|
|
to: newError(value.name),
|
||
|
|
maxDepth,
|
||
|
|
depth: 0,
|
||
|
|
serialize: false,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return new NonError(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isErrorLike(value) {
|
||
|
|
return Boolean(value)
|
||
|
|
&& typeof value === 'object'
|
||
|
|
&& typeof value.name === 'string'
|
||
|
|
&& typeof value.message === 'string'
|
||
|
|
&& typeof value.stack === 'string';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Used as a weak check for immediately-passed objects, whereas `isErrorLike` is used for nested values to avoid bad detection
|
||
|
|
function isMinimumViableSerializedError(value) {
|
||
|
|
return Boolean(value)
|
||
|
|
&& typeof value === 'object'
|
||
|
|
&& typeof value.message === 'string'
|
||
|
|
&& !Array.isArray(value);
|
||
|
|
}
|
||
|
|
|
||
|
|
export {addKnownErrorConstructor} from './error-constructors.js';
|