'use strict' const { hasOwnProperty } = Object.prototype const stringify = configure() // @ts-expect-error stringify.configure = configure // @ts-expect-error stringify.stringify = stringify // @ts-expect-error stringify.default = stringify // @ts-expect-error used for named export exports.stringify = stringify // @ts-expect-error used for named export exports.configure = configure module.exports = stringify // eslint-disable-next-line no-control-regex const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/ // Escape C0 control characters, double quotes, the backslash and every code // unit with a numeric value in the inclusive range 0xD800 to 0xDFFF. function strEscape (str) { // Some magic numbers that worked out fine while benchmarking with v8 8.0 if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) { return `"${str}"` } return JSON.stringify(str) } function insertSort (array) { // Insertion sort is very efficient for small input sizes but it has a bad // worst case complexity. Thus, use native array sort for bigger values. if (array.length > 2e2) { return array.sort() } for (let i = 1; i < array.length; i++) { const currentValue = array[i] let position = i while (position !== 0 && array[position - 1] > currentValue) { array[position] = array[position - 1] position-- } array[position] = currentValue } return array } const typedArrayPrototypeGetSymbolToStringTag = Object.getOwnPropertyDescriptor( Object.getPrototypeOf( Object.getPrototypeOf( new Int8Array() ) ), Symbol.toStringTag ).get function isTypedArrayWithEntries (value) { return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined && value.length !== 0 } function stringifyTypedArray (array, separator, maximumBreadth) { if (array.length < maximumBreadth) { maximumBreadth = array.length } const whitespace = separator === ',' ? '' : ' ' let res = `"0":${whitespace}${array[0]}` for (let i = 1; i < maximumBreadth; i++) { res += `${separator}"${i}":${whitespace}${array[i]}` } return res } function getCircularValueOption (options) { if (hasOwnProperty.call(options, 'circularValue')) { const circularValue = options.circularValue if (typeof circularValue === 'string') { return `"${circularValue}"` } if (circularValue == null) { return circularValue } if (circularValue === Error || circularValue === TypeError) { return { toString () { throw new TypeError('Converting circular structure to JSON') } } } throw new TypeError('The "circularValue" argument must be of type string or the value null or undefined') } return '"[Circular]"' } function getBooleanOption (options, key) { let value if (hasOwnProperty.call(options, key)) { value = options[key] if (typeof value !== 'boolean') { throw new TypeError(`The "${key}" argument must be of type boolean`) } } return value === undefined ? true : value } function getPositiveIntegerOption (options, key) { let value if (hasOwnProperty.call(options, key)) { value = options[key] if (typeof value !== 'number') { throw new TypeError(`The "${key}" argument must be of type number`) } if (!Number.isInteger(value)) { throw new TypeError(`The "${key}" argument must be an integer`) } if (value < 1) { throw new RangeError(`The "${key}" argument must be >= 1`) } } return value === undefined ? Infinity : value } function getItemCount (number) { if (number === 1) { return '1 item' } return `${number} items` } function getUniqueReplacerSet (replacerArray) { const replacerSet = new Set() for (const value of replacerArray) { if (typeof value === 'string' || typeof value === 'number') { replacerSet.add(String(value)) } } return replacerSet } function getStrictOption (options) { if (hasOwnProperty.call(options, 'strict')) { const value = options.strict if (typeof value !== 'boolean') { throw new TypeError('The "strict" argument must be of type boolean') } if (value) { return (value) => { let message = `Object can not safely be stringified. Received type ${typeof value}` if (typeof value !== 'function') message += ` (${value.toString()})` throw new Error(message) } } } } function configure (options) { options = { ...options } const fail = getStrictOption(options) if (fail) { if (options.bigint === undefined) { options.bigint = false } if (!('circularValue' in options)) { options.circularValue = Error } } const circularValue = getCircularValueOption(options) const bigint = getBooleanOption(options, 'bigint') const deterministic = getBooleanOption(options, 'deterministic') const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth') const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth') function stringifyFnReplacer (key, parent, stack, replacer, spacer, indentation) { let value = parent[key] if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') { value = value.toJSON(key) } value = replacer.call(parent, key, value) switch (typeof value) { case 'string': return strEscape(value) case 'object': { if (value === null) { return 'null' } if (stack.indexOf(value) !== -1) { return circularValue } let res = '' let join = ',' const originalIndentation = indentation if (Array.isArray(value)) { if (value.length === 0) { return '[]' } if (maximumDepth < stack.length + 1) { return '"[Array]"' } stack.push(value) if (spacer !== '') { indentation += spacer res += `\n${indentation}` join = `,\n${indentation}` } const maximumValuesToStringify = Math.min(value.length, maximumBreadth) let i = 0 for (; i < maximumValuesToStringify - 1; i++) { const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation) res += tmp !== undefined ? tmp : 'null' res += join } const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation) res += tmp !== undefined ? tmp : 'null' if (value.length - 1 > maximumBreadth) { const removedKeys = value.length - maximumBreadth - 1 res += `${join}"... ${getItemCount(removedKeys)} not stringified"` } if (spacer !== '') { res += `\n${originalIndentation}` } stack.pop() return `[${res}]` } let keys = Object.keys(value) const keyLength = keys.length if (keyLength === 0) { return '{}' } if (maximumDepth < stack.length + 1) { return '"[Object]"' } let whitespace = '' let separator = '' if (spacer !== '') { indentation += spacer join = `,\n${indentation}` whitespace = ' ' } const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) if (deterministic && !isTypedArrayWithEntries(value)) { keys = insertSort(keys) } stack.push(value) for (let i = 0; i < maximumPropertiesToStringify; i++) { const key = keys[i] const tmp = stringifyFnReplacer(key, value, stack, replacer, spacer, indentation) if (tmp !== undefined) { res += `${separator}${strEscape(key)}:${whitespace}${tmp}` separator = join } } if (keyLength > maximumBreadth) { const removedKeys = keyLength - maximumBreadth res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"` separator = join } if (spacer !== '' && separator.length > 1) { res = `\n${indentation}${res}\n${originalIndentation}` } stack.pop() return `{${res}}` } case 'number': return isFinite(value) ? String(value) : fail ? fail(value) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'undefined': return undefined case 'bigint': if (bigint) { return String(value) } // fallthrough default: return fail ? fail(value) : undefined } } function stringifyArrayReplacer (key, value, stack, replacer, spacer, indentation) { if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') { value = value.toJSON(key) } switch (typeof value) { case 'string': return strEscape(value) case 'object': { if (value === null) { return 'null' } if (stack.indexOf(value) !== -1) { return circularValue } const originalIndentation = indentation let res = '' let join = ',' if (Array.isArray(value)) { if (value.length === 0) { return '[]' } if (maximumDepth < stack.length + 1) { return '"[Array]"' } stack.push(value) if (spacer !== '') { indentation += spacer res += `\n${indentation}` join = `,\n${indentation}` } const maximumValuesToStringify = Math.min(value.length, maximumBreadth) let i = 0 for (; i < maximumValuesToStringify - 1; i++) { const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation) res += tmp !== undefined ? tmp : 'null' res += join } const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation) res += tmp !== undefined ? tmp : 'null' if (value.length - 1 > maximumBreadth) { const removedKeys = value.length - maximumBreadth - 1 res += `${join}"... ${getItemCount(removedKeys)} not stringified"` } if (spacer !== '') { res += `\n${originalIndentation}` } stack.pop() return `[${res}]` } stack.push(value) let whitespace = '' if (spacer !== '') { indentation += spacer join = `,\n${indentation}` whitespace = ' ' } let separator = '' for (const key of replacer) { const tmp = stringifyArrayReplacer(key, value[key], stack, replacer, spacer, indentation) if (tmp !== undefined) { res += `${separator}${strEscape(key)}:${whitespace}${tmp}` separator = join } } if (spacer !== '' && separator.length > 1) { res = `\n${indentation}${res}\n${originalIndentation}` } stack.pop() return `{${res}}` } case 'number': return isFinite(value) ? String(value) : fail ? fail(value) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'undefined': return undefined case 'bigint': if (bigint) { return String(value) } // fallthrough default: return fail ? fail(value) : undefined } } function stringifyIndent (key, value, stack, spacer, indentation) { switch (typeof value) { case 'string': return strEscape(value) case 'object': { if (value === null) { return 'null' } if (typeof value.toJSON === 'function') { value = value.toJSON(key) // Prevent calling `toJSON` again. if (typeof value !== 'object') { return stringifyIndent(key, value, stack, spacer, indentation) } if (value === null) { return 'null' } } if (stack.indexOf(value) !== -1) { return circularValue } const originalIndentation = indentation if (Array.isArray(value)) { if (value.length === 0) { return '[]' } if (maximumDepth < stack.length + 1) { return '"[Array]"' } stack.push(value) indentation += spacer let res = `\n${indentation}` const join = `,\n${indentation}` const maximumValuesToStringify = Math.min(value.length, maximumBreadth) let i = 0 for (; i < maximumValuesToStringify - 1; i++) { const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation) res += tmp !== undefined ? tmp : 'null' res += join } const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation) res += tmp !== undefined ? tmp : 'null' if (value.length - 1 > maximumBreadth) { const removedKeys = value.length - maximumBreadth - 1 res += `${join}"... ${getItemCount(removedKeys)} not stringified"` } res += `\n${originalIndentation}` stack.pop() return `[${res}]` } let keys = Object.keys(value) const keyLength = keys.length if (keyLength === 0) { return '{}' } if (maximumDepth < stack.length + 1) { return '"[Object]"' } indentation += spacer const join = `,\n${indentation}` let res = '' let separator = '' let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) if (isTypedArrayWithEntries(value)) { res += stringifyTypedArray(value, join, maximumBreadth) keys = keys.slice(value.length) maximumPropertiesToStringify -= value.length separator = join } if (deterministic) { keys = insertSort(keys) } stack.push(value) for (let i = 0; i < maximumPropertiesToStringify; i++) { const key = keys[i] const tmp = stringifyIndent(key, value[key], stack, spacer, indentation) if (tmp !== undefined) { res += `${separator}${strEscape(key)}: ${tmp}` separator = join } } if (keyLength > maximumBreadth) { const removedKeys = keyLength - maximumBreadth res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"` separator = join } if (separator !== '') { res = `\n${indentation}${res}\n${originalIndentation}` } stack.pop() return `{${res}}` } case 'number': return isFinite(value) ? String(value) : fail ? fail(value) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'undefined': return undefined case 'bigint': if (bigint) { return String(value) } // fallthrough default: return fail ? fail(value) : undefined } } function stringifySimple (key, value, stack) { switch (typeof value) { case 'string': return strEscape(value) case 'object': { if (value === null) { return 'null' } if (typeof value.toJSON === 'function') { value = value.toJSON(key) // Prevent calling `toJSON` again if (typeof value !== 'object') { return stringifySimple(key, value, stack) } if (value === null) { return 'null' } } if (stack.indexOf(value) !== -1) { return circularValue } let res = '' if (Array.isArray(value)) { if (value.length === 0) { return '[]' } if (maximumDepth < stack.length + 1) { return '"[Array]"' } stack.push(value) const maximumValuesToStringify = Math.min(value.length, maximumBreadth) let i = 0 for (; i < maximumValuesToStringify - 1; i++) { const tmp = stringifySimple(String(i), value[i], stack) res += tmp !== undefined ? tmp : 'null' res += ',' } const tmp = stringifySimple(String(i), value[i], stack) res += tmp !== undefined ? tmp : 'null' if (value.length - 1 > maximumBreadth) { const removedKeys = value.length - maximumBreadth - 1 res += `,"... ${getItemCount(removedKeys)} not stringified"` } stack.pop() return `[${res}]` } let keys = Object.keys(value) const keyLength = keys.length if (keyLength === 0) { return '{}' } if (maximumDepth < stack.length + 1) { return '"[Object]"' } let separator = '' let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) if (isTypedArrayWithEntries(value)) { res += stringifyTypedArray(value, ',', maximumBreadth) keys = keys.slice(value.length) maximumPropertiesToStringify -= value.length separator = ',' } if (deterministic) { keys = insertSort(keys) } stack.push(value) for (let i = 0; i < maximumPropertiesToStringify; i++) { const key = keys[i] const tmp = stringifySimple(key, value[key], stack) if (tmp !== undefined) { res += `${separator}${strEscape(key)}:${tmp}` separator = ',' } } if (keyLength > maximumBreadth) { const removedKeys = keyLength - maximumBreadth res += `${separator}"...":"${getItemCount(removedKeys)} not stringified"` } stack.pop() return `{${res}}` } case 'number': return isFinite(value) ? String(value) : fail ? fail(value) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'undefined': return undefined case 'bigint': if (bigint) { return String(value) } // fallthrough default: return fail ? fail(value) : undefined } } function stringify (value, replacer, space) { if (arguments.length > 1) { let spacer = '' if (typeof space === 'number') { spacer = ' '.repeat(Math.min(space, 10)) } else if (typeof space === 'string') { spacer = space.slice(0, 10) } if (replacer != null) { if (typeof replacer === 'function') { return stringifyFnReplacer('', { '': value }, [], replacer, spacer, '') } if (Array.isArray(replacer)) { return stringifyArrayReplacer('', value, [], getUniqueReplacerSet(replacer), spacer, '') } } if (spacer.length !== 0) { return stringifyIndent('', value, [], spacer, '') } } return stringifySimple('', value, []) } return stringify }