index.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  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]/
  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 sort (array, comparator) {
  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 || comparator) {
  30. return array.sort(comparator)
  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 getDeterministicOption (options) {
  87. let value
  88. if (hasOwnProperty.call(options, 'deterministic')) {
  89. value = options.deterministic
  90. if (typeof value !== 'boolean' && typeof value !== 'function') {
  91. throw new TypeError('The "deterministic" argument must be of type boolean or comparator function')
  92. }
  93. }
  94. return value === undefined ? true : value
  95. }
  96. function getBooleanOption (options, key) {
  97. let value
  98. if (hasOwnProperty.call(options, key)) {
  99. value = options[key]
  100. if (typeof value !== 'boolean') {
  101. throw new TypeError(`The "${key}" argument must be of type boolean`)
  102. }
  103. }
  104. return value === undefined ? true : value
  105. }
  106. function getPositiveIntegerOption (options, key) {
  107. let value
  108. if (hasOwnProperty.call(options, key)) {
  109. value = options[key]
  110. if (typeof value !== 'number') {
  111. throw new TypeError(`The "${key}" argument must be of type number`)
  112. }
  113. if (!Number.isInteger(value)) {
  114. throw new TypeError(`The "${key}" argument must be an integer`)
  115. }
  116. if (value < 1) {
  117. throw new RangeError(`The "${key}" argument must be >= 1`)
  118. }
  119. }
  120. return value === undefined ? Infinity : value
  121. }
  122. function getItemCount (number) {
  123. if (number === 1) {
  124. return '1 item'
  125. }
  126. return `${number} items`
  127. }
  128. function getUniqueReplacerSet (replacerArray) {
  129. const replacerSet = new Set()
  130. for (const value of replacerArray) {
  131. if (typeof value === 'string' || typeof value === 'number') {
  132. replacerSet.add(String(value))
  133. }
  134. }
  135. return replacerSet
  136. }
  137. function getStrictOption (options) {
  138. if (hasOwnProperty.call(options, 'strict')) {
  139. const value = options.strict
  140. if (typeof value !== 'boolean') {
  141. throw new TypeError('The "strict" argument must be of type boolean')
  142. }
  143. if (value) {
  144. return (value) => {
  145. let message = `Object can not safely be stringified. Received type ${typeof value}`
  146. if (typeof value !== 'function') message += ` (${value.toString()})`
  147. throw new Error(message)
  148. }
  149. }
  150. }
  151. }
  152. function configure (options) {
  153. options = { ...options }
  154. const fail = getStrictOption(options)
  155. if (fail) {
  156. if (options.bigint === undefined) {
  157. options.bigint = false
  158. }
  159. if (!('circularValue' in options)) {
  160. options.circularValue = Error
  161. }
  162. }
  163. const circularValue = getCircularValueOption(options)
  164. const bigint = getBooleanOption(options, 'bigint')
  165. const deterministic = getDeterministicOption(options)
  166. const comparator = typeof deterministic === 'function' ? deterministic : undefined
  167. const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth')
  168. const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth')
  169. function stringifyFnReplacer (key, parent, stack, replacer, spacer, indentation) {
  170. let value = parent[key]
  171. if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {
  172. value = value.toJSON(key)
  173. }
  174. value = replacer.call(parent, key, value)
  175. switch (typeof value) {
  176. case 'string':
  177. return strEscape(value)
  178. case 'object': {
  179. if (value === null) {
  180. return 'null'
  181. }
  182. if (stack.indexOf(value) !== -1) {
  183. return circularValue
  184. }
  185. let res = ''
  186. let join = ','
  187. const originalIndentation = indentation
  188. if (Array.isArray(value)) {
  189. if (value.length === 0) {
  190. return '[]'
  191. }
  192. if (maximumDepth < stack.length + 1) {
  193. return '"[Array]"'
  194. }
  195. stack.push(value)
  196. if (spacer !== '') {
  197. indentation += spacer
  198. res += `\n${indentation}`
  199. join = `,\n${indentation}`
  200. }
  201. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  202. let i = 0
  203. for (; i < maximumValuesToStringify - 1; i++) {
  204. const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation)
  205. res += tmp !== undefined ? tmp : 'null'
  206. res += join
  207. }
  208. const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation)
  209. res += tmp !== undefined ? tmp : 'null'
  210. if (value.length - 1 > maximumBreadth) {
  211. const removedKeys = value.length - maximumBreadth - 1
  212. res += `${join}"... ${getItemCount(removedKeys)} not stringified"`
  213. }
  214. if (spacer !== '') {
  215. res += `\n${originalIndentation}`
  216. }
  217. stack.pop()
  218. return `[${res}]`
  219. }
  220. let keys = Object.keys(value)
  221. const keyLength = keys.length
  222. if (keyLength === 0) {
  223. return '{}'
  224. }
  225. if (maximumDepth < stack.length + 1) {
  226. return '"[Object]"'
  227. }
  228. let whitespace = ''
  229. let separator = ''
  230. if (spacer !== '') {
  231. indentation += spacer
  232. join = `,\n${indentation}`
  233. whitespace = ' '
  234. }
  235. const maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
  236. if (deterministic && !isTypedArrayWithEntries(value)) {
  237. keys = sort(keys, comparator)
  238. }
  239. stack.push(value)
  240. for (let i = 0; i < maximumPropertiesToStringify; i++) {
  241. const key = keys[i]
  242. const tmp = stringifyFnReplacer(key, value, stack, replacer, spacer, indentation)
  243. if (tmp !== undefined) {
  244. res += `${separator}${strEscape(key)}:${whitespace}${tmp}`
  245. separator = join
  246. }
  247. }
  248. if (keyLength > maximumBreadth) {
  249. const removedKeys = keyLength - maximumBreadth
  250. res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`
  251. separator = join
  252. }
  253. if (spacer !== '' && separator.length > 1) {
  254. res = `\n${indentation}${res}\n${originalIndentation}`
  255. }
  256. stack.pop()
  257. return `{${res}}`
  258. }
  259. case 'number':
  260. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  261. case 'boolean':
  262. return value === true ? 'true' : 'false'
  263. case 'undefined':
  264. return undefined
  265. case 'bigint':
  266. if (bigint) {
  267. return String(value)
  268. }
  269. // fallthrough
  270. default:
  271. return fail ? fail(value) : undefined
  272. }
  273. }
  274. function stringifyArrayReplacer (key, value, stack, replacer, spacer, indentation) {
  275. if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') {
  276. value = value.toJSON(key)
  277. }
  278. switch (typeof value) {
  279. case 'string':
  280. return strEscape(value)
  281. case 'object': {
  282. if (value === null) {
  283. return 'null'
  284. }
  285. if (stack.indexOf(value) !== -1) {
  286. return circularValue
  287. }
  288. const originalIndentation = indentation
  289. let res = ''
  290. let join = ','
  291. if (Array.isArray(value)) {
  292. if (value.length === 0) {
  293. return '[]'
  294. }
  295. if (maximumDepth < stack.length + 1) {
  296. return '"[Array]"'
  297. }
  298. stack.push(value)
  299. if (spacer !== '') {
  300. indentation += spacer
  301. res += `\n${indentation}`
  302. join = `,\n${indentation}`
  303. }
  304. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  305. let i = 0
  306. for (; i < maximumValuesToStringify - 1; i++) {
  307. const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation)
  308. res += tmp !== undefined ? tmp : 'null'
  309. res += join
  310. }
  311. const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation)
  312. res += tmp !== undefined ? tmp : 'null'
  313. if (value.length - 1 > maximumBreadth) {
  314. const removedKeys = value.length - maximumBreadth - 1
  315. res += `${join}"... ${getItemCount(removedKeys)} not stringified"`
  316. }
  317. if (spacer !== '') {
  318. res += `\n${originalIndentation}`
  319. }
  320. stack.pop()
  321. return `[${res}]`
  322. }
  323. stack.push(value)
  324. let whitespace = ''
  325. if (spacer !== '') {
  326. indentation += spacer
  327. join = `,\n${indentation}`
  328. whitespace = ' '
  329. }
  330. let separator = ''
  331. for (const key of replacer) {
  332. const tmp = stringifyArrayReplacer(key, value[key], stack, replacer, spacer, indentation)
  333. if (tmp !== undefined) {
  334. res += `${separator}${strEscape(key)}:${whitespace}${tmp}`
  335. separator = join
  336. }
  337. }
  338. if (spacer !== '' && separator.length > 1) {
  339. res = `\n${indentation}${res}\n${originalIndentation}`
  340. }
  341. stack.pop()
  342. return `{${res}}`
  343. }
  344. case 'number':
  345. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  346. case 'boolean':
  347. return value === true ? 'true' : 'false'
  348. case 'undefined':
  349. return undefined
  350. case 'bigint':
  351. if (bigint) {
  352. return String(value)
  353. }
  354. // fallthrough
  355. default:
  356. return fail ? fail(value) : undefined
  357. }
  358. }
  359. function stringifyIndent (key, value, stack, spacer, indentation) {
  360. switch (typeof value) {
  361. case 'string':
  362. return strEscape(value)
  363. case 'object': {
  364. if (value === null) {
  365. return 'null'
  366. }
  367. if (typeof value.toJSON === 'function') {
  368. value = value.toJSON(key)
  369. // Prevent calling `toJSON` again.
  370. if (typeof value !== 'object') {
  371. return stringifyIndent(key, value, stack, spacer, indentation)
  372. }
  373. if (value === null) {
  374. return 'null'
  375. }
  376. }
  377. if (stack.indexOf(value) !== -1) {
  378. return circularValue
  379. }
  380. const originalIndentation = indentation
  381. if (Array.isArray(value)) {
  382. if (value.length === 0) {
  383. return '[]'
  384. }
  385. if (maximumDepth < stack.length + 1) {
  386. return '"[Array]"'
  387. }
  388. stack.push(value)
  389. indentation += spacer
  390. let res = `\n${indentation}`
  391. const join = `,\n${indentation}`
  392. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  393. let i = 0
  394. for (; i < maximumValuesToStringify - 1; i++) {
  395. const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation)
  396. res += tmp !== undefined ? tmp : 'null'
  397. res += join
  398. }
  399. const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation)
  400. res += tmp !== undefined ? tmp : 'null'
  401. if (value.length - 1 > maximumBreadth) {
  402. const removedKeys = value.length - maximumBreadth - 1
  403. res += `${join}"... ${getItemCount(removedKeys)} not stringified"`
  404. }
  405. res += `\n${originalIndentation}`
  406. stack.pop()
  407. return `[${res}]`
  408. }
  409. let keys = Object.keys(value)
  410. const keyLength = keys.length
  411. if (keyLength === 0) {
  412. return '{}'
  413. }
  414. if (maximumDepth < stack.length + 1) {
  415. return '"[Object]"'
  416. }
  417. indentation += spacer
  418. const join = `,\n${indentation}`
  419. let res = ''
  420. let separator = ''
  421. let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
  422. if (isTypedArrayWithEntries(value)) {
  423. res += stringifyTypedArray(value, join, maximumBreadth)
  424. keys = keys.slice(value.length)
  425. maximumPropertiesToStringify -= value.length
  426. separator = join
  427. }
  428. if (deterministic) {
  429. keys = sort(keys, comparator)
  430. }
  431. stack.push(value)
  432. for (let i = 0; i < maximumPropertiesToStringify; i++) {
  433. const key = keys[i]
  434. const tmp = stringifyIndent(key, value[key], stack, spacer, indentation)
  435. if (tmp !== undefined) {
  436. res += `${separator}${strEscape(key)}: ${tmp}`
  437. separator = join
  438. }
  439. }
  440. if (keyLength > maximumBreadth) {
  441. const removedKeys = keyLength - maximumBreadth
  442. res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`
  443. separator = join
  444. }
  445. if (separator !== '') {
  446. res = `\n${indentation}${res}\n${originalIndentation}`
  447. }
  448. stack.pop()
  449. return `{${res}}`
  450. }
  451. case 'number':
  452. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  453. case 'boolean':
  454. return value === true ? 'true' : 'false'
  455. case 'undefined':
  456. return undefined
  457. case 'bigint':
  458. if (bigint) {
  459. return String(value)
  460. }
  461. // fallthrough
  462. default:
  463. return fail ? fail(value) : undefined
  464. }
  465. }
  466. function stringifySimple (key, value, stack) {
  467. switch (typeof value) {
  468. case 'string':
  469. return strEscape(value)
  470. case 'object': {
  471. if (value === null) {
  472. return 'null'
  473. }
  474. if (typeof value.toJSON === 'function') {
  475. value = value.toJSON(key)
  476. // Prevent calling `toJSON` again
  477. if (typeof value !== 'object') {
  478. return stringifySimple(key, value, stack)
  479. }
  480. if (value === null) {
  481. return 'null'
  482. }
  483. }
  484. if (stack.indexOf(value) !== -1) {
  485. return circularValue
  486. }
  487. let res = ''
  488. const hasLength = value.length !== undefined
  489. if (hasLength && Array.isArray(value)) {
  490. if (value.length === 0) {
  491. return '[]'
  492. }
  493. if (maximumDepth < stack.length + 1) {
  494. return '"[Array]"'
  495. }
  496. stack.push(value)
  497. const maximumValuesToStringify = Math.min(value.length, maximumBreadth)
  498. let i = 0
  499. for (; i < maximumValuesToStringify - 1; i++) {
  500. const tmp = stringifySimple(String(i), value[i], stack)
  501. res += tmp !== undefined ? tmp : 'null'
  502. res += ','
  503. }
  504. const tmp = stringifySimple(String(i), value[i], stack)
  505. res += tmp !== undefined ? tmp : 'null'
  506. if (value.length - 1 > maximumBreadth) {
  507. const removedKeys = value.length - maximumBreadth - 1
  508. res += `,"... ${getItemCount(removedKeys)} not stringified"`
  509. }
  510. stack.pop()
  511. return `[${res}]`
  512. }
  513. let keys = Object.keys(value)
  514. const keyLength = keys.length
  515. if (keyLength === 0) {
  516. return '{}'
  517. }
  518. if (maximumDepth < stack.length + 1) {
  519. return '"[Object]"'
  520. }
  521. let separator = ''
  522. let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth)
  523. if (hasLength && isTypedArrayWithEntries(value)) {
  524. res += stringifyTypedArray(value, ',', maximumBreadth)
  525. keys = keys.slice(value.length)
  526. maximumPropertiesToStringify -= value.length
  527. separator = ','
  528. }
  529. if (deterministic) {
  530. keys = sort(keys, comparator)
  531. }
  532. stack.push(value)
  533. for (let i = 0; i < maximumPropertiesToStringify; i++) {
  534. const key = keys[i]
  535. const tmp = stringifySimple(key, value[key], stack)
  536. if (tmp !== undefined) {
  537. res += `${separator}${strEscape(key)}:${tmp}`
  538. separator = ','
  539. }
  540. }
  541. if (keyLength > maximumBreadth) {
  542. const removedKeys = keyLength - maximumBreadth
  543. res += `${separator}"...":"${getItemCount(removedKeys)} not stringified"`
  544. }
  545. stack.pop()
  546. return `{${res}}`
  547. }
  548. case 'number':
  549. return isFinite(value) ? String(value) : fail ? fail(value) : 'null'
  550. case 'boolean':
  551. return value === true ? 'true' : 'false'
  552. case 'undefined':
  553. return undefined
  554. case 'bigint':
  555. if (bigint) {
  556. return String(value)
  557. }
  558. // fallthrough
  559. default:
  560. return fail ? fail(value) : undefined
  561. }
  562. }
  563. function stringify (value, replacer, space) {
  564. if (arguments.length > 1) {
  565. let spacer = ''
  566. if (typeof space === 'number') {
  567. spacer = ' '.repeat(Math.min(space, 10))
  568. } else if (typeof space === 'string') {
  569. spacer = space.slice(0, 10)
  570. }
  571. if (replacer != null) {
  572. if (typeof replacer === 'function') {
  573. return stringifyFnReplacer('', { '': value }, [], replacer, spacer, '')
  574. }
  575. if (Array.isArray(replacer)) {
  576. return stringifyArrayReplacer('', value, [], getUniqueReplacerSet(replacer), spacer, '')
  577. }
  578. }
  579. if (spacer.length !== 0) {
  580. return stringifyIndent('', value, [], spacer, '')
  581. }
  582. }
  583. return stringifySimple('', value, [])
  584. }
  585. return stringify
  586. }