pax_global_header00006660000000000000000000000064141525426000014511gustar00rootroot0000000000000052 comment=78891ff37c6e8936118b8fa47ed59dd761c3208a safe-stable-stringify-2.3.1/000077500000000000000000000000001415254260000157165ustar00rootroot00000000000000safe-stable-stringify-2.3.1/.github/000077500000000000000000000000001415254260000172565ustar00rootroot00000000000000safe-stable-stringify-2.3.1/.github/workflows/000077500000000000000000000000001415254260000213135ustar00rootroot00000000000000safe-stable-stringify-2.3.1/.github/workflows/test.yml000066400000000000000000000010551415254260000230160ustar00rootroot00000000000000name: Tests on: push: branches: - main pull_request: branches: - main jobs: test: strategy: matrix: os: - ubuntu-latest node_version: - 10 - 12 - 14 - 16 name: Running tests with Node ${{ matrix.node_version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node_version }} - run: | npm install npm run test safe-stable-stringify-2.3.1/.gitignore000066400000000000000000000001371415254260000177070ustar00rootroot00000000000000.DS_Store node_modules/ npm-debug.log yarn.lock coverage .nyc_output .vscode package-lock.json safe-stable-stringify-2.3.1/.npmignore000066400000000000000000000001401415254260000177100ustar00rootroot00000000000000**/* !index.d.ts !index.js !LICENSE !package.json !esm/* !CHANGELOG.md !readme.md !tsconfig.jsonsafe-stable-stringify-2.3.1/CHANGELOG.md000066400000000000000000000054261415254260000175360ustar00rootroot00000000000000# Changelog ## v2.3.1 - Fix `invalid regexp group` error in browsers or environments that do not support the negative lookbehind regular expression assertion. ## v2.3.0 - Accept the `Error` constructor as `circularValue` option to throw on circular references as the regular JSON.stringify would: ```js import { configure } from 'safe-stable-stringify' const object = {} object.circular = object; const stringify = configure({ circularValue: TypeError }) stringify(object) // TypeError: Converting circular structure to JSON ``` - Fixed escaping wrong surrogates. Only lone surrogates are now escaped. ## v2.2.0 - Reduce module size by removing the test and benchmark files from the published package - Accept `undefined` as `circularValue` option to remove circular properties from the serialized output: ```js import { configure } from 'safe-stable-stringify' const object = { array: [] } object.circular = object; object.array.push(object) configure({ circularValue: undefined })(object) // '{"array":[null]}' ``` ## v2.1.0 - Added `maximumBreadth` option to limit stringification at a specific object or array "width" (number of properties / values) - Added `maximumDepth` option to limit stringification at a specific nesting depth - Implemented the [well formed stringify proposal](https://github.com/tc39/proposal-well-formed-stringify) that is now part of the spec - Fixed maximum spacer length (10) - Fixed TypeScript definition - Fixed duplicated array replacer values serialized more than once ## v2.0.0 - __[BREAKING]__ Convert BigInt to number by default instead of ignoring these values If you wish to ignore these values similar to earlier versions, just use the new `bigint` option and set it to `false`. - __[BREAKING]__ Support ESM - __[BREAKING]__ Requires ES6 - Optional BigInt support - Deterministic behavior is now optional - The value to indicate a circular structure is now adjustable - Significantly faster TypedArray stringification - Smaller Codebase - Removed stateful indentation to guarantee side-effect freeness ## v1.1.1 - Fixed an indentation issue in combination with empty arrays and objects - Updated dev dependencies ## v1.1.0 - Add support for IE11 (https://github.com/BridgeAR/safe-stable-stringify/commit/917b6128de135a950ec178d66d86b4d772c7656d) - Fix issue with undefined values (https://github.com/BridgeAR/safe-stable-stringify/commit/4196f87, https://github.com/BridgeAR/safe-stable-stringify/commit/4eab558) - Fix typescript definition (https://github.com/BridgeAR/safe-stable-stringify/commit/7a87478) - Improve code coverage (https://github.com/BridgeAR/safe-stable-stringify/commit/ed8cadc, https://github.com/BridgeAR/safe-stable-stringify/commit/b58c494) - Update dev dependencies (https://github.com/BridgeAR/safe-stable-stringify/commit/b857ea8) - Improve docssafe-stable-stringify-2.3.1/LICENSE000066400000000000000000000020671415254260000167300ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) Ruben Bridgewater Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. safe-stable-stringify-2.3.1/benchmark.js000066400000000000000000000062411415254260000202110ustar00rootroot00000000000000'use strict' const Benchmark = require('benchmark') const suite = new Benchmark.Suite() const stringify = require('.').configure({ deterministic: true }) // eslint-disable-next-line const array = Array({ length: 10 }, (_, i) => i) const obj = { array } const circ = JSON.parse(JSON.stringify(obj)) circ.o = { obj: circ, array } const deep = require('./package.json') deep.deep = JSON.parse(JSON.stringify(deep)) deep.deep.deep = JSON.parse(JSON.stringify(deep)) deep.deep.deep.deep = JSON.parse(JSON.stringify(deep)) deep.array = array const deepCirc = JSON.parse(JSON.stringify(deep)) deepCirc.deep.deep.deep.circ = deepCirc deepCirc.deep.deep.circ = deepCirc deepCirc.deep.circ = deepCirc deepCirc.array = array // One arg "simple" suite.add('simple: simple object', function () { stringify(obj) }) suite.add('simple: circular ', function () { stringify(circ) }) suite.add('simple: deep ', function () { stringify(deep) }) suite.add('simple: deep circular', function () { stringify(deepCirc) }) // Two args "replacer" suite.add('\nreplacer: simple object', function () { stringify(obj, (_, v) => v) }) suite.add('replacer: circular ', function () { stringify(circ, (_, v) => v) }) suite.add('replacer: deep ', function () { stringify(deep, (_, v) => v) }) suite.add('replacer: deep circular', function () { stringify(deepCirc, (_, v) => v) }) // Two args "array" suite.add('\narray: simple object', function () { stringify(obj, ['array']) }) suite.add('array: circular ', function () { stringify(circ, ['array']) }) suite.add('array: deep ', function () { stringify(deep, ['array']) }) suite.add('array: deep circular', function () { stringify(deepCirc, ['array']) }) // Three args "full replacer" suite.add('\nfull replacer: simple object', function () { stringify(obj, (_, v) => v, 2) }) suite.add('full replacer: circular ', function () { stringify(circ, (_, v) => v, 2) }) suite.add('full replacer: deep ', function () { stringify(deep, (_, v) => v, 2) }) suite.add('full replacer: deep circular', function () { stringify(deepCirc, (_, v) => v, 2) }) // Three args "full array" suite.add('\nfull array: simple object', function () { stringify(obj, ['array'], 2) }) suite.add('full array: circular ', function () { stringify(circ, ['array'], 2) }) suite.add('full array: deep ', function () { stringify(deep, ['array'], 2) }) suite.add('full array: deep circular', function () { stringify(deepCirc, ['array'], 2) }) // Three args "indentation only" suite.add('\nindentation: simple object', function () { stringify(obj, null, 2) }) suite.add('indentation: circular ', function () { stringify(circ, null, 2) }) suite.add('indentation: deep ', function () { stringify(deep, null, 2) }) suite.add('indentation: deep circular', function () { stringify(deepCirc, null, 2) }) // add listeners suite.on('cycle', function (event) { console.log(String(event.target)) }) suite.on('complete', function () { console.log('\nBenchmark done') // console.log('\nFastest is ' + this.filter('fastest').map('name')) }) suite.run({ delay: 1, minSamples: 150 }) safe-stable-stringify-2.3.1/compare.js000066400000000000000000000020211415254260000176750ustar00rootroot00000000000000'use strict' const Benchmark = require('benchmark') const suite = new Benchmark.Suite() const testData = require('./test.json') const stringifyPackages = { // 'JSON.stringify': JSON.stringify, 'fastest-stable-stringify': true, 'fast-json-stable-stringify': true, 'json-stable-stringify': true, 'fast-stable-stringify': true, 'faster-stable-stringify': true, 'json-stringify-deterministic': true, 'fast-safe-stringify': 'stable', this: require('.') } for (const name in stringifyPackages) { let fn if (typeof stringifyPackages[name] === 'function') { fn = stringifyPackages[name] } else if (typeof stringifyPackages[name] === 'string') { fn = require(name)[stringifyPackages[name]] } else { fn = require(name) } suite.add(name, function () { fn(testData) }) } suite .on('cycle', (event) => console.log(String(event.target))) .on('complete', function () { console.log('\nThe fastest is ' + this.filter('fastest').map('name')) }) .run({ async: true, delay: 5, minSamples: 150 }) safe-stable-stringify-2.3.1/esm/000077500000000000000000000000001415254260000165025ustar00rootroot00000000000000safe-stable-stringify-2.3.1/esm/package.json000066400000000000000000000000571415254260000207720ustar00rootroot00000000000000{ "type": "module", "main": "wrapper.js" } safe-stable-stringify-2.3.1/esm/wrapper.js000066400000000000000000000002171415254260000205200ustar00rootroot00000000000000import cjsModule from '../index.js' export const configure = cjsModule.configure export const stringify = cjsModule export default cjsModule safe-stable-stringify-2.3.1/index.d.ts000066400000000000000000000012071415254260000176170ustar00rootroot00000000000000export function stringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string; export function stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string; export interface StringifyOptions { bigint?: boolean, circularValue?: string | null | TypeErrorConstructor | ErrorConstructor, deterministic?: boolean, maximumBreadth?: number, maximumDepth?: number, } export namespace stringify { export function configure(options: StringifyOptions): typeof stringify; } export function configure(options: StringifyOptions): typeof stringify; export default stringify; safe-stable-stringify-2.3.1/index.js000066400000000000000000000471721415254260000173760ustar00rootroot00000000000000'use strict' 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 const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/ // eslint-disable-next-line const strEscapeSequencesReplacer = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/g // Escaped special characters. Use empty strings to fill up unused entries. const meta = [ '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', '\\n', '\\u000b', '\\f', '\\r', '\\u000e', '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', '\\u001e', '\\u001f', '', '', '\\"', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '\\\\' ] function escapeFn (str) { if (str.length === 2) { const charCode = str.charCodeAt(1) return `${str[0]}\\u${charCode.toString(16)}` } const charCode = str.charCodeAt(0) return meta.length > charCode ? meta[charCode] : `\\u${charCode.toString(16)}` } // 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 } if (str.length > 100) { return str.replace(strEscapeSequencesReplacer, escapeFn) } let result = '' let last = 0 for (let i = 0; i < str.length; i++) { const point = str.charCodeAt(i) if (point === 34 || point === 92 || point < 32) { result += `${str.slice(last, i)}${meta[point]}` last = i + 1 } else if (point >= 0xd800 && point <= 0xdfff) { if (point <= 0xdbff && i + 1 < str.length) { const point = str.charCodeAt(i + 1) if (point >= 0xdc00 && point <= 0xdfff) { i++ continue } } result += `${str.slice(last, i)}${`\\u${point.toString(16)}`}` last = i + 1 } } result += str.slice(last) return result } 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 Uint8Array() ) ), 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 (options && Object.prototype.hasOwnProperty.call(options, 'circularValue')) { var 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) { if (options && Object.prototype.hasOwnProperty.call(options, key)) { var 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) { if (options && Object.prototype.hasOwnProperty.call(options, key)) { var 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') { replacerSet.add(value) } else if (typeof value === 'number') { replacerSet.add(String(value)) } } return replacerSet } function configure (options) { 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(i, value, stack, replacer, spacer, indentation) res += tmp !== undefined ? tmp : 'null' res += join } const tmp = stringifyFnReplacer(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 = ' ' } 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 = 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) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'bigint': return bigint ? String(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(i, value[i], stack, replacer, spacer, indentation) res += tmp !== undefined ? tmp : 'null' res += join } const tmp = stringifyArrayReplacer(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}]` } if (replacer.size === 0) { return '{}' } 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) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'bigint': return bigint ? String(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(i, value[i], stack, spacer, indentation) res += tmp !== undefined ? tmp : 'null' res += join } const tmp = stringifyIndent(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) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'bigint': return bigint ? String(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(i, value[i], stack) res += tmp !== undefined ? tmp : 'null' res += ',' } const tmp = stringifySimple(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) : 'null' case 'boolean': return value === true ? 'true' : 'false' case 'bigint': return bigint ? String(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 } safe-stable-stringify-2.3.1/package.json000066400000000000000000000032631415254260000202100ustar00rootroot00000000000000{ "name": "safe-stable-stringify", "version": "2.3.1", "description": "Deterministic and safely JSON.stringify to quickly serialize JavaScript objects", "exports": { "require": "./index.js", "import": "./esm/wrapper.js" }, "keywords": [ "stable", "stringify", "JSON", "JSON.stringify", "safe", "serialize", "deterministic", "circular", "object", "predicable", "repeatable", "fast", "bigint" ], "main": "index.js", "scripts": { "test": "standard && tap test.js", "tap": "tap test.js", "tap:only": "tap test.js --watch --only", "benchmark": "node benchmark.js", "compare": "node compare.js", "lint": "standard --fix", "tsc": "tsc --project tsconfig.json" }, "engines": { "node": ">=10" }, "author": "Ruben Bridgewater", "license": "MIT", "typings": "index.d.ts", "devDependencies": { "@types/json-stable-stringify": "^1.0.32", "@types/node": "^16.11.1", "benchmark": "^2.1.4", "clone": "^2.1.2", "fast-json-stable-stringify": "^2.1.0", "fast-safe-stringify": "^2.0.7", "fast-stable-stringify": "^1.0.0", "faster-stable-stringify": "^1.0.0", "fastest-stable-stringify": "^2.0.2", "json-stable-stringify": "^1.0.1", "json-stringify-deterministic": "^1.0.1", "json-stringify-safe": "^5.0.1", "standard": "^15.0.0", "tap": "^15.0.9", "typescript": "^4.4.3" }, "repository": { "type": "git", "url": "git+https://github.com/BridgeAR/safe-stable-stringify.git" }, "bugs": { "url": "https://github.com/BridgeAR/safe-stable-stringify/issues" }, "homepage": "https://github.com/BridgeAR/safe-stable-stringify#readme" } safe-stable-stringify-2.3.1/readme.md000066400000000000000000000141421415254260000174770ustar00rootroot00000000000000# safe-stable-stringify Safe, deterministic and fast serialization alternative to [JSON.stringify][]. Zero dependencies. ESM and CJS. 100% coverage. Gracefully handles circular structures and bigint instead of throwing. Optional custom circular values and deterministic behavior. ## stringify(value[, replacer[, space]]) The same as [JSON.stringify][]. * `value` {any} * `replacer` {string[]|function|null} * `space` {number|string} * Returns: {string} ```js const stringify = require('safe-stable-stringify') const bigint = { a: 0, c: 2n, b: 1 } stringify(bigint) // '{"a":0,"b":1,"c":2}' JSON.stringify(bigint) // TypeError: Do not know how to serialize a BigInt const circular = { b: 1, a: 0 } circular.circular = circular stringify(circular) // '{"a":0,"b":1,"circular":"[Circular]"}' JSON.stringify(circular) // TypeError: Converting circular structure to JSON stringify(circular, ['a', 'b'], 2) // { // "a": 0, // "b": 1 // } ``` ## stringify.configure(options) * `bigint` {boolean} If `true`, bigint values are converted to a number. Otherwise they are ignored. **Default:** `true`. * `circularValue` {string|null|undefined|ErrorConstructor} Defines the value for circular references. Set to `undefined`, circular properties are not serialized (array entries are replaced with `null`). Set to `Error`, to throw on circular references. **Default:** `[Circular]`. * `deterministic` {boolean} If `true`, guarantee a deterministic key order instead of relying on the insertion order. **Default:** `true`. * `maximumBreadth` {number} Maximum number of entries to serialize per object (at least one). The serialized output contains information about how many entries have not been serialized. Ignored properties are counted as well (e.g., properties with symbol values). Using the array replacer overrules this option. **Default:** `Infinity` * `maximumDepth` {number} Maximum number of object nesting levels (at least 1) that will be serialized. Objects at the maximum level are serialized as `'[Object]'` and arrays as `'[Array]'`. **Default:** `Infinity` * Returns: {function} A stringify function with the options applied. ```js import { configure } from 'safe-stable-stringify' const stringify = configure({ bigint: true, circularValue: 'Magic circle!', deterministic: false, maximumDepth: 1, maximumBreadth: 4 }) const circular = { bigint: 999_999_999_999_999_999n, typed: new Uint8Array(3), deterministic: "I don't think so", } circular.circular = circular circular.ignored = true circular.alsoIgnored = 'Yes!' const stringified = stringify(circular, null, 4) console.log(stringified) // { // "bigint": 999999999999999999, // "typed": "[Object]", // "deterministic": "I don't think so", // "circular": "Magic circle!", // "...": "2 items not stringified" // } const throwOnCircular = configure({ circularValue: Error }) throwOnCircular(circular); // TypeError: Converting circular structure to JSON ``` ## Differences to JSON.stringify 1. _Circular values_ are replaced with the string `[Circular]` (the value may be changed). 1. _Object keys_ are sorted instead of using the insertion order (it is possible to deactivate this). 1. _BigInt_ values are stringified as regular number instead of throwing a TypeError. 1. _Boxed primitives_ (e.g., `Number(5)`) are not unboxed and are handled as regular object. Those are the only differences to `JSON.stringify()`. This is a side effect free variant and [`toJSON`][], [`replacer`][] and the [`spacer`][] work the same as with `JSON.stringify()`. ## Performance / Benchmarks Currently this is by far the fastest known stable (deterministic) stringify implementation. This is especially important for big objects and TypedArrays. (Dell Precision 5540, i7-9850H CPU @ 2.60GHz, Node.js 16.11.1) ```md simple: simple object x 3,463,894 ops/sec ±0.44% (98 runs sampled) simple: circular x 1,236,007 ops/sec ±0.46% (99 runs sampled) simple: deep x 18,942 ops/sec ±0.41% (93 runs sampled) simple: deep circular x 18,690 ops/sec ±0.72% (96 runs sampled) replacer: simple object x 2,664,940 ops/sec ±0.31% (98 runs sampled) replacer: circular x 1,015,981 ops/sec ±0.09% (99 runs sampled) replacer: deep x 17,328 ops/sec ±0.38% (97 runs sampled) replacer: deep circular x 17,071 ops/sec ±0.21% (98 runs sampled) array: simple object x 3,869,608 ops/sec ±0.22% (98 runs sampled) array: circular x 3,853,943 ops/sec ±0.45% (96 runs sampled) array: deep x 3,563,227 ops/sec ±0.20% (100 runs sampled) array: deep circular x 3,286,475 ops/sec ±0.07% (100 runs sampled) indentation: simple object x 2,183,162 ops/sec ±0.66% (97 runs sampled) indentation: circular x 872,538 ops/sec ±0.57% (98 runs sampled) indentation: deep x 16,795 ops/sec ±0.48% (93 runs sampled) indentation: deep circular x 16,443 ops/sec ±0.40% (97 runs sampled) ``` Comparing `safe-stable-stringify` with known alternatives: ```md fast-json-stable-stringify x 18,765 ops/sec ±0.71% (94 runs sampled) json-stable-stringify x 13,870 ops/sec ±0.72% (94 runs sampled) fast-stable-stringify x 21,343 ops/sec ±0.33% (95 runs sampled) faster-stable-stringify x 17,707 ops/sec ±0.44% (97 runs sampled) json-stringify-deterministic x 11,208 ops/sec ±0.57% (98 runs sampled) fast-safe-stringify x 21,460 ops/sec ±0.75% (99 runs sampled) this x 30,367 ops/sec ±0.39% (96 runs sampled) The fastest is this ``` The `fast-safe-stringify` comparison uses the modules stable implementation. ## Acknowledgements Sponsored by [MaibornWolff](https://www.maibornwolff.de/) and [nearForm](http://nearform.com) ## License MIT [`replacer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20replacer%20parameter [`spacer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20space%20argument [`toJSON`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior [JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify safe-stable-stringify-2.3.1/test.js000066400000000000000000000735461415254260000172520ustar00rootroot00000000000000const { test } = require('tap') const { stringify } = require('./index.js') const clone = require('clone') test('circular reference to root', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.circle = fixture const expected = JSON.stringify( { circle: '[Circular]', name: 'Tywin Lannister' } ) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('nested circular reference to root', function (assert) { const fixture = { name: 'Tywin\n\t"Lannister' } fixture.id = { circle: fixture } const expected = JSON.stringify( { id: { circle: '[Circular]' }, name: 'Tywin\n\t"Lannister' } ) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('throw if circularValue is set to TypeError', function (assert) { const noCircularStringify = stringify.configure({ circularValue: TypeError }) const object = { number: 42, boolean: true, string: 'Yes!' } object.circular = object assert.throws(() => noCircularStringify(object), TypeError) assert.end() }) test('throw if circularValue is set to Error', function (assert) { const noCircularStringify = stringify.configure({ circularValue: Error }) const object = { number: 42, boolean: true, string: 'Yes!' } object.circular = object assert.throws(() => noCircularStringify(object), TypeError) assert.end() }) test('child circular reference', function (assert) { const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion\n\t"Lannister'.repeat(20) } } fixture.child.dinklage = fixture.child const expected = JSON.stringify({ child: { dinklage: '[Circular]', name: 'Tyrion\n\t"Lannister'.repeat(20) }, name: 'Tywin Lannister' }) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('nested child circular reference', function (assert) { const fixture = { name: 'Tywin Lannister', child: { name: 'Tyrion Lannister' } } fixture.child.actor = { dinklage: fixture.child } const expected = JSON.stringify({ child: { actor: { dinklage: '[Circular]' }, name: 'Tyrion Lannister' }, name: 'Tywin Lannister' }) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('circular objects in an array', function (assert) { const fixture = { name: 'Tywin Lannister' } fixture.hand = [fixture, fixture] const expected = JSON.stringify({ hand: ['[Circular]', '[Circular]'], name: 'Tywin Lannister' }) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('nested circular references in an array', function (assert) { const fixture = { name: 'Tywin Lannister', offspring: [{ name: 'Tyrion Lannister' }, { name: 'Cersei Lannister' }] } fixture.offspring[0].dinklage = fixture.offspring[0] fixture.offspring[1].headey = fixture.offspring[1] const expected = JSON.stringify({ name: 'Tywin Lannister', offspring: [ { dinklage: '[Circular]', name: 'Tyrion Lannister' }, { headey: '[Circular]', name: 'Cersei Lannister' } ] }) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('circular arrays', function (assert) { const fixture = [] fixture.push(fixture, fixture) const expected = JSON.stringify(['[Circular]', '[Circular]']) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('nested circular arrays', function (assert) { const fixture = [] fixture.push( { name: 'Jon Snow', circular: fixture }, { name: 'Ramsay Bolton', circular: fixture } ) const expected = JSON.stringify([ { circular: '[Circular]', name: 'Jon Snow' }, { circular: '[Circular]', name: 'Ramsay Bolton' } ]) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('repeated non-circular references in objects', function (assert) { const daenerys = { name: 'Daenerys Targaryen' } const fixture = { motherOfDragons: daenerys, queenOfMeereen: daenerys } const expected = JSON.stringify(fixture) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('repeated non-circular references in arrays', function (assert) { const daenerys = { name: 'Daenerys Targaryen' } const fixture = [daenerys, daenerys] const expected = JSON.stringify(fixture) const actual = stringify(fixture) assert.equal(actual, expected) assert.end() }) test('double child circular reference', function (assert) { // create circular reference const child = { name: 'Tyrion Lannister' } child.dinklage = child // include it twice in the fixture const fixture = { name: 'Tywin Lannister', childA: child, childB: child } const cloned = clone(fixture) const expected = JSON.stringify({ childA: { dinklage: '[Circular]', name: 'Tyrion Lannister' }, childB: { dinklage: '[Circular]', name: 'Tyrion Lannister' }, name: 'Tywin Lannister' }) const actual = stringify(fixture) assert.equal(actual, expected) // check if the fixture has not been modified assert.same(fixture, cloned) assert.end() }) test('child circular reference with toJSON', function (assert) { // Create a test object that has an overridden `toJSON` property TestObject.prototype.toJSON = function () { return { special: 'case' } } function TestObject () {} // Creating a simple circular object structure const parentObject = {} parentObject.childObject = new TestObject() // @ts-expect-error parentObject.childObject.parentObject = parentObject // Creating a simple circular object structure const otherParentObject = new TestObject() // @ts-expect-error otherParentObject.otherChildObject = {} // @ts-expect-error otherParentObject.otherChildObject.otherParentObject = otherParentObject // Making sure our original tests work // @ts-expect-error assert.same(parentObject.childObject.parentObject, parentObject) // @ts-expect-error assert.same(otherParentObject.otherChildObject.otherParentObject, otherParentObject) // Should both be idempotent assert.equal(stringify(parentObject), '{"childObject":{"special":"case"}}') assert.equal(stringify(otherParentObject), '{"special":"case"}') // Therefore the following assertion should be `true` // @ts-expect-error assert.same(parentObject.childObject.parentObject, parentObject) // @ts-expect-error assert.same(otherParentObject.otherChildObject.otherParentObject, otherParentObject) assert.end() }) test('null object', function (assert) { const expected = JSON.stringify(null) const actual = stringify(null) assert.equal(actual, expected) assert.end() }) test('null property', function (assert) { const obj = { f: null } const expected = JSON.stringify(obj) const actual = stringify(obj) assert.equal(actual, expected) assert.end() }) test('null property', function (assert) { const obj = { toJSON () { return null } } const expected = JSON.stringify(obj) const actual = stringify(obj) assert.equal(actual, expected) assert.end() }) test('nested child circular reference in toJSON', function (assert) { var circle = { some: 'data' } circle.circle = circle var a = { b: { toJSON: function () { // @ts-expect-error a.b = 2 return '[Redacted]' } }, baz: { circle, toJSON: function () { // @ts-expect-error a.baz = circle return '[Redacted]' } } } var o = { a, bar: a } const expected = JSON.stringify({ a: { b: '[Redacted]', baz: '[Redacted]' }, bar: { b: 2, baz: { circle: '[Circular]', some: 'data' } } }) const actual = stringify(o) assert.equal(actual, expected) assert.end() }) test('invalid replacer being ignored', function (assert) { const obj = { a: true } // @ts-expect-error const actual = stringify(obj, 'invalidReplacer') const expected = stringify(obj) assert.equal(actual, expected) assert.end() }) test('replacer removing elements', function (assert) { const replacer = function (k, v) { if (k === 'remove') return return v } const obj = { f: null, remove: true, typed: new Int32Array(1) } const expected = JSON.stringify(obj, replacer) let actual = stringify(obj, replacer) assert.equal(actual, expected) obj.obj = obj actual = stringify(obj, replacer) assert.equal(actual, '{"f":null,"obj":"[Circular]","typed":{"0":0}}') assert.end() }) test('replacer removing elements and indentation', function (assert) { const replacer = function (k, v) { if (k === 'remove') return return v } const obj = { f: null, remove: true } const expected = JSON.stringify(obj, replacer, 2) const actual = stringify(obj, replacer, 2) assert.equal(actual, expected) assert.end() }) test('replacer removing all elements', function (assert) { const replacer = function (k, v) { if (k !== '') return return k } const obj = [{ f: null, remove: true }] let expected = JSON.stringify(obj, replacer) let actual = stringify(obj, replacer) assert.equal(actual, expected) expected = JSON.stringify({ toJSON () { return obj } }, replacer) actual = stringify({ toJSON () { return obj } }, replacer) assert.equal(actual, expected) assert.end() }) test('replacer removing all elements and indentation', function (assert) { const replacer = function (k, v) { if (k !== '') return return k } const obj = [{ f: null, remove: true }] const expected = JSON.stringify(obj, replacer, 2) const actual = stringify(obj, replacer, 2) assert.equal(actual, expected) assert.end() }) test('array replacer', function (assert) { const replacer = ['f', 1, null] const obj = { f: null, null: true, 1: false } // The null element will be removed! const expected = JSON.stringify(obj, replacer) let actual = stringify(obj, replacer) assert.equal(actual, expected) obj.f = obj actual = stringify({ toJSON () { return obj } }, replacer) assert.equal(actual, expected.replace('null', '"[Circular]"')) assert.end() }) test('empty array replacer', function (assert) { const replacer = [] const obj = { f: null, null: true, 1: false } // The null element will be removed! const expected = JSON.stringify(obj, replacer) const actual = stringify(obj, replacer) assert.equal(actual, expected) assert.end() }) test('array replacer and indentation', function (assert) { const replacer = ['f', 1, null] const obj = { f: null, null: true, 1: [false, -Infinity, 't'] } // The null element will be removed! const expected = JSON.stringify(obj, replacer, 2) const actual = stringify(obj, replacer, 2) assert.equal(actual, expected) assert.end() }) test('indent zero', function (assert) { const obj = { f: null, null: true, 1: false } const expected = JSON.stringify(obj, null, 0) const actual = stringify(obj, null, 0) assert.equal(actual, expected) assert.end() }) test('replacer and indentation without match', function (assert) { const replacer = function (k, v) { if (k === '') return v } const obj = { f: 1, b: null, c: 't', d: Infinity, e: true } const expected = JSON.stringify(obj, replacer, ' ') const actual = stringify(obj, replacer, ' ') assert.equal(actual, expected) assert.end() }) test('array replacer and indentation without match', function (assert) { const replacer = [''] const obj = { f: 1, b: null, c: 't', d: Infinity, e: true } const expected = JSON.stringify(obj, replacer, ' ') const actual = stringify(obj, replacer, ' ') assert.equal(actual, expected) assert.end() }) test('indentation without match', function (assert) { const obj = { f: undefined } const expected = JSON.stringify(obj, undefined, 3) const actual = stringify(obj, undefined, 3) assert.equal(actual, expected) assert.end() }) test('array nulls and indentation', function (assert) { const obj = [null, null] const expected = JSON.stringify(obj, undefined, 3) const actual = stringify(obj, undefined, 3) assert.equal(actual, expected) assert.end() }) test('array nulls, replacer and indentation', function (assert) { const obj = [null, Infinity, 5, true, false] const expected = JSON.stringify(obj, (_, v) => v, 3) const actual = stringify(obj, (_, v) => v, 3) assert.equal(actual, expected) assert.end() }) test('array nulls and replacer', function (assert) { const obj = [null, Infinity, 5, true, false, [], {}] const expected = JSON.stringify(obj, (_, v) => v) const actual = stringify(obj, (_, v) => v) assert.equal(actual, expected) assert.end() }) test('array nulls, array replacer and indentation', function (assert) { const obj = [null, null, [], {}] // @ts-expect-error const expected = JSON.stringify(obj, [false], 3) // @ts-expect-error const actual = stringify(obj, [false], 3) assert.equal(actual, expected) assert.end() }) test('array and array replacer', function (assert) { const obj = [null, null, 't', Infinity, true, false, [], {}] const expected = JSON.stringify(obj, [2]) const actual = stringify(obj, [2]) assert.equal(actual, expected) assert.end() }) test('indentation with elements', function (assert) { const obj = { a: 1, b: [null, 't', Infinity, true] } const expected = JSON.stringify(obj, null, 5) const actual = stringify(obj, null, 5) assert.equal(actual, expected) assert.end() }) test('object with undefined values', function (assert) { let obj = { a: 1, c: undefined, b: 'hello', d: [], e: {} } let expected = JSON.stringify(obj) let actual = stringify(obj) assert.equal(actual, expected) // @ts-expect-error obj = { b: 'hello', a: undefined, c: 1 } expected = JSON.stringify(obj) actual = stringify(obj) assert.equal(actual, expected) assert.end() }) test('undefined values and indented', function (assert) { let obj = { a: 1, c: undefined, b: 'hello' } let expected = JSON.stringify(obj, null, 2) let actual = stringify(obj, null, 2) assert.equal(actual, expected) obj = { b: 'hello', a: undefined, c: 1 } expected = JSON.stringify(obj) actual = stringify(obj) assert.equal(actual, expected) assert.end() }) test('bigint option', function (assert) { const stringifyNoBigInt = stringify.configure({ bigint: false }) const stringifyBigInt = stringify.configure({ bigint: true }) const obj = { a: 1n } const actualBigInt = stringifyBigInt(obj, null, 1) const actualNoBigInt = stringifyNoBigInt(obj, null, 1) const actualDefault = stringify(obj, null, 1) const expectedBigInt = '{\n "a": 1\n}' const expectedNoBigInt = '{}' assert.equal(actualNoBigInt, expectedNoBigInt) assert.throws(() => JSON.stringify(obj, null, 1), TypeError) assert.equal(actualBigInt, expectedBigInt) assert.equal(actualDefault, expectedBigInt) assert.throws(() => stringify.configure({ bigint: null }), /bigint/) assert.end() }) test('bigint option with replacer', function (assert) { const stringifyBigInt = stringify.configure({ bigint: true }) const obj = { a: new BigUint64Array([1n]), 0: 1n } const actualArrayReplacer = stringifyBigInt(obj, ['0', 'a']) const actualFnReplacer = stringifyBigInt(obj, (k, v) => v) const expected = '{"0":1,"a":{"0":1}}' assert.equal(actualArrayReplacer, expected) assert.equal(actualFnReplacer, expected) assert.end() }) test('bigint and typed array with indentation', function (assert) { const obj = { a: 1n, t: new Int8Array(1) } const expected = '{\n "a": 1,\n "t": {\n "0": 0\n }\n}' const actual = stringify(obj, null, 1) assert.equal(actual, expected) assert.end() }) test('bigint and typed array without indentation', function (assert) { const obj = { a: 1n, t: new Int8Array(1) } const expected = '{"a":1,"t":{"0":0}}' const actual = stringify(obj, null, 0) assert.equal(actual, expected) assert.end() }) test('no bigint without indentation', function (assert) { const stringifyNoBigInt = stringify.configure({ bigint: false }) const obj = { a: 1n, t: new Int8Array(1) } const expected = '{"t":{"0":0}}' const actual = stringifyNoBigInt(obj, null, 0) assert.equal(actual, expected) assert.end() }) test('circular value option should allow strings and null', function (assert) { let stringifyCircularValue = stringify.configure({ circularValue: 'YEAH!!!' }) const obj = {} obj.circular = obj const expected = '{"circular":"YEAH!!!"}' const actual = stringifyCircularValue(obj) assert.equal(actual, expected) assert.equal(stringify(obj), '{"circular":"[Circular]"}') stringifyCircularValue = stringify.configure({ circularValue: null }) assert.equal(stringifyCircularValue(obj), '{"circular":null}') assert.end() }) test('circular value option should throw for invalid values', function (assert) { // @ts-expect-error assert.throws(() => stringify.configure({ circularValue: { objects: 'are not allowed' } }), /circularValue/) assert.end() }) test('circular value option set to undefined should skip serialization', function (assert) { const stringifyCircularValue = stringify.configure({ circularValue: undefined }) const obj = { a: 1 } obj.circular = obj obj.b = [2, obj] const expected = '{"a":1,"b":[2,null]}' const actual = stringifyCircularValue(obj) assert.equal(actual, expected) assert.end() }) test('non-deterministic', function (assert) { const stringifyNonDeterministic = stringify.configure({ deterministic: false }) const obj = { b: true, a: false } const expected = JSON.stringify(obj) const actual = stringifyNonDeterministic(obj) assert.equal(actual, expected) // @ts-expect-error assert.throws(() => stringify.configure({ deterministic: 1 }), /deterministic/) assert.end() }) test('non-deterministic with replacer', function (assert) { const stringifyNonDeterministic = stringify.configure({ deterministic: false, bigint: false }) const obj = { b: true, a: 5n, c: Infinity, d: 4, e: [Symbol('null'), 5, Symbol('null')] } const keys = Object.keys(obj) const expected = stringify(obj, ['b', 'c', 'd', 'e']) let actual = stringifyNonDeterministic(obj, keys) assert.equal(actual, expected) actual = stringifyNonDeterministic(obj, (k, v) => v) assert.equal(actual, expected) assert.end() }) test('non-deterministic with indentation', function (assert) { const stringifyNonDeterministic = stringify.configure({ deterministic: false, bigint: false }) const obj = { b: true, a: 5, c: Infinity, d: false, e: [Symbol('null'), 5, Symbol('null')] } const expected = JSON.stringify(obj, null, 1) const actual = stringifyNonDeterministic(obj, null, 1) assert.equal(actual, expected) assert.end() }) test('check typed arrays', function (assert) { const obj = [null, null, new Float32Array(99), Infinity, Symbol('null'), true, false, [], {}, Symbol('null')] const expected = JSON.stringify(obj) const actual = stringify(obj) assert.equal(actual, expected) assert.end() }) test('check small typed arrays with extra properties', function (assert) { const obj = new Uint8Array(0) // @ts-expect-error obj.foo = true let expected = JSON.stringify(obj) let actual = stringify(obj) assert.equal(actual, expected) expected = JSON.stringify(obj, null, 2) actual = stringify(obj, null, 2) assert.equal(actual, expected) expected = JSON.stringify(obj, ['foo']) actual = stringify(obj, ['foo']) assert.equal(actual, expected) expected = JSON.stringify(obj, (a, b) => b) actual = stringify(obj, (a, b) => b) assert.equal(actual, expected) assert.end() }) test('trigger sorting fast path for objects with lots of properties', function (assert) { const keys = [] const obj = {} for (let i = 0; i < 1e4; i++) { obj[`a${i}`] = i keys.push(`a${i}`) } const start = Date.now() stringify(obj) assert.ok(Date.now() - start < 100) const now = Date.now() const actualTime = now - start keys.sort() const expectedTime = Date.now() - now assert.ok(Math.abs(actualTime - expectedTime) < 50) assert.end() }) test('maximum spacer length', function (assert) { const input = { a: 0 } const expected = `{\n${' '.repeat(10)}"a": 0\n}` assert.equal(stringify(input, null, 11), expected) assert.equal(stringify(input, null, 1e5), expected) assert.equal(stringify(input, null, ' '.repeat(11)), expected) assert.equal(stringify(input, null, ' '.repeat(1e3)), expected) assert.end() }) test('indent properly; regression test for issue #16', function (assert) { const o = { collections: {}, config: { label: 'Some\ttext\t', options: { toJSON () { return { exportNotes: true } } }, preferences: [] }, items: [{ creators: [{ lastName: 'Lander' }, { toJSON () { return null } }], date: { toJSON () { return '01/01/1989' } } }] } const arrayReplacer = ['config', 'items', 'options', 'circular', 'preferences', 'creators'] const indentedJSON = JSON.stringify(o, null, 2) const indentedJSONArrayReplacer = JSON.stringify(o, arrayReplacer, 2) const indentedJSONArrayEmpty = JSON.stringify(o, [], 2) const indentedJSONReplacer = JSON.stringify(o, (k, v) => v, 2) assert.equal( stringify(o, null, 2), indentedJSON ) assert.equal( stringify(o, arrayReplacer, 2), indentedJSONArrayReplacer ) assert.equal( stringify(o, [], 2), indentedJSONArrayEmpty ) assert.equal( // @ts-ignore stringify(o, (k, v) => v, 2), indentedJSONReplacer ) o.items[0].circular = o const circularReplacement = '"items": [\n {\n "circular": "[Circular]",\n' const circularIdentifier = '"items": [\n {\n' assert.equal( stringify(o, arrayReplacer, 2), indentedJSONArrayReplacer.replace(circularIdentifier, circularReplacement) ) assert.equal( stringify(o, null, 2), indentedJSON.replace(circularIdentifier, circularReplacement) ) assert.equal( // @ts-ignore stringify(o, (k, v) => v, 2), indentedJSONReplacer.replace(circularIdentifier, circularReplacement) ) assert.end() }) test('should stop if max depth is reached', (assert) => { const serialize = stringify.configure({ maximumDepth: 5 }) const nested = {} const MAX_DEPTH = 10 let currentNestedObject = null for (let i = 0; i < MAX_DEPTH; i++) { const k = 'nest_' + i if (!currentNestedObject) { currentNestedObject = nested } currentNestedObject[k] = { foo: 'bar' } currentNestedObject = currentNestedObject[k] } const res = serialize(nested) assert.ok(res.indexOf('"nest_4":"[Object]"')) assert.end() }) test('should serialize only first 10 elements', (assert) => { const serialize = stringify.configure({ maximumBreadth: 10 }) const breadth = {} const MAX_BREADTH = 100 for (let i = 0; i < MAX_BREADTH; i++) { const k = 'key_' + i breadth[k] = 'foobar' } const res = serialize(breadth) const expected = '{"key_0":"foobar","key_1":"foobar","key_10":"foobar","key_11":"foobar","key_12":"foobar","key_13":"foobar","key_14":"foobar","key_15":"foobar","key_16":"foobar","key_17":"foobar","...":"90 items not stringified"}' assert.equal(res, expected) assert.end() }) test('should serialize only first 10 elements with custom replacer and indentation', (assert) => { const serialize = stringify.configure({ maximumBreadth: 10, maximumDepth: 1 }) const breadth = { a: Array.from({ length: 100 }, (_, i) => i) } const MAX_BREADTH = 100 for (let i = 0; i < MAX_BREADTH; i++) { const k = 'key_' + i breadth[k] = 'foobar' } const res = serialize(breadth, (k, v) => v, 2) const expected = `{ "a": "[Array]", "key_0": "foobar", "key_1": "foobar", "key_10": "foobar", "key_11": "foobar", "key_12": "foobar", "key_13": "foobar", "key_14": "foobar", "key_15": "foobar", "key_16": "foobar", "...": "91 items not stringified" }` assert.equal(res, expected) assert.end() }) test('maximumDepth config', function (assert) { const obj = { a: { b: { c: 1 }, a: [1, 2, 3] } } const serialize = stringify.configure({ maximumDepth: 2 }) const result = serialize(obj, (key, val) => val) assert.equal(result, '{"a":{"a":"[Array]","b":"[Object]"}}') const res2 = serialize(obj, ['a', 'b']) assert.equal(res2, '{"a":{"a":"[Array]","b":{}}}') const json = JSON.stringify(obj, ['a', 'b']) assert.equal(json, '{"a":{"a":[1,2,3],"b":{}}}') const res3 = serialize(obj, null, 2) assert.equal(res3, `{ "a": { "a": "[Array]", "b": "[Object]" } }`) const res4 = serialize(obj) assert.equal(res4, '{"a":{"a":"[Array]","b":"[Object]"}}') assert.end() }) test('maximumBreadth config', function (assert) { const obj = { a: ['a', 'b', 'c', 'd', 'e'] } const serialize = stringify.configure({ maximumBreadth: 3 }) const result = serialize(obj, (key, val) => val) assert.equal(result, '{"a":["a","b","c","... 1 item not stringified"]}') const res2 = serialize(obj, ['a', 'b']) assert.equal(res2, '{"a":["a","b","c","... 1 item not stringified"]}') const res3 = serialize(obj, null, 2) assert.equal(res3, `{ "a": [ "a", "b", "c", "... 1 item not stringified" ] }`) const res4 = serialize({ a: { a: 1, b: 1, c: 1, d: 1, e: 1 } }, null, 2) assert.equal(res4, `{ "a": { "a": 1, "b": 1, "c": 1, "...": "2 items not stringified" } }`) assert.end() }) test('limit number of keys with array replacer', function (assert) { const replacer = ['a', 'b', 'c', 'd', 'e'] const obj = { a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g', h: 'h' } const serialize = stringify.configure({ maximumBreadth: 3 }) const res = serialize(obj, replacer, 2) const expected = `{ "a": "a", "b": "b", "c": "c", "d": "d", "e": "e" }` assert.equal(res, expected) assert.end() }) test('limit number of keys in array', (assert) => { const serialize = stringify.configure({ maximumBreadth: 3 }) const arr = [] const MAX_BREADTH = 100 for (let i = 0; i < MAX_BREADTH; i++) { arr.push(i) } const res = serialize(arr) const expected = '[0,1,2,"... 96 items not stringified"]' assert.equal(res, expected) assert.end() }) test('limit number of keys in typed array', (assert) => { const serialize = stringify.configure({ maximumBreadth: 3 }) const MAX = 100 const arr = new Int32Array(MAX) for (let i = 0; i < MAX; i++) { arr[i] = i } // @ts-expect-error we want to explicitly test this behavior. arr.foobar = true const res = serialize(arr) const expected = '{"0":0,"1":1,"2":2,"...":"98 items not stringified"}' assert.equal(res, expected) const res2 = serialize(arr, (a, b) => b) assert.equal(res2, expected) const res3 = serialize(arr, [0, 1, 2]) assert.equal(res3, '{"0":0,"1":1,"2":2}') const res4 = serialize(arr, null, 4) assert.equal(res4, `{ "0": 0, "1": 1, "2": 2, "...": "98 items not stringified" }`) assert.end() }) test('show skipped keys even non were serliazable', (assert) => { const serialize = stringify.configure({ maximumBreadth: 1 }) const input = { a: Symbol('ignored'), b: Symbol('ignored') } let actual = serialize(input) let expected = '{"...":"1 item not stringified"}' assert.equal(actual, expected) actual = serialize(input, (a, b) => b) assert.equal(actual, expected) actual = serialize(input, null, 1) expected = '{\n "...": "1 item not stringified"\n}' assert.equal(actual, expected) actual = serialize(input, (a, b) => b, 1) assert.equal(actual, expected) actual = serialize(input, ['a']) expected = '{}' assert.equal(actual, expected) actual = serialize(input, ['a', 'b', 'c']) assert.equal(actual, expected) assert.end() }) test('array replacer entries are unique', (assert) => { const input = { 0: 0, b: 1 } const replacer = ['b', {}, [], 0, 'b', '0'] // @ts-expect-error const actual = stringify(input, replacer) // @ts-expect-error const expected = JSON.stringify(input, replacer) assert.equal(actual, expected) assert.end() }) test('should throw when maximumBreadth receives malformed input', (assert) => { assert.throws(() => { stringify.configure({ // @ts-expect-error maximumBreadth: '3' }) }) assert.throws(() => { stringify.configure({ maximumBreadth: 3.1 }) }) assert.throws(() => { stringify.configure({ maximumBreadth: 0 }) }) assert.end() }) test('check that all single characters are identical to JSON.stringify', (assert) => { for (let i = 0; i < 2 ** 16; i++) { const string = String.fromCharCode(i) const actual = stringify(string) const expected = JSON.stringify(string) // Older Node.js versions do not use the well formed JSON implementation. if (Number(process.version.split('.')[0].slice(1)) >= 12 || i < 0xd800 || i > 0xdfff) { assert.equal(actual, expected) } else { assert.not(actual, expected) } } // Trigger special case const longStringEscape = stringify(`${'a'.repeat(100)}\uD800`) assert.equal(longStringEscape, `"${'a'.repeat(100)}\\ud800"`) assert.end() }) test('check for lone surrogate pairs', (assert) => { const edgeChar = String.fromCharCode(0xd799) for (let charCode = 0xD800; charCode < 0xDFFF; charCode++) { const surrogate = String.fromCharCode(charCode) assert.equal( stringify(surrogate), `"\\u${charCode.toString(16)}"` ) assert.equal( stringify(`${'a'.repeat(200)}${surrogate}`), `"${'a'.repeat(200)}\\u${charCode.toString(16)}"` ) assert.equal( stringify(`${surrogate}${'a'.repeat(200)}`), `"\\u${charCode.toString(16)}${'a'.repeat(200)}"` ) if (charCode < 0xdc00) { const highSurrogate = surrogate const lowSurrogate = String.fromCharCode(charCode + 1024) assert.notOk( stringify( `${edgeChar}${highSurrogate}${lowSurrogate}${edgeChar}` ).includes('\\u') ) assert.equal( (stringify( `${highSurrogate}${highSurrogate}${lowSurrogate}` ).match(/\\u/g) || []).length, 1 ) } else { assert.equal( stringify(`${edgeChar}${surrogate}${edgeChar}`), `"${edgeChar}\\u${charCode.toString(16)}${edgeChar}"` ) } } assert.end() }) safe-stable-stringify-2.3.1/test.json000066400000000000000000000154071415254260000175770ustar00rootroot00000000000000[ { "_id": "5a660b85cc23d42973bb8f61", "index": 0, "guid": "cbd5519b-24d2-479b-bd07-c6d676d350e7", "isActive": true, "balance": "$3,182.51", "picture": "http://placehold.it/32x32", "age": 37, "eyeColor": "brown", "name": "Tami Burton", "gender": "female", "company": "PUSHCART", "email": "tamiburton@pushcart.com", "phone": "+1 (821) 429-2754", "address": "272 Nichols Avenue, Brownlee, Iowa, 8202", "about": "Incididunt aute irure ex dolore amet id dolor occaecat eu ullamco. Consectetur adipisicing ea Lorem laborum. Incididunt culpa nulla deserunt sit laboris pariatur occaecat est exercitation sit. Laboris non proident nulla do enim cillum laboris labore id est nisi commodo sint esse. Sit eiusmod ad elit consequat irure Lorem ex do id minim.\r\n", "registered": "2015-04-02T06:41:59 -02:00", "latitude": -49.91047, "longitude": 125.004934, "tags": [ "do", "sint", "tempor", "enim", "velit", "eu", "proident" ], "friends": [ { "id": 0, "name": "Montgomery Chandler" }, { "id": 1, "name": "Pollard Nixon" }, { "id": 2, "name": "Elvia Curry" } ], "greeting": "Hello, Tami Burton! You have 1 unread messages.", "favoriteFruit": "strawberry" }, { "_id": "5a660b857751ef05c308268b", "index": 1, "guid": "1628176c-c9f6-4756-b605-5d5815a8c05d", "isActive": false, "balance": "$2,649.96", "picture": "http://placehold.it/32x32", "age": 38, "eyeColor": "brown", "name": "Jami Buck", "gender": "female", "company": "BICOL", "email": "jamibuck@bicol.com", "phone": "+1 (866) 562-3777", "address": "141 Gallatin Place, Geyserville, Alabama, 7547", "about": "In occaecat veniam nisi do velit occaecat laboris cupidatat est voluptate. Labore quis id enim enim do tempor non ad dolor consectetur ea nisi. Eu cupidatat aute occaecat et in consequat aute nisi cupidatat est. Occaecat aliquip magna proident nostrud magna ad deserunt. Lorem Lorem ipsum irure laborum est Lorem mollit consequat eu. Et irure aliqua enim sit excepteur mollit.\r\n", "registered": "2015-08-21T04:06:37 -02:00", "latitude": -32.157219, "longitude": 167.849252, "tags": [ "aute", "laborum", "enim", "ut", "proident", "aliqua", "est" ], "friends": [ { "id": 0, "name": "Nell Foreman" }, { "id": 1, "name": "Dillard Waters" }, { "id": 2, "name": "Tabatha Hunter" } ], "greeting": "Hello, Jami Buck! You have 9 unread messages.", "favoriteFruit": "banana" }, { "_id": "5a660b8524acee2f78c68ad3", "index": 2, "guid": "e6b479c7-995f-4742-a939-f7c6b03a9d84", "isActive": false, "balance": "$1,199.92", "picture": "http://placehold.it/32x32", "age": 29, "eyeColor": "blue", "name": "Aisha Conner", "gender": "female", "company": "ZILPHUR", "email": "aishaconner@zilphur.com", "phone": "+1 (806) 419-3132", "address": "815 Lafayette Avenue, Knowlton, Connecticut, 4266", "about": "Culpa proident exercitation consequat amet do nisi qui dolore do exercitation ea. Officia esse est mollit cillum. Eu qui laboris minim sint pariatur. Esse occaecat est esse elit.\r\n", "registered": "2016-07-28T05:45:25 -02:00", "latitude": -25.704426, "longitude": -72.539193, "tags": [ "adipisicing", "exercitation", "sunt", "est", "ut", "do", "est" ], "friends": [ { "id": 0, "name": "Barbara Mckee" }, { "id": 1, "name": "Rachel Pennington" }, { "id": 2, "name": "Beverley Christian" } ], "greeting": "Hello, Aisha Conner! You have 5 unread messages.", "favoriteFruit": "strawberry" }, { "_id": "5a660b85d031eea36971a882", "index": 3, "guid": "d52d9dcc-db48-445b-9c5f-b435fda26099", "isActive": true, "balance": "$3,059.34", "picture": "http://placehold.it/32x32", "age": 22, "eyeColor": "green", "name": "Brooke Whitfield", "gender": "female", "company": "JIMBIES", "email": "brookewhitfield@jimbies.com", "phone": "+1 (991) 538-3370", "address": "906 Lincoln Terrace, Bascom, Utah, 2326", "about": "Lorem non sit mollit proident. Velit minim ex quis et amet proident officia ut elit ad est culpa sit mollit. Ullamco tempor adipisicing consectetur laborum esse minim laborum est exercitation nostrud eu. Fugiat sint consectetur velit reprehenderit. Ullamco pariatur mollit do proident anim quis tempor elit. In est ad sit sint ea laborum minim.\r\n", "registered": "2017-02-22T02:41:41 -01:00", "latitude": -33.567516, "longitude": -87.814848, "tags": [ "eu", "eu", "sit", "incididunt", "ea", "laborum", "ullamco" ], "friends": [ { "id": 0, "name": "Schwartz Tanner" }, { "id": 1, "name": "Gutierrez Porter" }, { "id": 2, "name": "Terra Mcguire" } ], "greeting": "Hello, Brooke Whitfield! You have 6 unread messages.", "favoriteFruit": "strawberry" }, { "_id": "5a660b858f41af2de9dfd5be", "index": 4, "guid": "88a4bdd5-e4fa-440d-8345-50c149a636f9", "isActive": true, "balance": "$1,867.97", "picture": "http://placehold.it/32x32", "age": 39, "eyeColor": "brown", "name": "Shauna Leonard", "gender": "female", "company": "XUMONK", "email": "shaunaleonard@xumonk.com", "phone": "+1 (890) 549-3263", "address": "865 Folsom Place, Gorham, Pennsylvania, 9359", "about": "Veniam ullamco elit do in velit tempor eiusmod eiusmod sit duis. Esse sit cupidatat culpa nisi mollit nisi ut nisi nulla nulla. Reprehenderit culpa anim ea dolore enim occaecat est. Incididunt incididunt dolor anim ullamco qui labore consequat exercitation elit sint incididunt culpa labore. Cupidatat proident duis tempor nostrud pariatur adipisicing. Tempor occaecat proident deserunt non irure irure. Lorem sint anim dolore exercitation cupidatat commodo proident labore irure commodo sunt.\r\n", "registered": "2016-12-17T08:40:42 -01:00", "latitude": -50.040955, "longitude": -39.104934, "tags": [ "magna", "et", "ex", "duis", "ex", "non", "mollit" ], "friends": [ { "id": 0, "name": "Berry Carr" }, { "id": 1, "name": "Ashley Williams" }, { "id": 2, "name": "Hickman Pace" } ], "greeting": "Hello, Shauna Leonard! You have 9 unread messages.", "favoriteFruit": "banana" } ]safe-stable-stringify-2.3.1/tsconfig.json000066400000000000000000000007471415254260000204350ustar00rootroot00000000000000{ "compilerOptions": { "baseUrl": "./", "allowJs": true, "checkJs": true, "sourceMap": false, "declaration": false, "noEmit": true, "downlevelIteration": false, "experimentalDecorators": false, "moduleResolution": "node", "importHelpers": false, "target": "esnext", "module": "CommonJS", "lib": [ "esnext", "dom" ] }, "include": ["**/*.js", "**/*.ts"], "exclude": ["compare.js", "benchmark.js", "./coverage"] }