UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

135 lines (134 loc) 5.13 kB
import { _isBackendErrorResponseObject, _isErrorLike, _isErrorObject } from '../error/error.util.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 = _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) { globalStringifyFunction = fn; } /** * 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, opt = {}) { 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; // 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.code === 'string') { // Error that has no `data`, but has `code` property s += `\ncode: ${obj.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; }