@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
326 lines (288 loc) • 9.44 kB
text/typescript
/**
* Functions for converting errors to/from other types of objects.
* @module
*/
import { Constructor } from "./types.ts";
import { isPlainObject, omit } from "./object.ts";
import Exception, { type ExceptionOptions } from "./error/Exception.ts";
import { createErrorEvent } from "./event.ts";
export { Exception };
export type { ExceptionOptions };
/**
* Transforms the error to a plain object.
*
* @example
* ```ts
* import { toObject } from "@ayonli/jsext/error";
*
* const err = new Error("Something went wrong.");
*
* const obj = toObject(err);
* console.log(obj);
* // {
* // "@@type": "Error",
* // name: "Error",
* // message: "Something went wrong.",
* // stack: "Error: Something went wrong.\n at <anonymous>:1:13"
* // }
* ```
*/
export function toObject<T extends Error>(err: T): { [x: string | symbol]: any; } {
if (!(err instanceof Error) && err["name"] && err["message"]) { // Error-like
err = fromObject(err, Error) as any;
}
const obj = {
"@@type": err.constructor.name,
...omit(err, ["toString", "toJSON", "__callSiteEvals"]),
} as { [x: string | symbol]: any; };
if (obj["@@type"] === "AggregateError" && Array.isArray(obj["errors"])) {
obj["errors"] = (obj["errors"] as unknown[]).map(item => {
return item instanceof Error ? toObject(item) : item;
});
}
return obj;
}
/**
* Reverses a plain object to a specific error type.
*
* @example
* ```ts
* import { fromObject } from "@ayonli/jsext/error";
*
* const obj = {
* "@@type": "Error",
* name: "Error",
* message: "Something went wrong.",
* stack: "Error: Something went wrong.\n at <anonymous>:1:13"
* };
*
* const err = fromObject(obj);
* console.log(err);
* // Error: Something went wrong.
* // at <anonymous>:1:13
* ```
*/
export function fromObject<T extends { name: "Error"; }>(obj: T): Error;
export function fromObject<T extends { name: "EvalError"; }>(obj: T): EvalError;
export function fromObject<T extends { name: "RangeError"; }>(obj: T): RangeError;
export function fromObject<T extends { name: "ReferenceError"; }>(obj: T): ReferenceError;
export function fromObject<T extends { name: "SyntaxError"; }>(obj: T): SyntaxError;
export function fromObject<T extends { name: "TypeError"; }>(obj: T): TypeError;
export function fromObject<T extends { name: "URIError"; }>(obj: T): URIError;
export function fromObject<T extends { name: "Exception"; }>(obj: T): Exception;
export function fromObject<T extends Error>(obj: { [x: string | symbol]: any; }, ctor?: Constructor<Error>): T | null;
export function fromObject<T extends Error>(
obj: { [x: string | symbol]: any; },
ctor: Function | undefined = undefined
): T | null {
// @ts-ignore
if (!obj?.name) {
return null;
}
// @ts-ignore
ctor ||= (globalThis[obj["@@type"] || obj.name] || globalThis[obj.name]) as new (...args: any) => T;
if (!ctor) {
if (obj["@@type"] === "Exception") {
ctor = Exception as unknown as new (...args: any) => T;
} else {
ctor = Error as unknown as new (...args: any) => T;
}
}
let err: T;
if (ctor.name === "DOMException" && typeof DOMException === "function") {
err = new (ctor as typeof DOMException)(obj["message"] ?? "", obj["name"]) as any;
} else {
err = Object.create(ctor.prototype, {
message: {
configurable: true,
enumerable: false,
writable: true,
value: obj["message"] ?? "",
},
});
}
if (err.name !== obj["name"]) {
Object.defineProperty(err, "name", {
configurable: true,
enumerable: false,
writable: true,
value: obj["name"],
});
}
if (obj["stack"] !== undefined) {
Object.defineProperty(err, "stack", {
configurable: true,
enumerable: false,
writable: true,
value: obj["stack"],
});
}
if (obj["cause"] != undefined) {
Object.defineProperty(err, "cause", {
configurable: true,
enumerable: false,
writable: true,
value: obj["cause"],
});
}
const otherKeys = Reflect.ownKeys(obj).filter(key => !([
"@@type",
"name",
"message",
"stack",
"cause"
] as (string | symbol)[]).includes(key));
otherKeys.forEach(key => {
// @ts-ignore
err[key] ??= obj[key];
});
// @ts-ignore
if (isAggregateError(err) && Array.isArray(err["errors"])) {
(err as any)["errors"] = ((err as any)["errors"] as unknown[]).map(item => {
return isPlainObject(item) ? fromObject(item) : item;
});
}
return err;
}
/**
* Creates an `ErrorEvent` instance based on the given error.
*
* @example
* ```ts
* import { toErrorEvent } from "@ayonli/jsext/error";
*
* const err = new Error("Something went wrong.");
*
* const event = toErrorEvent(err);
* console.log(event);
* // ErrorEvent {
* // error: Error: Something went wrong.
* // at <anonymous>:1:13,
* // message: "Something went wrong.",
* // filename: "",
* // lineno: 1,
* // colno: 13
* // }
* ```
*/
export function toErrorEvent(err: Error, type: string = "error"): ErrorEvent {
let filename = "";
let lineno = 0;
let colno = 0;
if (err.stack) {
const lines = err.stack.split("\n").map(line => line.trim());
let callSite = lines.find(line => line.startsWith("at "));
if (callSite) {
callSite = callSite.slice(3);
} else if (callSite = lines.find(line => line.includes("@") && line.length > 1)) {
callSite = callSite.slice(callSite.indexOf("@") + 1);
}
if (callSite) {
let start = callSite.lastIndexOf("(");
let end = 0;
if (start !== -1) {
start += 1;
end = callSite.indexOf(")", start);
callSite = callSite.slice(start, end);
}
const matches = callSite.match(/:(\d+):(\d+)$/);
if (matches) {
filename = callSite.slice(0, matches.index);
lineno = parseInt(matches[1] as string);
colno = parseInt(matches[2] as string);
}
}
}
return createErrorEvent(type, {
error: err,
message: err.message,
filename,
lineno,
colno,
});
}
/**
* Creates an error instance based on the given `ErrorEvent` instance.
*
* @example
* ```ts
* import { fromErrorEvent } from "@ayonli/jsext/error";
*
* const event = new ErrorEvent("error", {
* message: "Something went wrong.",
* filename: "",
* lineno: 1,
* colno: 13,
* });
*
* const err = fromErrorEvent(event);
* console.log(err);
* // Error: Something went wrong.
* // at <anonymous>:1:13
* ```
*/
export function fromErrorEvent<T extends Error>(event: ErrorEvent): T | null {
if (event.error instanceof Error) {
return event.error as T;
}
let err: T;
let shouldPatchStack = false;
if (event.error
&& typeof event.error === "object"
&& event.error["name"]
&& event.error["message"]
) { // Error-like
err = fromObject(event.error, Error) as T;
shouldPatchStack = !err.stack;
} else if (event.message) {
err = new Error(event.message) as T;
shouldPatchStack = true;
} else {
return null;
}
if (shouldPatchStack) {
let stack = "";
if (typeof navigator === "object" && navigator.userAgent.includes("Firefox")) {
if (event.filename) {
stack = "@" + event.filename;
} else {
stack = "@debugger eval code";
}
} else if (typeof navigator === "object"
&& navigator.userAgent.includes("Safari")
&& !navigator.userAgent.includes("Chrome") // Chrome likes to pretend it's Safari
) {
if (event.filename) {
stack = "@" + event.filename;
} else {
stack = "global code@";
}
} else {
stack = `${err.name}: ${event.message}\n at ${event.filename || "<anonymous>"}`;
}
if (event.lineno && stack !== "global code@") {
stack += ":" + event.lineno;
}
if (event.colno && stack !== "global code@") {
stack += ":" + event.colno;
}
Object.defineProperty(err, "stack", {
configurable: true,
enumerable: false,
writable: true,
value: stack,
});
}
return err;
}
/** @inner */
export function isDOMException(value: unknown): value is DOMException {
return ((typeof DOMException === "function") && (value instanceof DOMException))
|| (value instanceof Error && value.constructor.name === "DOMException"); // Node.js v16-
}
/** @inner */
export function isAggregateError(value: unknown): boolean {
// @ts-ignore
return (typeof AggregateError === "function" && value instanceof AggregateError)
|| (value instanceof Error && value.constructor.name === "AggregateError");
}