strong-tie/inbound-calls
0
1'use strict'2 3const { hasOwnProperty } = Object.prototype4 5const stringify = configure()6 7// @ts-expect-error8stringify.configure = configure9// @ts-expect-error10stringify.stringify = stringify11 12// @ts-expect-error13stringify.default = stringify14 15// @ts-expect-error used for named export16exports.stringify = stringify17// @ts-expect-error used for named export18exports.configure = configure19 20module.exports = stringify21 22// eslint-disable-next-line no-control-regex23const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/24 25// Escape C0 control characters, double quotes, the backslash and every code26// unit with a numeric value in the inclusive range 0xD800 to 0xDFFF.27function strEscape (str) {28 // Some magic numbers that worked out fine while benchmarking with v8 8.029 if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) {30 return `"${str}"`31 }32 return JSON.stringify(str)33}34 35function sort (array, comparator) {36 // Insertion sort is very efficient for small input sizes, but it has a bad37 // worst case complexity. Thus, use native array sort for bigger values.38 if (array.length > 2e2 || comparator) {39 return array.sort(comparator)40 }41 for (let i = 1; i < array.length; i++) {42 const currentValue = array[i]43 let position = i44 while (position !== 0 && array[position - 1] > currentValue) {45 array[position] = array[position - 1]46 position--47 }48 array[position] = currentValue49 }50 return array51}52 53const typedArrayPrototypeGetSymbolToStringTag =54 Object.getOwnPropertyDescriptor(55 Object.getPrototypeOf(56 Object.getPrototypeOf(57 new Int8Array()58 )59 ),60 Symbol.toStringTag61 ).get62 63function isTypedArrayWithEntries (value) {64 return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined && value.length !== 065}66 67function stringifyTypedArray (array, separator, maximumBreadth) {68 if (array.length < maximumBreadth) {69 maximumBreadth = array.length70 }71 const whitespace = separator === ',' ? '' : ' '72 let res = `"0":${whitespace}${array[0]}`73 for (let i = 1; i < maximumBreadth; i++) {74 res += `${separator}"${i}":${whitespace}${array[i]}`75 }76 return res77}78 79function getCircularValueOption (options) {80 if (hasOwnProperty.call(options, 'circularValue')) {81 const circularValue = options.circularValue82 if (typeof circularValue === 'string') {83 return `"${circularValue}"`84 }85 if (circularValue == null) {86 return circularValue87 }88 if (circularValue === Error || circularValue === TypeError) {89 return {90 toString () {91 throw new TypeError('Converting circular structure to JSON')92 }93 }94 }95 throw new TypeError('The "circularValue" argument must be of type string or the value null or undefined')96 }97 return '"[Circular]"'98}99 100function getDeterministicOption (options) {101 let value102 if (hasOwnProperty.call(options, 'deterministic')) {103 value = options.deterministic104 if (typeof value !== 'boolean' && typeof value !== 'function') {105 throw new TypeError('The "deterministic" argument must be of type boolean or comparator function')106 }107 }108 return value === undefined ? true : value109}110 111function getBooleanOption (options, key) {112 let value113 if (hasOwnProperty.call(options, key)) {114 value = options[key]115 if (typeof value !== 'boolean') {116 throw new TypeError(`The "${key}" argument must be of type boolean`)117 }118 }119 return value === undefined ? true : value120}121 122function getPositiveIntegerOption (options, key) {123 let value124 if (hasOwnProperty.call(options, key)) {125 value = options[key]126 if (typeof value !== 'number') {127 throw new TypeError(`The "${key}" argument must be of type number`)128 }129 if (!Number.isInteger(value)) {130 throw new TypeError(`The "${key}" argument must be an integer`)131 }132 if (value < 1) {133 throw new RangeError(`The "${key}" argument must be >= 1`)134 }135 }136 return value === undefined ? Infinity : value137}138 139function getItemCount (number) {140 if (number === 1) {141 return '1 item'142 }143 return `${number} items`144}145 146function getUniqueReplacerSet (replacerArray) {147 const replacerSet = new Set()148 for (const value of replacerArray) {149 if (typeof value === 'string' || typeof value === 'number') {150 replacerSet.add(String(value))151 }152 }153 return replacerSet154}155 156function getStrictOption (options) {157 if (hasOwnProperty.call(options, 'strict')) {158 const value = options.strict159 if (typeof value !== 'boolean') {160 throw new TypeError('The "strict" argument must be of type boolean')161 }162 if (value) {163 return (value) => {164 let message = `Object can not safely be stringified. Received type ${typeof value}`165 if (typeof value !== 'function') message += ` (${value.toString()})`166 throw new Error(message)167 }168 }169 }170}171 172function configure (options) {173 options = { ...options }174 const fail = getStrictOption(options)175 if (fail) {176 if (options.bigint === undefined) {177 options.bigint = false178 }179 if (!('circularValue' in options)) {180 options.circularValue = Error181 }182 }183 const circularValue = getCircularValueOption(options)184 const bigint = getBooleanOption(options, 'bigint')185 const deterministic = getDeterministicOption(options)186 const comparator = typeof deterministic === 'function' ? deterministic : undefined187 const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth')188 const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth')189 190 function stringifyFnReplacer (key, parent, stack, replacer, spacer, indentation) {191 let value = parent[key]192 193 if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {194 value = value.toJSON(key)195 }196 value = replacer.call(parent, key, value)197 198 switch (typeof value) {199 case 'string':200 return strEscape(value)201 case 'object': {202 if (value === null) {203 return 'null'204 }205 if (stack.indexOf(value) !== -1) {206 return circularValue207 }208 209 let res = ''210 let join = ','211 const originalIndentation = indentation212 213 if (Array.isArray(value)) {214 if (value.length === 0) {215 return '[]'216 }217 if (maximumDepth < stack.length + 1) {218 return '"[Array]"'219 }220 stack.push(value)221 if (spacer !== '') {222 indentation += spacer223 res += `\n${indentation}`224 join = `,\n${indentation}`225 }226 const maximumValuesToStringify = Math.min(value.length, maximumBreadth)227 let i = 0228 for (; i < maximumValuesToStringify - 1; i++) {229 const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation)230 res += tmp !== undefined ? tmp : 'null'231 res += join232 }233 const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation)234 res += tmp !== undefined ? tmp : 'null'235 if (value.length - 1 > maximumBreadth) {236 const removedKeys = value.length - maximumBreadth - 1237 res += `${join}"... ${getItemCount(removedKeys)} not stringified"`238 }239 if (spacer !== '') {240 res += `\n${originalIndentation}`241 }242 stack.pop()243 return `[${res}]`244 }245 246 let keys = Object.keys(value)247 const keyLength = keys.length248 if (keyLength === 0) {249 return '{}'250 }251 if (maximumDepth < stack.length + 1) {252 return '"[Object]"'253 }254 let whitespace = ''255 let separator = ''256 if (spacer !== '') {257 indentation += spacer258 join = `,\n${indentation}`259 whitespace = ' '260 }261 const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)262 if (deterministic && !isTypedArrayWithEntries(value)) {263 keys = sort(keys, comparator)264 }265 stack.push(value)266 for (let i = 0; i < maximumPropertiesToStringify; i++) {267 const key = keys[i]268 const tmp = stringifyFnReplacer(key, value, stack, replacer, spacer, indentation)269 if (tmp !== undefined) {270 res += `${separator}${strEscape(key)}:${whitespace}${tmp}`271 separator = join272 }273 }274 if (keyLength > maximumBreadth) {275 const removedKeys = keyLength - maximumBreadth276 res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`277 separator = join278 }279 if (spacer !== '' && separator.length > 1) {280 res = `\n${indentation}${res}\n${originalIndentation}`281 }282 stack.pop()283 return `{${res}}`284 }285 case 'number':286 return isFinite(value) ? String(value) : fail ? fail(value) : 'null'287 case 'boolean':288 return value === true ? 'true' : 'false'289 case 'undefined':290 return undefined291 case 'bigint':292 if (bigint) {293 return String(value)294 }295 // fallthrough296 default:297 return fail ? fail(value) : undefined298 }299 }300 301 function stringifyArrayReplacer (key, value, stack, replacer, spacer, indentation) {302 if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {303 value = value.toJSON(key)304 }305 306 switch (typeof value) {307 case 'string':308 return strEscape(value)309 case 'object': {310 if (value === null) {311 return 'null'312 }313 if (stack.indexOf(value) !== -1) {314 return circularValue315 }316 317 const originalIndentation = indentation318 let res = ''319 let join = ','320 321 if (Array.isArray(value)) {322 if (value.length === 0) {323 return '[]'324 }325 if (maximumDepth < stack.length + 1) {326 return '"[Array]"'327 }328 stack.push(value)329 if (spacer !== '') {330 indentation += spacer331 res += `\n${indentation}`332 join = `,\n${indentation}`333 }334 const maximumValuesToStringify = Math.min(value.length, maximumBreadth)335 let i = 0336 for (; i < maximumValuesToStringify - 1; i++) {337 const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation)338 res += tmp !== undefined ? tmp : 'null'339 res += join340 }341 const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation)342 res += tmp !== undefined ? tmp : 'null'343 if (value.length - 1 > maximumBreadth) {344 const removedKeys = value.length - maximumBreadth - 1345 res += `${join}"... ${getItemCount(removedKeys)} not stringified"`346 }347 if (spacer !== '') {348 res += `\n${originalIndentation}`349 }350 stack.pop()351 return `[${res}]`352 }353 stack.push(value)354 let whitespace = ''355 if (spacer !== '') {356 indentation += spacer357 join = `,\n${indentation}`358 whitespace = ' '359 }360 let separator = ''361 for (const key of replacer) {362 const tmp = stringifyArrayReplacer(key, value[key], stack, replacer, spacer, indentation)363 if (tmp !== undefined) {364 res += `${separator}${strEscape(key)}:${whitespace}${tmp}`365 separator = join366 }367 }368 if (spacer !== '' && separator.length > 1) {369 res = `\n${indentation}${res}\n${originalIndentation}`370 }371 stack.pop()372 return `{${res}}`373 }374 case 'number':375 return isFinite(value) ? String(value) : fail ? fail(value) : 'null'376 case 'boolean':377 return value === true ? 'true' : 'false'378 case 'undefined':379 return undefined380 case 'bigint':381 if (bigint) {382 return String(value)383 }384 // fallthrough385 default:386 return fail ? fail(value) : undefined387 }388 }389 390 function stringifyIndent (key, value, stack, spacer, indentation) {391 switch (typeof value) {392 case 'string':393 return strEscape(value)394 case 'object': {395 if (value === null) {396 return 'null'397 }398 if (typeof value.toJSON === 'function') {399 value = value.toJSON(key)400 // Prevent calling `toJSON` again.401 if (typeof value !== 'object') {402 return stringifyIndent(key, value, stack, spacer, indentation)403 }404 if (value === null) {405 return 'null'406 }407 }408 if (stack.indexOf(value) !== -1) {409 return circularValue410 }411 const originalIndentation = indentation412 413 if (Array.isArray(value)) {414 if (value.length === 0) {415 return '[]'416 }417 if (maximumDepth < stack.length + 1) {418 return '"[Array]"'419 }420 stack.push(value)421 indentation += spacer422 let res = `\n${indentation}`423 const join = `,\n${indentation}`424 const maximumValuesToStringify = Math.min(value.length, maximumBreadth)425 let i = 0426 for (; i < maximumValuesToStringify - 1; i++) {427 const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation)428 res += tmp !== undefined ? tmp : 'null'429 res += join430 }431 const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation)432 res += tmp !== undefined ? tmp : 'null'433 if (value.length - 1 > maximumBreadth) {434 const removedKeys = value.length - maximumBreadth - 1435 res += `${join}"... ${getItemCount(removedKeys)} not stringified"`436 }437 res += `\n${originalIndentation}`438 stack.pop()439 return `[${res}]`440 }441 442 let keys = Object.keys(value)443 const keyLength = keys.length444 if (keyLength === 0) {445 return '{}'446 }447 if (maximumDepth < stack.length + 1) {448 return '"[Object]"'449 }450 indentation += spacer451 const join = `,\n${indentation}`452 let res = ''453 let separator = ''454 let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)455 if (isTypedArrayWithEntries(value)) {456 res += stringifyTypedArray(value, join, maximumBreadth)457 keys = keys.slice(value.length)458 maximumPropertiesToStringify -= value.length459 separator = join460 }461 if (deterministic) {462 keys = sort(keys, comparator)463 }464 stack.push(value)465 for (let i = 0; i < maximumPropertiesToStringify; i++) {466 const key = keys[i]467 const tmp = stringifyIndent(key, value[key], stack, spacer, indentation)468 if (tmp !== undefined) {469 res += `${separator}${strEscape(key)}: ${tmp}`470 separator = join471 }472 }473 if (keyLength > maximumBreadth) {474 const removedKeys = keyLength - maximumBreadth475 res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`476 separator = join477 }478 if (separator !== '') {479 res = `\n${indentation}${res}\n${originalIndentation}`480 }481 stack.pop()482 return `{${res}}`483 }484 case 'number':485 return isFinite(value) ? String(value) : fail ? fail(value) : 'null'486 case 'boolean':487 return value === true ? 'true' : 'false'488 case 'undefined':489 return undefined490 case 'bigint':491 if (bigint) {492 return String(value)493 }494 // fallthrough495 default:496 return fail ? fail(value) : undefined497 }498 }499 500 function stringifySimple (key, value, stack) {501 switch (typeof value) {502 case 'string':503 return strEscape(value)504 case 'object': {505 if (value === null) {506 return 'null'507 }508 if (typeof value.toJSON === 'function') {509 value = value.toJSON(key)510 // Prevent calling `toJSON` again511 if (typeof value !== 'object') {512 return stringifySimple(key, value, stack)513 }514 if (value === null) {515 return 'null'516 }517 }518 if (stack.indexOf(value) !== -1) {519 return circularValue520 }521 522 let res = ''523 524 const hasLength = value.length !== undefined525 if (hasLength && Array.isArray(value)) {526 if (value.length === 0) {527 return '[]'528 }529 if (maximumDepth < stack.length + 1) {530 return '"[Array]"'531 }532 stack.push(value)533 const maximumValuesToStringify = Math.min(value.length, maximumBreadth)534 let i = 0535 for (; i < maximumValuesToStringify - 1; i++) {536 const tmp = stringifySimple(String(i), value[i], stack)537 res += tmp !== undefined ? tmp : 'null'538 res += ','539 }540 const tmp = stringifySimple(String(i), value[i], stack)541 res += tmp !== undefined ? tmp : 'null'542 if (value.length - 1 > maximumBreadth) {543 const removedKeys = value.length - maximumBreadth - 1544 res += `,"... ${getItemCount(removedKeys)} not stringified"`545 }546 stack.pop()547 return `[${res}]`548 }549 550 let keys = Object.keys(value)551 const keyLength = keys.length552 if (keyLength === 0) {553 return '{}'554 }555 if (maximumDepth < stack.length + 1) {556 return '"[Object]"'557 }558 let separator = ''559 let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)560 if (hasLength && isTypedArrayWithEntries(value)) {561 res += stringifyTypedArray(value, ',', maximumBreadth)562 keys = keys.slice(value.length)563 maximumPropertiesToStringify -= value.length564 separator = ','565 }566 if (deterministic) {567 keys = sort(keys, comparator)568 }569 stack.push(value)570 for (let i = 0; i < maximumPropertiesToStringify; i++) {571 const key = keys[i]572 const tmp = stringifySimple(key, value[key], stack)573 if (tmp !== undefined) {574 res += `${separator}${strEscape(key)}:${tmp}`575 separator = ','576 }577 }578 if (keyLength > maximumBreadth) {579 const removedKeys = keyLength - maximumBreadth580 res += `${separator}"...":"${getItemCount(removedKeys)} not stringified"`581 }582 stack.pop()583 return `{${res}}`584 }585 case 'number':586 return isFinite(value) ? String(value) : fail ? fail(value) : 'null'587 case 'boolean':588 return value === true ? 'true' : 'false'589 case 'undefined':590 return undefined591 case 'bigint':592 if (bigint) {593 return String(value)594 }595 // fallthrough596 default:597 return fail ? fail(value) : undefined598 }599 }600 601 function stringify (value, replacer, space) {602 if (arguments.length > 1) {603 let spacer = ''604 if (typeof space === 'number') {605 spacer = ' '.repeat(Math.min(space, 10))606 } else if (typeof space === 'string') {607 spacer = space.slice(0, 10)608 }609 if (replacer != null) {610 if (typeof replacer === 'function') {611 return stringifyFnReplacer('', { '': value }, [], replacer, spacer, '')612 }613 if (Array.isArray(replacer)) {614 return stringifyArrayReplacer('', value, [], getUniqueReplacerSet(replacer), spacer, '')615 }616 }617 if (spacer.length !== 0) {618 return stringifyIndent('', value, [], spacer, '')619 }620 }621 return stringifySimple('', value, [])622 }623 624 return stringify625}626 