@jchip/error
Version:
utilities and polyfill for node.js errors
141 lines (128 loc) • 3.97 kB
text/typescript
import Path from "path";
/* eslint-disable @typescript-eslint/ban-ts-comment */
const defaultPathFilter = [
new RegExp(`/node_modules.*/(pirates/|isomorphic-loader/lib/extend-require)`),
] as (string | RegExp)[];
/** options for cleanErrorStack */
type CleanErrorStackOptions = {
/** string to replace part of the stack trace paths */
replacePath?: false | string;
/** list of string or RegExp to match stack trace paths to ignore */
ignorePathFilter?: (string | RegExp)[];
};
/**
* Return the stack text of an error with internal modules removed
*
* @param error - error
* @param options - clean error stack options
* @returns cleaned up stack trace
*/
export function cleanErrorStack(
error: Error,
{ replacePath = `${process.cwd()}/`, ignorePathFilter = [] }: CleanErrorStackOptions = {}
): string {
const stack = error && (error.stack || error.message);
if (!stack) {
return String(stack);
}
const result = stack
.split("\n")
.map((line) => {
// keep all non stack tracing lines
if (!line.match(/ {4,}at/)) {
return line;
}
const match = line.match(/( {4,}at)([^\(]+\()([^\)]+\))(.*)/);
// skip any stack tracing line not in these formats:
// - " at Blah (/foo/bar:##:##)" format
// - "scheme://path" (ie: webpack://path)
if (!match || (!match[3].match(/[^:]+:\/\//) && !Path.isAbsolute(match[3]))) {
return false;
}
const path = match[3].replace(/\\/g, "/");
if (
defaultPathFilter
.concat(ignorePathFilter)
.find((s) => s && (s instanceof RegExp ? path.match(s) : path.includes(s)))
) {
return false;
}
const path2 = replacePath && replacePath.length > 1 ? path.replace(replacePath, "") : path;
return `${match[1]}${match[2]}${path2}${match[4]}`;
})
.filter((x) => x)
.join("\n");
return result;
}
/**
* Build stack of aggregate errors
*
* @param stack - top error
* @param errors - aggregated errors
* @returns aggregate stack
*/
export function aggregateStack(stack: string, errors: any[]): string {
return [stack]
.concat(
errors &&
errors.map &&
errors.map((e) => {
const s = e && (e.stack || e.message);
return (s || String(e)).replace(/^/gm, " ");
})
)
.join("\n");
}
/**
* build the aggregate stack of an AggregateError
*
* @param error aggregate error
* @returns aggregate stack
*/
/* eslint-disable-next-line no-use-before-define */
export function aggregateErrorStack(error: AggregateError): string {
return aggregateStack(error.__stack || error.message || String(error), error.errors);
}
/**
* AggregateError
* - https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects
*/
export class AggregateError extends Error {
/** "AggregateError" */
readonly name: string;
/** errors collected */
errors: any[];
/** aggregate stack */
stack: string;
/** original error stack before generating an aggregate one */
__stack: string;
constructor(errors?: any[], msg?: string) {
if (!errors || !(errors[Symbol.iterator] instanceof Function)) {
throw new TypeError(`input errors must be iterable but it's ${typeof errors}`);
}
super(msg);
// Using defineProperty to replicate behavior of Object.keys(new Error()) returns []
Object.defineProperty(this, "name", { value: "AggregateError" });
let aggStack: string;
Object.defineProperties(this, {
// specify errors according to spec
errors: {
configurable: true,
enumerable: false,
writable: true,
value: [].concat(errors),
},
// save original stack
__stack: {
enumerable: false,
value: this.stack,
},
// make aggregate stack
stack: {
get() {
return aggStack || (aggStack = aggregateErrorStack(this));
},
},
});
}
}