safe-stable-stringify
Advanced tools
Comparing version 2.0.0 to 2.1.0
@@ -5,3 +5,3 @@ 'use strict' | ||
const suite = new Benchmark.Suite() | ||
const stringify = require('.') | ||
const stringify = require('.').configure({ deterministic: true }) | ||
@@ -8,0 +8,0 @@ // eslint-disable-next-line |
# Changelog | ||
## 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 | ||
@@ -4,0 +13,0 @@ |
@@ -1,3 +0,3 @@ | ||
declare function stringify(value: any, replacer?: (key: string, value: any) => any, space?: string | number): string; | ||
declare function stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string; | ||
export 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; | ||
@@ -8,8 +8,12 @@ export interface StringifyOptions { | ||
deterministic?: boolean, | ||
maximumBreadth?: number, | ||
maximumDepth?: number, | ||
} | ||
declare function configure(StringifyOptions): stringify; | ||
export namespace stringify { | ||
export function configure(options: StringifyOptions): typeof stringify; | ||
} | ||
stringify.configure = configure; | ||
export function configure(options: StringifyOptions): typeof stringify; | ||
export default stringify; |
257
index.js
'use strict' | ||
const stringify = main() | ||
stringify.configure = main | ||
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 = /[\x00-\x1f\x22\x5c]/ | ||
const strEscapeSequencesRegExp = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/ | ||
// eslint-disable-next-line | ||
const strEscapeSequencesReplacer = /[\x00-\x1f\x22\x5c]/g | ||
const strEscapeSequencesReplacer = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]/g | ||
@@ -32,9 +43,10 @@ // Escaped special characters. Use empty strings to fill up unused entries. | ||
function escapeFn (str) { | ||
return meta[str.charCodeAt(0)] | ||
const charCode = str.charCodeAt(0) | ||
return meta.length > charCode | ||
? meta[charCode] | ||
: `\\u${charCode.toString(16).padStart(4, '0')}` | ||
} | ||
// Escape control characters, double quotes and the backslash. | ||
// Note: it is faster to run this only once for a big string instead of only for | ||
// the parts that it is necessary for. But this is only true if we do not add | ||
// extra indentation to the string before. | ||
// 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) { | ||
@@ -50,19 +62,13 @@ // Some magic numbers that worked out fine while benchmarking with v8 8.0 | ||
let last = 0 | ||
let i = 0 | ||
for (; i < str.length; i++) { | ||
for (let i = 0; i < str.length; i++) { | ||
const point = str.charCodeAt(i) | ||
if (point === 34 || point === 92 || point < 32) { | ||
if (last === i) { | ||
result += meta[point] | ||
} else { | ||
result += `${str.slice(last, i)}${meta[point]}` | ||
} | ||
result += `${str.slice(last, i)}${meta[point]}` | ||
last = i + 1 | ||
} else if (point >= 55296 && point <= 57343) { | ||
result += `${str.slice(last, i)}${`\\u${point.toString(16).padStart(4, '0')}`}` | ||
last = i + 1 | ||
} | ||
} | ||
if (last === 0) { | ||
result = str | ||
} else if (last !== i) { | ||
result += str.slice(last) | ||
} | ||
result += str.slice(last) | ||
return result | ||
@@ -99,13 +105,13 @@ } | ||
function isTypedArray (value) { | ||
return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined | ||
function isTypedArrayWithEntries (value) { | ||
return typedArrayPrototypeGetSymbolToStringTag.call(value) !== undefined && value.length !== 0 | ||
} | ||
function stringifyTypedArray (array, separator) { | ||
if (array.length === 0) { | ||
return '' | ||
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 < array.length; i++) { | ||
for (let i = 1; i < maximumBreadth; i++) { | ||
res += `${separator}"${i}":${whitespace}${array[i]}` | ||
@@ -128,29 +134,55 @@ } | ||
function getBigIntOption (options) { | ||
if (options && Object.prototype.hasOwnProperty.call(options, 'bigint')) { | ||
var bigint = options.bigint | ||
if (typeof bigint !== 'boolean') { | ||
throw new TypeError('The "bigint" argument must be of type boolean') | ||
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 bigint === undefined ? true : bigint | ||
return value === undefined ? true : value | ||
} | ||
function getDeterministicOption (options) { | ||
if (options && Object.prototype.hasOwnProperty.call(options, 'deterministic')) { | ||
var deterministic = options.deterministic | ||
if (typeof deterministic !== 'boolean') { | ||
throw new TypeError('The "deterministic" argument must be of type boolean') | ||
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 deterministic === undefined ? true : deterministic | ||
return value === undefined ? Infinity : value | ||
} | ||
function main (options) { | ||
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 = getBigIntOption(options) | ||
const deterministic = getDeterministicOption(options) | ||
const bigint = getBooleanOption(options, 'bigint') | ||
const deterministic = getBooleanOption(options, 'deterministic') | ||
const maximumDepth = getPositiveIntegerOption(options, 'maximumDepth') | ||
const maximumBreadth = getPositiveIntegerOption(options, 'maximumBreadth') | ||
// Full version: supports all options | ||
function stringifyFullFn (key, parent, stack, replacer, spacer, indentation) { | ||
function stringifyFnReplacer (key, parent, stack, replacer, spacer, indentation) { | ||
let value = parent[key] | ||
@@ -182,2 +214,5 @@ | ||
} | ||
if (maximumDepth < stack.length + 1) { | ||
return '"[Array]"' | ||
} | ||
stack.push(value) | ||
@@ -189,10 +224,15 @@ if (spacer !== '') { | ||
} | ||
const maximumValuesToStringify = Math.min(value.length, maximumBreadth) | ||
let i = 0 | ||
for (; i < value.length - 1; i++) { | ||
const tmp = stringifyFullFn(i, value, stack, replacer, spacer, indentation) | ||
for (; i < maximumValuesToStringify - 1; i++) { | ||
const tmp = stringifyFnReplacer(i, value, stack, replacer, spacer, indentation) | ||
res += tmp !== undefined ? tmp : 'null' | ||
res += join | ||
} | ||
const tmp = stringifyFullFn(i, value, stack, replacer, spacer, indentation) | ||
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 !== '') { | ||
@@ -206,5 +246,9 @@ res += `\n${originalIndentation}` | ||
let keys = Object.keys(value) | ||
if (keys.length === 0) { | ||
const keyLength = keys.length | ||
if (keyLength === 0) { | ||
return '{}' | ||
} | ||
if (maximumDepth < stack.length + 1) { | ||
return '"[Object]"' | ||
} | ||
let whitespace = '' | ||
@@ -217,5 +261,7 @@ let separator = '' | ||
} | ||
if (isTypedArray(value)) { | ||
res += stringifyTypedArray(value, join) | ||
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) | ||
if (isTypedArrayWithEntries(value)) { | ||
res += stringifyTypedArray(value, join, maximumBreadth) | ||
keys = keys.slice(value.length) | ||
maximumPropertiesToStringify -= value.length | ||
separator = join | ||
@@ -227,4 +273,5 @@ } | ||
stack.push(value) | ||
for (const key of keys) { | ||
const tmp = stringifyFullFn(key, value, stack, replacer, spacer, indentation) | ||
for (let i = 0; i < maximumPropertiesToStringify; i++) { | ||
const key = keys[i] | ||
const tmp = stringifyFnReplacer(key, value, stack, replacer, spacer, indentation) | ||
if (tmp !== undefined) { | ||
@@ -235,2 +282,7 @@ res += `${separator}"${strEscape(key)}":${whitespace}${tmp}` | ||
} | ||
if (keyLength > maximumBreadth) { | ||
const removedKeys = keyLength - maximumBreadth | ||
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"` | ||
separator = join | ||
} | ||
if (spacer !== '' && separator.length > 1) { | ||
@@ -251,3 +303,3 @@ res = `\n${indentation}${res}\n${originalIndentation}` | ||
function stringifyFullArr (key, value, stack, replacer, spacer, indentation) { | ||
function stringifyArrayReplacer (key, value, stack, replacer, spacer, indentation) { | ||
if (typeof value === 'object' && value !== null && typeof value.toJSON === 'function') { | ||
@@ -276,2 +328,5 @@ value = value.toJSON(key) | ||
} | ||
if (maximumDepth < stack.length + 1) { | ||
return '"[Array]"' | ||
} | ||
stack.push(value) | ||
@@ -283,10 +338,15 @@ if (spacer !== '') { | ||
} | ||
const maximumValuesToStringify = Math.min(value.length, maximumBreadth) | ||
let i = 0 | ||
for (; i < value.length - 1; i++) { | ||
const tmp = stringifyFullArr(i, value[i], stack, replacer, spacer, indentation) | ||
for (; i < maximumValuesToStringify - 1; i++) { | ||
const tmp = stringifyArrayReplacer(i, value[i], stack, replacer, spacer, indentation) | ||
res += tmp !== undefined ? tmp : 'null' | ||
res += join | ||
} | ||
const tmp = stringifyFullArr(i, value[i], stack, replacer, spacer, indentation) | ||
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 !== '') { | ||
@@ -298,4 +358,3 @@ res += `\n${originalIndentation}` | ||
} | ||
if (replacer.length === 0) { | ||
if (replacer.size === 0) { | ||
return '{}' | ||
@@ -312,8 +371,6 @@ } | ||
for (const key of replacer) { | ||
if (typeof key === 'string' || typeof key === 'number') { | ||
const tmp = stringifyFullArr(key, value[key], stack, replacer, spacer, indentation) | ||
if (tmp !== undefined) { | ||
res += `${separator}"${strEscape(key)}":${whitespace}${tmp}` | ||
separator = join | ||
} | ||
const tmp = stringifyArrayReplacer(key, value[key], stack, replacer, spacer, indentation) | ||
if (tmp !== undefined) { | ||
res += `${separator}"${strEscape(key)}":${whitespace}${tmp}` | ||
separator = join | ||
} | ||
@@ -336,3 +393,2 @@ } | ||
// Supports only the spacer option | ||
function stringifyIndent (key, value, stack, spacer, indentation) { | ||
@@ -365,2 +421,5 @@ switch (typeof value) { | ||
} | ||
if (maximumDepth < stack.length + 1) { | ||
return '"[Array]"' | ||
} | ||
stack.push(value) | ||
@@ -370,4 +429,5 @@ indentation += spacer | ||
const join = `,\n${indentation}` | ||
const maximumValuesToStringify = Math.min(value.length, maximumBreadth) | ||
let i = 0 | ||
for (; i < value.length - 1; i++) { | ||
for (; i < maximumValuesToStringify - 1; i++) { | ||
const tmp = stringifyIndent(i, value[i], stack, spacer, indentation) | ||
@@ -379,2 +439,6 @@ res += tmp !== undefined ? tmp : 'null' | ||
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}` | ||
@@ -386,5 +450,9 @@ stack.pop() | ||
let keys = Object.keys(value) | ||
if (keys.length === 0) { | ||
const keyLength = keys.length | ||
if (keyLength === 0) { | ||
return '{}' | ||
} | ||
if (maximumDepth < stack.length + 1) { | ||
return '"[Object]"' | ||
} | ||
indentation += spacer | ||
@@ -394,5 +462,7 @@ const join = `,\n${indentation}` | ||
let separator = '' | ||
if (isTypedArray(value)) { | ||
res += stringifyTypedArray(value, join) | ||
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) | ||
if (isTypedArrayWithEntries(value)) { | ||
res += stringifyTypedArray(value, join, maximumBreadth) | ||
keys = keys.slice(value.length) | ||
maximumPropertiesToStringify -= value.length | ||
separator = join | ||
@@ -404,3 +474,4 @@ } | ||
stack.push(value) | ||
for (const key of keys) { | ||
for (let i = 0; i < maximumPropertiesToStringify; i++) { | ||
const key = keys[i] | ||
const tmp = stringifyIndent(key, value[key], stack, spacer, indentation) | ||
@@ -412,2 +483,7 @@ if (tmp !== undefined) { | ||
} | ||
if (keyLength > maximumBreadth) { | ||
const removedKeys = keyLength - maximumBreadth | ||
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"` | ||
separator = join | ||
} | ||
if (separator !== '') { | ||
@@ -428,3 +504,2 @@ res = `\n${indentation}${res}\n${originalIndentation}` | ||
// Simple without any options | ||
function stringifySimple (key, value, stack) { | ||
@@ -458,5 +533,9 @@ switch (typeof value) { | ||
} | ||
if (maximumDepth < stack.length + 1) { | ||
return '"[Array]"' | ||
} | ||
stack.push(value) | ||
const maximumValuesToStringify = Math.min(value.length, maximumBreadth) | ||
let i = 0 | ||
for (; i < value.length - 1; i++) { | ||
for (; i < maximumValuesToStringify - 1; i++) { | ||
const tmp = stringifySimple(i, value[i], stack) | ||
@@ -468,2 +547,6 @@ res += tmp !== undefined ? tmp : 'null' | ||
res += tmp !== undefined ? tmp : 'null' | ||
if (value.length - 1 > maximumBreadth) { | ||
const removedKeys = value.length - maximumBreadth - 1 | ||
res += `,"... ${getItemCount(removedKeys)} not stringified"` | ||
} | ||
stack.pop() | ||
@@ -474,9 +557,16 @@ return `[${res}]` | ||
let keys = Object.keys(value) | ||
if (keys.length === 0) { | ||
const keyLength = keys.length | ||
if (keyLength === 0) { | ||
return '{}' | ||
} | ||
if (maximumDepth < stack.length + 1) { | ||
return '"[Object]"' | ||
} | ||
let separator = '' | ||
if (isTypedArray(value)) { | ||
res += stringifyTypedArray(value, ',') | ||
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth) | ||
if (isTypedArrayWithEntries(value)) { | ||
res += stringifyTypedArray(value, ',', maximumBreadth) | ||
keys = keys.slice(value.length) | ||
maximumPropertiesToStringify -= value.length | ||
separator = ',' | ||
} | ||
@@ -487,3 +577,4 @@ if (deterministic) { | ||
stack.push(value) | ||
for (const key of keys) { | ||
for (let i = 0; i < maximumPropertiesToStringify; i++) { | ||
const key = keys[i] | ||
const tmp = stringifySimple(key, value[key], stack) | ||
@@ -495,2 +586,6 @@ if (tmp !== undefined) { | ||
} | ||
if (keyLength > maximumBreadth) { | ||
const removedKeys = keyLength - maximumBreadth | ||
res += `${separator}"...":"${getItemCount(removedKeys)} not stringified"` | ||
} | ||
stack.pop() | ||
@@ -512,15 +607,15 @@ return `{${res}}` | ||
if (typeof space === 'number') { | ||
spacer = ' '.repeat(space) | ||
spacer = ' '.repeat(Math.min(space, 10)) | ||
} else if (typeof space === 'string') { | ||
spacer = space | ||
spacer = space.slice(0, 10) | ||
} | ||
if (replacer != null) { | ||
if (typeof replacer === 'function') { | ||
return stringifyFullFn('', { '': value }, [], replacer, spacer, '') | ||
return stringifyFnReplacer('', { '': value }, [], replacer, spacer, '') | ||
} | ||
if (Array.isArray(replacer)) { | ||
return stringifyFullArr('', value, [], replacer, spacer, '') | ||
return stringifyArrayReplacer('', value, [], getUniqueReplacerSet(replacer), spacer, '') | ||
} | ||
} | ||
if (spacer !== '') { | ||
if (spacer.length !== 0) { | ||
return stringifyIndent('', value, [], spacer, '') | ||
@@ -527,0 +622,0 @@ } |
{ | ||
"name": "safe-stable-stringify", | ||
"version": "2.0.0", | ||
"version": "2.1.0", | ||
"description": "Deterministic and safely JSON.stringify to quickly serialize JavaScript objects", | ||
@@ -26,11 +26,18 @@ "exports": { | ||
"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" | ||
"lint": "standard --fix", | ||
"tsc": "tsc" | ||
}, | ||
"engines": { | ||
"node": ">=10" | ||
}, | ||
"author": "Ruben Bridgewater", | ||
"license": "MIT", | ||
"typings": "index", | ||
"typings": "index.d.ts", | ||
"devDependencies": { | ||
"@types/json-stable-stringify": "^1.0.32", | ||
"@types/node": "^16.11.1", | ||
"benchmark": "^2.1.4", | ||
@@ -46,3 +53,4 @@ "clone": "^2.1.2", | ||
"standard": "^15.0.0", | ||
"tap": "^15.0.9" | ||
"tap": "^15.0.9", | ||
"typescript": "^4.4.3" | ||
}, | ||
@@ -49,0 +57,0 @@ "repository": { |
@@ -50,2 +50,10 @@ # safe-stable-stringify | ||
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. | ||
@@ -60,2 +68,4 @@ | ||
deterministic: false, | ||
maximumDepth: 1, | ||
maximumBreadth: 4 | ||
}) | ||
@@ -69,2 +79,4 @@ | ||
circular.circular = circular | ||
circular.ignored = true | ||
circular.alsoIgnored = 'Yes!' | ||
@@ -76,9 +88,6 @@ const stringified = stringify(circular, null, 4) | ||
// "bigint": 999999999999999999, | ||
// "typed": { | ||
// "0": 0, | ||
// "1": 0, | ||
// "2": 0 | ||
// }, | ||
// "typed": "[Object]", | ||
// "deterministic": "I don't think so", | ||
// "circular": "Magic circle!" | ||
// "circular": "Magic circle!", | ||
// "...": "2 items not stringified" | ||
// } | ||
@@ -92,6 +101,8 @@ ``` | ||
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 the real JSON.stringify. This is a side effect | ||
free variant and [`toJSON`][], [`replacer`][] and the [`spacer`][] work the same | ||
as with the native JSON.stringify. | ||
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()`. | ||
@@ -103,19 +114,24 @@ ## Performance / Benchmarks | ||
(Lenovo T450s with a i7-5600U CPU using Node.js 8.9.4) | ||
(Dell Precision 5540, i7-9850H CPU @ 2.60GHz, Node.js 16.11.1) | ||
```md | ||
simple: simple object x 1,733,045 ops/sec ±1.82% (86 runs sampled) | ||
simple: circular x 717,021 ops/sec ±0.78% (91 runs sampled) | ||
simple: deep x 17,674 ops/sec ±0.77% (94 runs sampled) | ||
simple: deep circular x 17,396 ops/sec ±0.70% (93 runs sampled) | ||
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 1,126,942 ops/sec ±2.22% (91 runs sampled) | ||
replacer: circular x 541,243 ops/sec ±0.87% (94 runs sampled) | ||
replacer: deep x 17,229 ops/sec ±0.90% (94 runs sampled) | ||
replacer: deep circular x 16,948 ops/sec ±0.86% (97 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 1,470,751 ops/sec ±0.84% (95 runs sampled) | ||
array: circular x 1,360,269 ops/sec ±2.94% (91 runs sampled) | ||
array: deep x 1,289,785 ops/sec ±2.82% (87 runs sampled) | ||
array: deep circular x 1,400,577 ops/sec ±1.00% (92 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) | ||
``` | ||
@@ -126,9 +142,9 @@ | ||
```md | ||
fast-json-stable-stringify x 9,336 ops/sec ±0.64% (90 runs sampled) | ||
json-stable-stringify x 7,512 ops/sec ±0.63% (91 runs sampled) | ||
fast-stable-stringify x 11,674 ops/sec ±0.58% (92 runs sampled) | ||
faster-stable-stringify x 8,893 ops/sec ±0.51% (92 runs sampled) | ||
json-stringify-deterministic x 6,240 ops/sec ±0.68% (94 runs sampled) | ||
fast-safe-stringify x 15,939 ops/sec ±0.42% (96 runs sampled) | ||
this x 24,048 ops/sec ±0.44% (91 runs sampled) | ||
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) | ||
@@ -135,0 +151,0 @@ The fastest is this |
354
test.js
const { test } = require('tap') | ||
const stringify = require('./') | ||
const { stringify } = require('./index.js') | ||
const clone = require('clone') | ||
@@ -98,8 +98,8 @@ | ||
fixture.push( | ||
{ name: 'Jon Snow', bastards: fixture }, | ||
{ name: 'Ramsay Bolton', bastards: fixture } | ||
{ name: 'Jon Snow', circular: fixture }, | ||
{ name: 'Ramsay Bolton', circular: fixture } | ||
) | ||
const expected = JSON.stringify([ | ||
{ bastards: '[Circular]', name: 'Jon Snow' }, | ||
{ bastards: '[Circular]', name: 'Ramsay Bolton' } | ||
{ circular: '[Circular]', name: 'Jon Snow' }, | ||
{ circular: '[Circular]', name: 'Ramsay Bolton' } | ||
]) | ||
@@ -158,5 +158,5 @@ const actual = stringify(fixture) | ||
test('child circular reference with toJSON', function (assert) { | ||
// Create a test object that has an overriden `toJSON` property | ||
// Create a test object that has an overridden `toJSON` property | ||
TestObject.prototype.toJSON = function () { return { special: 'case' } } | ||
function TestObject (content) {} | ||
function TestObject () {} | ||
@@ -166,2 +166,3 @@ // Creating a simple circular object structure | ||
parentObject.childObject = new TestObject() | ||
// @ts-expect-error | ||
parentObject.childObject.parentObject = parentObject | ||
@@ -171,7 +172,11 @@ | ||
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) | ||
@@ -184,3 +189,5 @@ | ||
// 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) | ||
@@ -220,2 +227,3 @@ | ||
toJSON: function () { | ||
// @ts-expect-error | ||
a.b = 2 | ||
@@ -228,2 +236,3 @@ return '[Redacted]' | ||
toJSON: function () { | ||
// @ts-expect-error | ||
a.baz = circle | ||
@@ -260,2 +269,3 @@ return '[Redacted]' | ||
// @ts-expect-error | ||
const actual = stringify(obj, 'invalidReplacer') | ||
@@ -425,3 +435,5 @@ const expected = stringify(obj) | ||
const obj = [null, null, [], {}] | ||
// @ts-expect-error | ||
const expected = JSON.stringify(obj, [false], 3) | ||
// @ts-expect-error | ||
const actual = stringify(obj, [false], 3) | ||
@@ -455,2 +467,3 @@ assert.equal(actual, expected) | ||
// @ts-expect-error | ||
obj = { b: 'hello', a: undefined, c: 1 } | ||
@@ -554,2 +567,3 @@ | ||
// @ts-expect-error | ||
assert.throws(() => stringify.configure({ circularValue: { objects: 'are not allowed' } }), /circularValue/) | ||
@@ -572,2 +586,3 @@ | ||
// @ts-expect-error | ||
assert.throws(() => stringify.configure({ deterministic: 1 }), /deterministic/) | ||
@@ -616,6 +631,20 @@ | ||
const obj = new Uint8Array(0) | ||
// @ts-expect-error | ||
obj.foo = true | ||
const expected = JSON.stringify(obj) | ||
const actual = stringify(obj) | ||
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() | ||
@@ -625,12 +654,31 @@ }) | ||
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) { | ||
@@ -670,2 +718,3 @@ const o = { | ||
assert.equal( | ||
// @ts-ignore | ||
stringify(o, (k, v) => v, 2), | ||
@@ -689,2 +738,3 @@ indentedJSONReplacer | ||
assert.equal( | ||
// @ts-ignore | ||
stringify(o, (k, v) => v, 2), | ||
@@ -696,1 +746,287 @@ indentedJSONReplacer.replace(circularIdentifier, circularReplacement) | ||
}) | ||
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 for well formed stringify implementation', (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() | ||
}) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
71099
14
1832
161
14