UNPKG

editions

Version:

Publish multiple editions for your JavaScript packages consistently and easily (e.g. source edition, esnext edition, es2015 edition)

189 lines (172 loc) 6.76 kB
/** * Checks if a haystack string contains a needle string, used for ES5 compatibility. * @param haystack - The string to search within * @param needle - The string to search for * @returns True if the haystack does contain the needle, false if it does not contain the needle */ export function includes(haystack: string, needle: string): boolean { return haystack.indexOf(needle) !== -1 } /** * Checks if a haystack string does not contain a needle string, used for ES5 compatibility. * @param haystack - The string to search within * @param needle - The string to search for * @returns True if the haystack does not contain the needle, false if it does contain the needle */ export function excludes(haystack: string, needle: string): boolean { return haystack.indexOf(needle) === -1 } // ============================================================================ // ErrorLike /** The {@link Error}-like properties that provide details to {@link ErrorDetailed} */ export interface ErrorLike { /** The message that describes the error */ message: string /** The code to identify the category of the error for automated processing */ code?: unknown /** The severity level of the error */ level?: unknown /** The parent of the error */ parent?: unknown /** The parents of the error */ parents?: unknown /** The stack of the error, this is used internally. */ stack?: string } /** * Assert that the error is compatible with {@link ErrorLike}. * @param error - The error to assert * @throws {Error} If the error is not compatible with {@link ErrorLike} */ export function assertErrorLike(error: unknown): asserts error is ErrorLike { if ( !( error && typeof error === 'object' && 'message' in error && typeof error.message === 'string' ) ) { console.error({ error: error }) throw new Error('The error input is incompatible with ErrorLike') } } // ============================================================================ // ErrorInput /** The range of compatible error inputs for {@link ErrorDetailed} */ export type ErrorInput = ErrorLike | Error | string /** * Assert that the error is compatible with {@link ErrorInput}. * @param error - The error to assert for compatibility with {@link ErrorInput} * @throws {Error} If the error is not compatible with {@link ErrorInput} */ export function assertErrorInput(error: unknown): asserts error is ErrorInput { if ( typeof error === 'string' || error instanceof Error || (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') ) { console.error({ error: error }) throw new Error('The error input is incompatible with ErrorInput') } } // ============================================================================ // ErrorDetailed /** The resultant detailed error instance created by {@link detailedError} */ export interface ErrorDetailed extends ErrorLike, Error { code: string | number | null level: string | number | null parents: ErrorLike[] } /* eslint-disable @typescript-eslint/unified-signatures -- this eslint rule prevents the types from actually being correct on the callers, such as giving correct details for message when you mouse over it */ // disabling that rule, and doing what we do, it enables intellisense for message: // try {} catch (error: unknown) { throw detailedError({ message: '...', code: '...' }, error as ErrorInput) } // enabling the rule and just doing unknown, requires the caller to typecast for such intellisense: // try {} catch (error: unknown) { throw detailedError({ message: '...', code: '...' } as ErrorLike, error as ErrorInput) } /** * Ensure the error is a proper error instance, and when stringified include any code, level, and parent details if defined upon construction. * We do this instead of a class extension, as class extensions do not interop well on Node.js 0.8, which is a target. * @param error - The error, or its details, accepts {@link ErrorInput}, ideally {@link ErrorLike} * @param parents - The parent(s) of the error, accepts {@link ErrorLike} * @returns The detailed error instance, matching {@link ErrorDetailed} * @throws {Error} If the error or parent were incompatible with {@link ErrorLike} * @example * ```ts * try {} catch (error: unknown) { throw detailedError({ message: '...', code: '...' } as ErrorLike, error as ErrorInput) } * ``` */ export function detailedError( error: ErrorLike, ...parents: unknown[] ): ErrorDetailed export function detailedError( error: ErrorInput, ...parents: unknown[] ): ErrorDetailed export function detailedError( error: unknown, ...parents: unknown[] ): ErrorDetailed export function detailedError<T>( error: T extends ErrorInput ? ErrorInput : unknown, ...parents: unknown[] ): ErrorDetailed { let errorLike: Error & ErrorLike if (typeof error === 'string') { errorLike = new Error(error) errorLike.code ??= null errorLike.level ??= null } else if (error instanceof Error) { errorLike = error as Error & ErrorLike errorLike.code ??= null errorLike.level ??= null } else { assertErrorLike(error) errorLike = new Error(error.message) errorLike.code = error.code ?? null errorLike.level = error.level ?? null } // ensure parents are correct, note this is not all ancestors, just immediate parents if (errorLike.parent && parents.indexOf(errorLike.parent) === -1) { parents.push(errorLike.parent) } if (errorLike.parents && Array.isArray(errorLike.parents)) { parents.push(...(errorLike.parents as unknown[])) } errorLike.parents = [] // error now has all the correct types const errorDetailed = errorLike as ErrorDetailed // ensure parents are valid, and not duplicated for (const parent of parents) { assertErrorLike(parent) if (errorDetailed.parents.indexOf(parent) === -1) { errorDetailed.parents.push(parent) } } // adjust the properties if necessary if ( errorDetailed.code && excludes(errorDetailed.message, `${errorDetailed.code}: `) ) { errorDetailed.message = `${errorDetailed.code}: ${errorDetailed.message}` errorDetailed.stack = `${errorDetailed.code}: ${errorDetailed.stack}` } if ( errorDetailed.level && excludes(errorDetailed.message, `${errorDetailed.level}: `) ) { errorDetailed.message = `${errorDetailed.level}: ${errorDetailed.message}` errorDetailed.stack = `${errorDetailed.level}: ${errorDetailed.stack}` } for (const parent of errorDetailed.parents) { if (excludes(errorDetailed.message, `\n↪ ${parent.message}`)) { errorDetailed.message = `${errorDetailed.message}\n↪ ${parent.message || parent}` errorDetailed.stack = `${errorDetailed.stack}\n↪ ${parent.stack || parent.message || parent.parent}` } } // return return errorDetailed }