index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. 'use strict'
  2. const { hasOwnProperty } = Object.prototype
  3. const stringify = configure()
  4. // @ts-expect-error
  5. stringify.configure = configure
  6. // @ts-expect-error
  7. stringify.stringify = stringify
  8. // @ts-expect-error
  9. stringify.default = stringify
  10. // @ts-expect-error used for named export
  11. exports.stringify = stringify
  12. // @ts-expect-error used for named export
  13. exports.configure = configure
  14. module.exports = stringify
  15. // eslint-disable-next-line no-control-regex
  16. const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/
  17. // Escape C0 control characters, double quotes, the backslash and every code
  18. // unit with a numeric value in the inclusive range 0xD800 to 0xDFFF.
  19. function strEscape (str) {
  20. // Some magic numbers that worked out fine while benchmarking with v8 8.0
  21. if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) {
  22. return `"${str}"`
  23. }
  24. return JSON.stringify(str)
  25. }
  26. function insertSort (array) {
  27. // Insertion sort is very efficient for small input sizes but it has a bad
  28. // worst case complexity. Thus, use native array sort for bigger values.
  29. if (array.length > 2e2) {
  30. return array.sort()
  31. }
  32. for (let i = 1; i < array.length; i++) {
  33. const currentValue = array[i]
  34. let position = i
  35. while (position !== 0 && array[position - 1] > currentValue) {
  36. array[position] = array[position - 1]
  37. position--
  38. }
  39. array[position] = currentValue
  40. }
  41. return array
  42. }
  43. const typedArrayPrototypeGetSymbolToStringTag =
  44. Object.getOwnPropertyDescriptor(
  45. Object.getPrototypeOf(
  46. Object.getPrototypeOf(
  47. new Int8Array()
  48. )
  49. ),
  50. Symbol.toStringTag
  51. ).get
  52. function isTypedArrayWithEntries (value) {
  53. return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined && value.length !== 0
  54. }
  55. function stringifyTypedArray (array, separator, maximumBreadth) {
  56. if (array.length < maximumBreadth) {
  57. maximumBreadth = array.length
  58. }
  59. const whitespace = separator === ',' ? '' : ' '
  60. let res = `"0":${whitespace}${array[0]}`
  61. for (let i = 1; i < maximumBreadth; i++) {
  62. res += `${separator}"${i}":${whitespace}${array[i]}`
  63. }
  64. return res
  65. }
  66. function getCircularValueOption (options) {
  67. if (hasOwnProperty.call(options, 'circularValue')) {
  68. const circularValue = options.circularValue
  69. if (typeof circularValue === 'string') {
  70. return `"${circularValue}"`
  71. }
  72. if (circularValue == null) {
  73. return circularValue
  74. }
  75. if (circularValue === Error || circularValue === TypeError) {
  76. return {
  77. toString () {
  78. throw new TypeError('Converting circular structure to JSON')
  79. }
  80. }
  81. }
  82. throw new TypeError('The "circularValue" argument must be of type string or the value null or undefined')
  83. }
  84. return '"[Circular]"'
  85. }
  86. function getBooleanOption (options, key) {
  87. let value
  88. if (hasOwnProperty.call(options, key)) {
  89. value = options[key]
  90. if (typeof value !== 'boolean') {
  91. throw new TypeError(`The "${key}" argument must be of type boolean`)
  92. }
  93. }
  94. return value === undefined ? true : value
  95. }
  96. function getPositiveIntegerOption (options, key) {
  97. let value
  98. if (hasOwnProperty.call(options, key)) {
  99. value = options[key]
  100. if (typeof value !== 'number') {
  101. throw new TypeError(`The "${key}" argument must be of type number`)
  102. }
  103. if (!Number.isInteger(value)) {
  104. throw new TypeError(`The "${key}" argument must be an integer`)
  105. }
  106. if (value < 1) {
  107. throw new RangeError(`The "${key}" argument must be >= 1`)
  108. }
  109. }
  110. return value === undefined ? Infinity : value
  111. }
  112. function getItemCount (number) {
  113. if (number === 1) {
  114. return '1 item'
  115. }
  116. return `${number} items`
  117. }
  118. function getUniqueReplacerSet (replacerArray) {
  119. const replacerSet = new Set()
  120. for (const value of replacerArray) {
  121. if (typeof value === 'string' || typeof value === 'number') {
  122. replacerSet.add(String(value))
  123. }
  124. }
  125. return replacerSet
  126. }
  127. function getStrictOption (options) {
  128. if (hasOwnProperty.call(options, 'strict')) {
  129. const value = options.strict
  130. if (typeof value !== 'boolean') {
  131. throw new TypeError('The "strict" argument must be of type boolean')
  132. }
  133. if (value) {
  134. return (value) => {
  135. let message = `Object can not safely be stringified. Received type ${typeof value}`
  136. if (typeof value !== 'function') message += ` (${value.toString()})`
  137. throw new Error(message)
  138. }
  139. }
  140. }
  141. }
  142. function configure (options) {
  143. options = { ...options }
  144. const fail = getStrictOption(options)
  145. if (fail) {
  146. if (options.bigint === undefined) {
  147. options.bigint = false
  148. }
  149. if (!('circularValue' in options)) {
  150. options.circularValue = Error
  151. }
  152. }
  153. const circularValue = getCircularValueOption(options)
  154. const bigint = getBooleanOption(options, 'bigint')
  155. const deterministic = getBooleanOption(options, 'deterministic')
  156. const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth')
  157. const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth')
  158. function stringifyFnReplacer (key, parent, stack, replacer, spacer, indentation) {
  159. let value = parent[key]
  160. if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {
  161. value = value.toJSON(key)
  162. }
  163. value = replacer.call(parent, key, value)
  164. switch (typeof value) {
  165. case 'string':
  166. return strEscape(value)
  167. case 'object': {
  168. if (value === null) {
  169. return 'null'
  170. }
  171. if (stack.indexOf(value) !== -1) {
  172. return circularValue
  173. }
  174. let res = ''
  175. let join = ','
  176. const originalIndentation = indentation
  177. if (Array.isArray(value)) {
  178. if (value.length === 0) {
  179. return '[]'
  180. }
  181. if (maximumDepth < stack.length + 1) {
  182. return '"[Array]"'
  183. }
  184. stack.push(value)
  185. if (spacer !== '') {
  186. indentation += spacer
  187. res += `\n${indentation}`
  188. join = `,\n${indentation}`
  189. }
  190. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  191. let i = 0
  192. for (; i < maximumValuesToStringify - 1; i++) {
  193. const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation)
  194. res += tmp !== undefined ? tmp : 'null'
  195. res += join
  196. }
  197. const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation)
  198. res += tmp !== undefined ? tmp : 'null'
  199. if (value.length - 1 > maximumBreadth) {
  200. const removedKeys = value.length - maximumBreadth - 1
  201. res += `${join}"... ${getItemCount(removedKeys)} not stringified"`
  202. }
  203. if (spacer !== '') {
  204. res += `\n${originalIndentation}`
  205. }
  206. stack.pop()
  207. return `[${res}]`
  208. }
  209. let keys = Object.keys(value)
  210. const keyLength = keys.length
  211. if (keyLength === 0) {
  212. return '{}'
  213. }
  214. if (maximumDepth < stack.length + 1) {
  215. return '"[Object]"'
  216. }
  217. let whitespace = ''
  218. let separator = ''
  219. if (spacer !== '') {
  220. indentation += spacer
  221. join = `,\n${indentation}`
  222. whitespace = ' '
  223. }
  224. const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
  225. if (deterministic && !isTypedArrayWithEntries(value)) {
  226. keys = insertSort(keys)
  227. }
  228. stack.push(value)
  229. for (let i = 0; i < maximumPropertiesToStringify; i++) {
  230. const key = keys[i]
  231. const tmp = stringifyFnReplacer(key, value, stack, replacer, spacer, indentation)
  232. if (tmp !== undefined) {
  233. res += `${separator}${strEscape(key)}:${whitespace}${tmp}`
  234. separator = join
  235. }
  236. }
  237. if (keyLength > maximumBreadth) {
  238. const removedKeys = keyLength - maximumBreadth
  239. res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`
  240. separator = join
  241. }
  242. if (spacer !== '' && separator.length > 1) {
  243. res = `\n${indentation}${res}\n${originalIndentation}`
  244. }
  245. stack.pop()
  246. return `{${res}}`
  247. }
  248. case 'number':
  249. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  250. case 'boolean':
  251. return value === true ? 'true' : 'false'
  252. case 'undefined':
  253. return undefined
  254. case 'bigint':
  255. if (bigint) {
  256. return String(value)
  257. }
  258. // fallthrough
  259. default:
  260. return fail ? fail(value) : undefined
  261. }
  262. }
  263. function stringifyArrayReplacer (key, value, stack, replacer, spacer, indentation) {
  264. if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {
  265. value = value.toJSON(key)
  266. }
  267. switch (typeof value) {
  268. case 'string':
  269. return strEscape(value)
  270. case 'object': {
  271. if (value === null) {
  272. return 'null'
  273. }
  274. if (stack.indexOf(value) !== -1) {
  275. return circularValue
  276. }
  277. const originalIndentation = indentation
  278. let res = ''
  279. let join = ','
  280. if (Array.isArray(value)) {
  281. if (value.length === 0) {
  282. return '[]'
  283. }
  284. if (maximumDepth < stack.length + 1) {
  285. return '"[Array]"'
  286. }
  287. stack.push(value)
  288. if (spacer !== '') {
  289. indentation += spacer
  290. res += `\n${indentation}`
  291. join = `,\n${indentation}`
  292. }
  293. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  294. let i = 0
  295. for (; i < maximumValuesToStringify - 1; i++) {
  296. const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation)
  297. res += tmp !== undefined ? tmp : 'null'
  298. res += join
  299. }
  300. const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation)
  301. res += tmp !== undefined ? tmp : 'null'
  302. if (value.length - 1 > maximumBreadth) {
  303. const removedKeys = value.length - maximumBreadth - 1
  304. res += `${join}"... ${getItemCount(removedKeys)} not stringified"`
  305. }
  306. if (spacer !== '') {
  307. res += `\n${originalIndentation}`
  308. }
  309. stack.pop()
  310. return `[${res}]`
  311. }
  312. stack.push(value)
  313. let whitespace = ''
  314. if (spacer !== '') {
  315. indentation += spacer
  316. join = `,\n${indentation}`
  317. whitespace = ' '
  318. }
  319. let separator = ''
  320. for (const key of replacer) {
  321. const tmp = stringifyArrayReplacer(key, value[key], stack, replacer, spacer, indentation)
  322. if (tmp !== undefined) {
  323. res += `${separator}${strEscape(key)}:${whitespace}${tmp}`
  324. separator = join
  325. }
  326. }
  327. if (spacer !== '' && separator.length > 1) {
  328. res = `\n${indentation}${res}\n${originalIndentation}`
  329. }
  330. stack.pop()
  331. return `{${res}}`
  332. }
  333. case 'number':
  334. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  335. case 'boolean':
  336. return value === true ? 'true' : 'false'
  337. case 'undefined':
  338. return undefined
  339. case 'bigint':
  340. if (bigint) {
  341. return String(value)
  342. }
  343. // fallthrough
  344. default:
  345. return fail ? fail(value) : undefined
  346. }
  347. }
  348. function stringifyIndent (key, value, stack, spacer, indentation) {
  349. switch (typeof value) {
  350. case 'string':
  351. return strEscape(value)
  352. case 'object': {
  353. if (value === null) {
  354. return 'null'
  355. }
  356. if (typeof value.toJSON === 'function') {
  357. value = value.toJSON(key)
  358. // Prevent calling `toJSON` again.
  359. if (typeof value !== 'object') {
  360. return stringifyIndent(key, value, stack, spacer, indentation)
  361. }
  362. if (value === null) {
  363. return 'null'
  364. }
  365. }
  366. if (stack.indexOf(value) !== -1) {
  367. return circularValue
  368. }
  369. const originalIndentation = indentation
  370. if (Array.isArray(value)) {
  371. if (value.length === 0) {
  372. return '[]'
  373. }
  374. if (maximumDepth < stack.length + 1) {
  375. return '"[Array]"'
  376. }
  377. stack.push(value)
  378. indentation += spacer
  379. let res = `\n${indentation}`
  380. const join = `,\n${indentation}`
  381. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  382. let i = 0
  383. for (; i < maximumValuesToStringify - 1; i++) {
  384. const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation)
  385. res += tmp !== undefined ? tmp : 'null'
  386. res += join
  387. }
  388. const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation)
  389. res += tmp !== undefined ? tmp : 'null'
  390. if (value.length - 1 > maximumBreadth) {
  391. const removedKeys = value.length - maximumBreadth - 1
  392. res += `${join}"... ${getItemCount(removedKeys)} not stringified"`
  393. }
  394. res += `\n${originalIndentation}`
  395. stack.pop()
  396. return `[${res}]`
  397. }
  398. let keys = Object.keys(value)
  399. const keyLength = keys.length
  400. if (keyLength === 0) {
  401. return '{}'
  402. }
  403. if (maximumDepth < stack.length + 1) {
  404. return '"[Object]"'
  405. }
  406. indentation += spacer
  407. const join = `,\n${indentation}`
  408. let res = ''
  409. let separator = ''
  410. let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
  411. if (isTypedArrayWithEntries(value)) {
  412. res += stringifyTypedArray(value, join, maximumBreadth)
  413. keys = keys.slice(value.length)
  414. maximumPropertiesToStringify -= value.length
  415. separator = join
  416. }
  417. if (deterministic) {
  418. keys = insertSort(keys)
  419. }
  420. stack.push(value)
  421. for (let i = 0; i < maximumPropertiesToStringify; i++) {
  422. const key = keys[i]
  423. const tmp = stringifyIndent(key, value[key], stack, spacer, indentation)
  424. if (tmp !== undefined) {
  425. res += `${separator}${strEscape(key)}: ${tmp}`
  426. separator = join
  427. }
  428. }
  429. if (keyLength > maximumBreadth) {
  430. const removedKeys = keyLength - maximumBreadth
  431. res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`
  432. separator = join
  433. }
  434. if (separator !== '') {
  435. res = `\n${indentation}${res}\n${originalIndentation}`
  436. }
  437. stack.pop()
  438. return `{${res}}`
  439. }
  440. case 'number':
  441. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  442. case 'boolean':
  443. return value === true ? 'true' : 'false'
  444. case 'undefined':
  445. return undefined
  446. case 'bigint':
  447. if (bigint) {
  448. return String(value)
  449. }
  450. // fallthrough
  451. default:
  452. return fail ? fail(value) : undefined
  453. }
  454. }
  455. function stringifySimple (key, value, stack) {
  456. switch (typeof value) {
  457. case 'string':
  458. return strEscape(value)
  459. case 'object': {
  460. if (value === null) {
  461. return 'null'
  462. }
  463. if (typeof value.toJSON === 'function') {
  464. value = value.toJSON(key)
  465. // Prevent calling `toJSON` again
  466. if (typeof value !== 'object') {
  467. return stringifySimple(key, value, stack)
  468. }
  469. if (value === null) {
  470. return 'null'
  471. }
  472. }
  473. if (stack.indexOf(value) !== -1) {
  474. return circularValue
  475. }
  476. let res = ''
  477. if (Array.isArray(value)) {
  478. if (value.length === 0) {
  479. return '[]'
  480. }
  481. if (maximumDepth < stack.length + 1) {
  482. return '"[Array]"'
  483. }
  484. stack.push(value)
  485. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  486. let i = 0
  487. for (; i < maximumValuesToStringify - 1; i++) {
  488. const tmp = stringifySimple(String(i), value[i], stack)
  489. res += tmp !== undefined ? tmp : 'null'
  490. res += ','
  491. }
  492. const tmp = stringifySimple(String(i), value[i], stack)
  493. res += tmp !== undefined ? tmp : 'null'
  494. if (value.length - 1 > maximumBreadth) {
  495. const removedKeys = value.length - maximumBreadth - 1
  496. res += `,"... ${getItemCount(removedKeys)} not stringified"`
  497. }
  498. stack.pop()
  499. return `[${res}]`
  500. }
  501. let keys = Object.keys(value)
  502. const keyLength = keys.length
  503. if (keyLength === 0) {
  504. return '{}'
  505. }
  506. if (maximumDepth < stack.length + 1) {
  507. return '"[Object]"'
  508. }
  509. let separator = ''
  510. let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
  511. if (isTypedArrayWithEntries(value)) {
  512. res += stringifyTypedArray(value, ',', maximumBreadth)
  513. keys = keys.slice(value.length)
  514. maximumPropertiesToStringify -= value.length
  515. separator = ','
  516. }
  517. if (deterministic) {
  518. keys = insertSort(keys)
  519. }
  520. stack.push(value)
  521. for (let i = 0; i < maximumPropertiesToStringify; i++) {
  522. const key = keys[i]
  523. const tmp = stringifySimple(key, value[key], stack)
  524. if (tmp !== undefined) {
  525. res += `${separator}${strEscape(key)}:${tmp}`
  526. separator = ','
  527. }
  528. }
  529. if (keyLength > maximumBreadth) {
  530. const removedKeys = keyLength - maximumBreadth
  531. res += `${separator}"...":"${getItemCount(removedKeys)} not stringified"`
  532. }
  533. stack.pop()
  534. return `{${res}}`
  535. }
  536. case 'number':
  537. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  538. case 'boolean':
  539. return value === true ? 'true' : 'false'
  540. case 'undefined':
  541. return undefined
  542. case 'bigint':
  543. if (bigint) {
  544. return String(value)
  545. }
  546. // fallthrough
  547. default:
  548. return fail ? fail(value) : undefined
  549. }
  550. }
  551. function stringify (value, replacer, space) {
  552. if (arguments.length > 1) {
  553. let spacer = ''
  554. if (typeof space === 'number') {
  555. spacer = ' '.repeat(Math.min(space, 10))
  556. } else if (typeof space === 'string') {
  557. spacer = space.slice(0, 10)
  558. }
  559. if (replacer != null) {
  560. if (typeof replacer === 'function') {
  561. return stringifyFnReplacer('', { '': value }, [], replacer, spacer, '')
  562. }
  563. if (Array.isArray(replacer)) {
  564. return stringifyArrayReplacer('', value, [], getUniqueReplacerSet(replacer), spacer, '')
  565. }
  566. }
  567. if (spacer.length !== 0) {
  568. return stringifyIndent('', value, [], spacer, '')
  569. }
  570. }
  571. return stringifySimple('', value, [])
  572. }
  573. return stringify
  574. }