@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
192 lines (163 loc) • 5.78 kB
text/typescript
import { _isBackendErrorResponseObject, _isErrorLike, _isErrorObject } from '../error/error.util.js'
import type { Reviver } from '../types.js'
import { _jsonParseIfPossible } from './json.util.js'
import { _safeJsonStringify } from './safeJsonStringify.js'
import { _truncateMiddle } from './string.util.js'
const supportsAggregateError = typeof globalThis.AggregateError === 'function'
let globalStringifyFunction: JsonStringifyFunction = _safeJsonStringify
/**
* Allows to set Global "stringifyFunction" that will be used to "pretty-print" objects
* in various cases.
*
* Used, for example, by _stringify() to pretty-print objects/arrays.
*
* Defaults to _safeJsonStringify.
*
* Node.js project can set it to _inspect, which allows to use `util.inspect`
* as pretty-printing function.
*
* It's recommended that this function is circular-reference-safe.
*/
export function setGlobalStringifyFunction(fn: JsonStringifyFunction): void {
globalStringifyFunction = fn
}
export type JsonStringifyFunction = (obj: any, reviver?: Reviver, space?: number) => string
export interface StringifyOptions {
/**
* @default 10_000
* Default limit is less than in Node, cause it's likely to be used e.g in Browser alert()
*/
maxLen?: number
/**
* Set to true to print Error.stack instead of just Error.message.
*
* @default false
*/
includeErrorStack?: boolean
/**
* Set to false to skip including Error.cause.
*
* @default true
*/
includeErrorCause?: boolean
/**
* Set to true to include Error.data.
*
* @default false
*/
includeErrorData?: boolean
/**
* Allows to pass custom "stringify function".
* E.g in Node.js you can pass `util.inspect` instead.
*
* Defaults to `globalStringifyFunction`, which defaults to `_safeJsonStringify`
*/
stringifyFn?: JsonStringifyFunction
}
/**
* Inspired by `_inspect` from nodejs-lib, which is based on util.inpect that is not available in the Browser.
* Potentially can do this (with extra 2Kb gz size): https://github.com/deecewan/browser-util-inspect
*
* Transforms ANY to human-readable string (via JSON.stringify pretty).
* Safe (no error throwing).
*
* Correctly prints Errors, AppErrors, ErrorObjects: error.message + \n + _stringify(error.data)
*
* Enforces max length (default to 1000, pass 0 to skip it).
*
* Logs numbers as-is, e.g: `6`.
* Logs strings as-is (without single quotes around, unlike default util.inspect behavior).
* Otherwise - just uses JSON.stringify().
*
* Returns 'empty_string' if empty string is passed.
* Returns 'undefined' if undefined is passed (default util.inspect behavior).
*/
export function _stringify(obj: any, opt: StringifyOptions = {}): string {
if (obj === undefined) return 'undefined'
if (obj === null) return 'null'
if (typeof obj === 'function') return 'function'
if (typeof obj === 'symbol') return obj.toString()
let s: string
// Parse JSON string, if possible
obj = _jsonParseIfPossible(obj) // in case it's e.g non-pretty JSON, or even a stringified ErrorObject
//
// HttpErrorResponse
//
if (_isBackendErrorResponseObject(obj)) {
return _stringify(obj.error, opt)
}
if (obj instanceof Error || _isErrorLike(obj)) {
const { includeErrorCause = true } = opt
//
// Error or ErrorLike
//
// Omit "default" error name as non-informative
// UPD: no, it's still important to understand that we're dealing with Error and not just some string
// if (obj?.name === 'Error') {
// s = obj.message
// }
// if (_isErrorObject(obj) && _isHttpErrorObject(obj)) {
// // Printing (0) to avoid ambiguity
// s = `${obj.name}(${obj.data.backendResponseStatusCode}): ${obj.message}`
// }
s = [obj.name, obj.message].filter(Boolean).join(': ')
if (typeof (obj as any).code === 'string') {
// Error that has no `data`, but has `code` property
s += `\ncode: ${(obj as any).code}`
}
if (opt.includeErrorData && _isErrorObject(obj) && Object.keys(obj.data).length) {
s += '\n' + _stringify(obj.data, opt)
}
if (opt.includeErrorStack && obj.stack) {
// Here we're using the previously-generated "title line" (e.g "Error: some_message"),
// concatenating it with the Stack (but without the title line of the Stack)
// This is to fix the rare error (happened with Got) where `err.message` was changed,
// but err.stack had "old" err.message
// This should "fix" that
const sLines = s.split('\n').length
s = [s, ...obj.stack.split('\n').slice(sLines)].join('\n')
}
if (supportsAggregateError && obj instanceof AggregateError && obj.errors.length) {
s = [
s,
`${obj.errors.length} error(s):`,
...obj.errors.map((err, i) => `${i + 1}. ${_stringify(err, opt)}`),
].join('\n')
}
if (obj.cause && includeErrorCause) {
s = s + '\nCaused by: ' + _stringify(obj.cause, opt)
}
} else if (typeof obj === 'string') {
//
// String
//
s = obj.trim() || 'empty_string'
} else {
//
// Other
//
if (obj instanceof Map) {
obj = Object.fromEntries(obj)
} else if (obj instanceof Set) {
obj = [...obj]
}
try {
const { stringifyFn = globalStringifyFunction } = opt
s = stringifyFn(obj, undefined, 2)
} catch {
s = String(obj) // fallback
}
}
// Shouldn't happen, but some weird input parameters may return this
if (s === undefined) return 'undefined'
// Handle maxLen
const { maxLen = 10_000 } = opt
if (maxLen && s.length > maxLen) {
return _truncateMiddle(
s,
maxLen,
`\n... ${Math.ceil(s.length / 1024)} Kb message truncated ...\n`,
)
}
return s
}