├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── benchmark.js ├── index.d.ts ├── index.js ├── package.json ├── readme.md ├── test-stable.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | yarn.lock 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '4' 5 | - '6' 6 | - '8' 7 | - '9' 8 | - '10' 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v.2.0.0 4 | 5 | Features 6 | 7 | - Added stable-stringify (see documentation) 8 | - Support replacer 9 | - Support spacer 10 | - toJSON support without forceDecirc property 11 | - Improved performance 12 | 13 | Breaking changes 14 | 15 | - Manipulating the input value in a `toJSON` function is not possible anymore in 16 | all cases (see documentation) 17 | - Dropped support for e.g. IE8 and Node.js < 4 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Mark Clements 4 | Copyright (c) 2017 David Mark Clements & Matteo Collina 5 | Copyright (c) 2018 David Mark Clements, Matteo Collina & Ruben Bridgewater 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark') 2 | const suite = new Benchmark.Suite() 3 | const { inspect } = require('util') 4 | const jsonStringifySafe = require('json-stringify-safe') 5 | const fastSafeStringify = require('./') 6 | 7 | const array = new Array(10).fill(0).map((_, i) => i) 8 | const obj = { foo: array } 9 | const circ = JSON.parse(JSON.stringify(obj)) 10 | circ.o = { obj: circ, array } 11 | const circGetters = JSON.parse(JSON.stringify(obj)) 12 | Object.assign(circGetters, { get o () { return { obj: circGetters, array } } }) 13 | 14 | const deep = require('./package.json') 15 | deep.deep = JSON.parse(JSON.stringify(deep)) 16 | deep.deep.deep = JSON.parse(JSON.stringify(deep)) 17 | deep.deep.deep.deep = JSON.parse(JSON.stringify(deep)) 18 | deep.array = array 19 | 20 | const deepCirc = JSON.parse(JSON.stringify(deep)) 21 | deepCirc.deep.deep.deep.circ = deepCirc 22 | deepCirc.deep.deep.circ = deepCirc 23 | deepCirc.deep.circ = deepCirc 24 | deepCirc.array = array 25 | 26 | const deepCircGetters = JSON.parse(JSON.stringify(deep)) 27 | for (let i = 0; i < 10; i++) { 28 | deepCircGetters[i.toString()] = { 29 | deep: { 30 | deep: { 31 | get circ () { return deep.deep }, 32 | deep: { get circ () { return deep.deep.deep } } 33 | }, 34 | get circ () { return deep } 35 | }, 36 | get array () { return array } 37 | } 38 | } 39 | 40 | const deepCircNonCongifurableGetters = JSON.parse(JSON.stringify(deep)) 41 | Object.defineProperty(deepCircNonCongifurableGetters.deep.deep.deep, 'circ', { 42 | get: () => deepCircNonCongifurableGetters, 43 | enumerable: true, 44 | configurable: false 45 | }) 46 | Object.defineProperty(deepCircNonCongifurableGetters.deep.deep, 'circ', { 47 | get: () => deepCircNonCongifurableGetters, 48 | enumerable: true, 49 | configurable: false 50 | }) 51 | Object.defineProperty(deepCircNonCongifurableGetters.deep, 'circ', { 52 | get: () => deepCircNonCongifurableGetters, 53 | enumerable: true, 54 | configurable: false 55 | }) 56 | Object.defineProperty(deepCircNonCongifurableGetters, 'array', { 57 | get: () => array, 58 | enumerable: true, 59 | configurable: false 60 | }) 61 | 62 | suite.add('util.inspect: simple object ', function () { 63 | inspect(obj, { showHidden: false, depth: null }) 64 | }) 65 | suite.add('util.inspect: circular ', function () { 66 | inspect(circ, { showHidden: false, depth: null }) 67 | }) 68 | suite.add('util.inspect: circular getters ', function () { 69 | inspect(circGetters, { showHidden: false, depth: null }) 70 | }) 71 | suite.add('util.inspect: deep ', function () { 72 | inspect(deep, { showHidden: false, depth: null }) 73 | }) 74 | suite.add('util.inspect: deep circular ', function () { 75 | inspect(deepCirc, { showHidden: false, depth: null }) 76 | }) 77 | suite.add('util.inspect: large deep circular getters ', function () { 78 | inspect(deepCircGetters, { showHidden: false, depth: null }) 79 | }) 80 | suite.add('util.inspect: deep non-conf circular getters', function () { 81 | inspect(deepCircNonCongifurableGetters, { showHidden: false, depth: null }) 82 | }) 83 | 84 | suite.add('\njson-stringify-safe: simple object ', function () { 85 | jsonStringifySafe(obj) 86 | }) 87 | suite.add('json-stringify-safe: circular ', function () { 88 | jsonStringifySafe(circ) 89 | }) 90 | suite.add('json-stringify-safe: circular getters ', function () { 91 | jsonStringifySafe(circGetters) 92 | }) 93 | suite.add('json-stringify-safe: deep ', function () { 94 | jsonStringifySafe(deep) 95 | }) 96 | suite.add('json-stringify-safe: deep circular ', function () { 97 | jsonStringifySafe(deepCirc) 98 | }) 99 | suite.add('json-stringify-safe: large deep circular getters ', function () { 100 | jsonStringifySafe(deepCircGetters) 101 | }) 102 | suite.add('json-stringify-safe: deep non-conf circular getters', function () { 103 | jsonStringifySafe(deepCircNonCongifurableGetters) 104 | }) 105 | 106 | suite.add('\nfast-safe-stringify: simple object ', function () { 107 | fastSafeStringify(obj) 108 | }) 109 | suite.add('fast-safe-stringify: circular ', function () { 110 | fastSafeStringify(circ) 111 | }) 112 | suite.add('fast-safe-stringify: circular getters ', function () { 113 | fastSafeStringify(circGetters) 114 | }) 115 | suite.add('fast-safe-stringify: deep ', function () { 116 | fastSafeStringify(deep) 117 | }) 118 | suite.add('fast-safe-stringify: deep circular ', function () { 119 | fastSafeStringify(deepCirc) 120 | }) 121 | suite.add('fast-safe-stringify: large deep circular getters ', function () { 122 | fastSafeStringify(deepCircGetters) 123 | }) 124 | suite.add('fast-safe-stringify: deep non-conf circular getters', function () { 125 | fastSafeStringify(deepCircNonCongifurableGetters) 126 | }) 127 | 128 | // add listeners 129 | suite.on('cycle', function (event) { 130 | console.log(String(event.target)) 131 | }) 132 | 133 | suite.on('complete', function () { 134 | console.log('\nFastest is ' + this.filter('fastest').map('name')) 135 | }) 136 | 137 | suite.run({ delay: 1, minSamples: 150 }) 138 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function stringify( 2 | value: any, 3 | replacer?: (key: string, value: any) => any, 4 | space?: string | number, 5 | options?: { depthLimit: number | undefined; edgesLimit: number | undefined } 6 | ): string; 7 | 8 | declare namespace stringify { 9 | import _default = stringify; 10 | export { _default as default }; 11 | 12 | export function stable( 13 | value: any, 14 | replacer?: (key: string, value: any) => any, 15 | space?: string | number, 16 | options?: { depthLimit: number | undefined; edgesLimit: number | undefined } 17 | ): string; 18 | export function stableStringify( 19 | value: any, 20 | replacer?: (key: string, value: any) => any, 21 | space?: string | number, 22 | options?: { depthLimit: number | undefined; edgesLimit: number | undefined } 23 | ): string; 24 | } 25 | 26 | export = stringify; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = stringify 2 | stringify.default = stringify 3 | stringify.stable = deterministicStringify 4 | stringify.stableStringify = deterministicStringify 5 | 6 | var LIMIT_REPLACE_NODE = '[...]' 7 | var CIRCULAR_REPLACE_NODE = '[Circular]' 8 | 9 | var arr = [] 10 | var replacerStack = [] 11 | 12 | function defaultOptions () { 13 | return { 14 | depthLimit: Number.MAX_SAFE_INTEGER, 15 | edgesLimit: Number.MAX_SAFE_INTEGER 16 | } 17 | } 18 | 19 | // Regular stringify 20 | function stringify (obj, replacer, spacer, options) { 21 | if (typeof options === 'undefined') { 22 | options = defaultOptions() 23 | } 24 | 25 | decirc(obj, '', 0, [], undefined, 0, options) 26 | var res 27 | try { 28 | if (replacerStack.length === 0) { 29 | res = JSON.stringify(obj, replacer, spacer) 30 | } else { 31 | res = JSON.stringify(obj, replaceGetterValues(replacer), spacer) 32 | } 33 | } catch (_) { 34 | return JSON.stringify('[unable to serialize, circular reference is too complex to analyze]') 35 | } finally { 36 | while (arr.length !== 0) { 37 | var part = arr.pop() 38 | if (part.length === 4) { 39 | Object.defineProperty(part[0], part[1], part[3]) 40 | } else { 41 | part[0][part[1]] = part[2] 42 | } 43 | } 44 | } 45 | return res 46 | } 47 | 48 | function setReplace (replace, val, k, parent) { 49 | var propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k) 50 | if (propertyDescriptor.get !== undefined) { 51 | if (propertyDescriptor.configurable) { 52 | Object.defineProperty(parent, k, { value: replace }) 53 | arr.push([parent, k, val, propertyDescriptor]) 54 | } else { 55 | replacerStack.push([val, k, replace]) 56 | } 57 | } else { 58 | parent[k] = replace 59 | arr.push([parent, k, val]) 60 | } 61 | } 62 | 63 | function decirc (val, k, edgeIndex, stack, parent, depth, options) { 64 | depth += 1 65 | var i 66 | if (typeof val === 'object' && val !== null) { 67 | for (i = 0; i < stack.length; i++) { 68 | if (stack[i] === val) { 69 | setReplace(CIRCULAR_REPLACE_NODE, val, k, parent) 70 | return 71 | } 72 | } 73 | 74 | if ( 75 | typeof options.depthLimit !== 'undefined' && 76 | depth > options.depthLimit 77 | ) { 78 | setReplace(LIMIT_REPLACE_NODE, val, k, parent) 79 | return 80 | } 81 | 82 | if ( 83 | typeof options.edgesLimit !== 'undefined' && 84 | edgeIndex + 1 > options.edgesLimit 85 | ) { 86 | setReplace(LIMIT_REPLACE_NODE, val, k, parent) 87 | return 88 | } 89 | 90 | stack.push(val) 91 | // Optimize for Arrays. Big arrays could kill the performance otherwise! 92 | if (Array.isArray(val)) { 93 | for (i = 0; i < val.length; i++) { 94 | decirc(val[i], i, i, stack, val, depth, options) 95 | } 96 | } else { 97 | var keys = Object.keys(val) 98 | for (i = 0; i < keys.length; i++) { 99 | var key = keys[i] 100 | decirc(val[key], key, i, stack, val, depth, options) 101 | } 102 | } 103 | stack.pop() 104 | } 105 | } 106 | 107 | // Stable-stringify 108 | function compareFunction (a, b) { 109 | if (a < b) { 110 | return -1 111 | } 112 | if (a > b) { 113 | return 1 114 | } 115 | return 0 116 | } 117 | 118 | function deterministicStringify (obj, replacer, spacer, options) { 119 | if (typeof options === 'undefined') { 120 | options = defaultOptions() 121 | } 122 | 123 | var tmp = deterministicDecirc(obj, '', 0, [], undefined, 0, options) || obj 124 | var res 125 | try { 126 | if (replacerStack.length === 0) { 127 | res = JSON.stringify(tmp, replacer, spacer) 128 | } else { 129 | res = JSON.stringify(tmp, replaceGetterValues(replacer), spacer) 130 | } 131 | } catch (_) { 132 | return JSON.stringify('[unable to serialize, circular reference is too complex to analyze]') 133 | } finally { 134 | // Ensure that we restore the object as it was. 135 | while (arr.length !== 0) { 136 | var part = arr.pop() 137 | if (part.length === 4) { 138 | Object.defineProperty(part[0], part[1], part[3]) 139 | } else { 140 | part[0][part[1]] = part[2] 141 | } 142 | } 143 | } 144 | return res 145 | } 146 | 147 | function deterministicDecirc (val, k, edgeIndex, stack, parent, depth, options) { 148 | depth += 1 149 | var i 150 | if (typeof val === 'object' && val !== null) { 151 | for (i = 0; i < stack.length; i++) { 152 | if (stack[i] === val) { 153 | setReplace(CIRCULAR_REPLACE_NODE, val, k, parent) 154 | return 155 | } 156 | } 157 | try { 158 | if (typeof val.toJSON === 'function') { 159 | return 160 | } 161 | } catch (_) { 162 | return 163 | } 164 | 165 | if ( 166 | typeof options.depthLimit !== 'undefined' && 167 | depth > options.depthLimit 168 | ) { 169 | setReplace(LIMIT_REPLACE_NODE, val, k, parent) 170 | return 171 | } 172 | 173 | if ( 174 | typeof options.edgesLimit !== 'undefined' && 175 | edgeIndex + 1 > options.edgesLimit 176 | ) { 177 | setReplace(LIMIT_REPLACE_NODE, val, k, parent) 178 | return 179 | } 180 | 181 | stack.push(val) 182 | // Optimize for Arrays. Big arrays could kill the performance otherwise! 183 | if (Array.isArray(val)) { 184 | for (i = 0; i < val.length; i++) { 185 | deterministicDecirc(val[i], i, i, stack, val, depth, options) 186 | } 187 | } else { 188 | // Create a temporary object in the required way 189 | var tmp = {} 190 | var keys = Object.keys(val).sort(compareFunction) 191 | for (i = 0; i < keys.length; i++) { 192 | var key = keys[i] 193 | deterministicDecirc(val[key], key, i, stack, val, depth, options) 194 | tmp[key] = val[key] 195 | } 196 | if (typeof parent !== 'undefined') { 197 | arr.push([parent, k, val]) 198 | parent[k] = tmp 199 | } else { 200 | return tmp 201 | } 202 | } 203 | stack.pop() 204 | } 205 | } 206 | 207 | // wraps replacer function to handle values we couldn't replace 208 | // and mark them as replaced value 209 | function replaceGetterValues (replacer) { 210 | replacer = 211 | typeof replacer !== 'undefined' 212 | ? replacer 213 | : function (k, v) { 214 | return v 215 | } 216 | return function (key, val) { 217 | if (replacerStack.length > 0) { 218 | for (var i = 0; i < replacerStack.length; i++) { 219 | var part = replacerStack[i] 220 | if (part[1] === key && part[0] === val) { 221 | val = part[2] 222 | replacerStack.splice(i, 1) 223 | break 224 | } 225 | } 226 | } 227 | return replacer.call(this, key, val) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-safe-stringify", 3 | "version": "2.1.1", 4 | "description": "Safely and quickly serialize JavaScript objects", 5 | "keywords": [ 6 | "stable", 7 | "stringify", 8 | "JSON", 9 | "JSON.stringify", 10 | "safe", 11 | "serialize" 12 | ], 13 | "main": "index.js", 14 | "scripts": { 15 | "test": "standard && tap --no-esm test.js test-stable.js", 16 | "benchmark": "node benchmark.js" 17 | }, 18 | "author": "David Mark Clements", 19 | "contributors": [ 20 | "Ruben Bridgewater", 21 | "Matteo Collina", 22 | "Ben Gourley", 23 | "Gabriel Lesperance", 24 | "Alex Liu", 25 | "Christoph Walcher", 26 | "Nicholas Young" 27 | ], 28 | "license": "MIT", 29 | "typings": "index", 30 | "devDependencies": { 31 | "benchmark": "^2.1.4", 32 | "clone": "^2.1.0", 33 | "json-stringify-safe": "^5.0.1", 34 | "standard": "^11.0.0", 35 | "tap": "^12.0.0" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/davidmarkclements/fast-safe-stringify.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/davidmarkclements/fast-safe-stringify/issues" 43 | }, 44 | "homepage": "https://github.com/davidmarkclements/fast-safe-stringify#readme", 45 | "dependencies": {} 46 | } 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # fast-safe-stringify 2 | 3 | Safe and fast serialization alternative to [JSON.stringify][]. 4 | 5 | Gracefully handles circular structures instead of throwing in most cases. 6 | It could return an error string if the circular object is too complex to analyze, 7 | e.g. in case there are proxies involved. 8 | 9 | Provides a deterministic ("stable") version as well that will also gracefully 10 | handle circular structures. See the example below for further information. 11 | 12 | ## Usage 13 | 14 | The same as [JSON.stringify][]. 15 | 16 | `stringify(value[, replacer[, space[, options]]])` 17 | 18 | ```js 19 | const safeStringify = require('fast-safe-stringify') 20 | const o = { a: 1 } 21 | o.o = o 22 | 23 | console.log(safeStringify(o)) 24 | // '{"a":1,"o":"[Circular]"}' 25 | console.log(JSON.stringify(o)) 26 | // TypeError: Converting circular structure to JSON 27 | 28 | function replacer(key, value) { 29 | console.log('Key:', JSON.stringify(key), 'Value:', JSON.stringify(value)) 30 | // Remove the circular structure 31 | if (value === '[Circular]') { 32 | return 33 | } 34 | return value 35 | } 36 | 37 | // those are also defaults limits when no options object is passed into safeStringify 38 | // configure it to lower the limit. 39 | const options = { 40 | depthLimit: Number.MAX_SAFE_INTEGER, 41 | edgesLimit: Number.MAX_SAFE_INTEGER 42 | }; 43 | 44 | const serialized = safeStringify(o, replacer, 2, options) 45 | // Key: "" Value: {"a":1,"o":"[Circular]"} 46 | // Key: "a" Value: 1 47 | // Key: "o" Value: "[Circular]" 48 | console.log(serialized) 49 | // { 50 | // "a": 1 51 | // } 52 | ``` 53 | 54 | 55 | Using the deterministic version also works the same: 56 | 57 | ```js 58 | const safeStringify = require('fast-safe-stringify') 59 | const o = { b: 1, a: 0 } 60 | o.o = o 61 | 62 | console.log(safeStringify(o)) 63 | // '{"b":1,"a":0,"o":"[Circular]"}' 64 | console.log(safeStringify.stableStringify(o)) 65 | // '{"a":0,"b":1,"o":"[Circular]"}' 66 | console.log(JSON.stringify(o)) 67 | // TypeError: Converting circular structure to JSON 68 | ``` 69 | 70 | A faster and side-effect free implementation is available in the 71 | [safe-stable-stringify][] module. However it is still considered experimental 72 | due to a new and more complex implementation. 73 | 74 | ### Replace strings constants 75 | 76 | - `[Circular]` - when same reference is found 77 | - `[...]` - when some limit from options object is reached 78 | 79 | ## Differences to JSON.stringify 80 | 81 | In general the behavior is identical to [JSON.stringify][]. The [`replacer`][] 82 | and [`space`][] options are also available. 83 | 84 | A few exceptions exist to [JSON.stringify][] while using [`toJSON`][] or 85 | [`replacer`][]: 86 | 87 | ### Regular safe stringify 88 | 89 | - Manipulating a circular structure of the passed in value in a `toJSON` or the 90 | `replacer` is not possible! It is possible for any other value and property. 91 | 92 | - In case a circular structure is detected and the [`replacer`][] is used it 93 | will receive the string `[Circular]` as the argument instead of the circular 94 | object itself. 95 | 96 | ### Deterministic ("stable") safe stringify 97 | 98 | - Manipulating the input object either in a [`toJSON`][] or the [`replacer`][] 99 | function will not have any effect on the output. The output entirely relies on 100 | the shape the input value had at the point passed to the stringify function! 101 | 102 | - In case a circular structure is detected and the [`replacer`][] is used it 103 | will receive the string `[Circular]` as the argument instead of the circular 104 | object itself. 105 | 106 | A side effect free variation without these limitations can be found as well 107 | ([`safe-stable-stringify`][]). It is also faster than the current 108 | implementation. It is still considered experimental due to a new and more 109 | complex implementation. 110 | 111 | ## Benchmarks 112 | 113 | Although not JSON, the Node.js `util.inspect` method can be used for similar 114 | purposes (e.g. logging) and also handles circular references. 115 | 116 | Here we compare `fast-safe-stringify` with some alternatives: 117 | (Lenovo T450s with a i7-5600U CPU using Node.js 8.9.4) 118 | 119 | ```md 120 | fast-safe-stringify: simple object x 1,121,497 ops/sec ±0.75% (97 runs sampled) 121 | fast-safe-stringify: circular x 560,126 ops/sec ±0.64% (96 runs sampled) 122 | fast-safe-stringify: deep x 32,472 ops/sec ±0.57% (95 runs sampled) 123 | fast-safe-stringify: deep circular x 32,513 ops/sec ±0.80% (92 runs sampled) 124 | 125 | util.inspect: simple object x 272,837 ops/sec ±1.48% (90 runs sampled) 126 | util.inspect: circular x 116,896 ops/sec ±1.19% (95 runs sampled) 127 | util.inspect: deep x 19,382 ops/sec ±0.66% (92 runs sampled) 128 | util.inspect: deep circular x 18,717 ops/sec ±0.63% (96 runs sampled) 129 | 130 | json-stringify-safe: simple object x 233,621 ops/sec ±0.97% (94 runs sampled) 131 | json-stringify-safe: circular x 110,409 ops/sec ±1.85% (95 runs sampled) 132 | json-stringify-safe: deep x 8,705 ops/sec ±0.87% (96 runs sampled) 133 | json-stringify-safe: deep circular x 8,336 ops/sec ±2.20% (93 runs sampled) 134 | ``` 135 | 136 | For stable stringify comparisons, see the performance benchmarks in the 137 | [`safe-stable-stringify`][] readme. 138 | 139 | ## Protip 140 | 141 | Whether `fast-safe-stringify` or alternatives are used: if the use case 142 | consists of deeply nested objects without circular references the following 143 | pattern will give best results. 144 | Shallow or one level nested objects on the other hand will slow down with it. 145 | It is entirely dependant on the use case. 146 | 147 | ```js 148 | const stringify = require('fast-safe-stringify') 149 | 150 | function tryJSONStringify (obj) { 151 | try { return JSON.stringify(obj) } catch (_) {} 152 | } 153 | 154 | const serializedString = tryJSONStringify(deep) || stringify(deep) 155 | ``` 156 | 157 | ## Acknowledgements 158 | 159 | Sponsored by [nearForm](http://nearform.com) 160 | 161 | ## License 162 | 163 | MIT 164 | 165 | [`replacer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20replacer%20parameter 166 | [`safe-stable-stringify`]: https://github.com/BridgeAR/safe-stable-stringify 167 | [`space`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The%20space%20argument 168 | [`toJSON`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior 169 | [benchmark]: https://github.com/epoberezkin/fast-json-stable-stringify/blob/67f688f7441010cfef91a6147280cc501701e83b/benchmark 170 | [JSON.stringify]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify 171 | -------------------------------------------------------------------------------- /test-stable.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const fss = require('./').stable 3 | const clone = require('clone') 4 | const s = JSON.stringify 5 | const stream = require('stream') 6 | 7 | test('circular reference to root', function (assert) { 8 | const fixture = { name: 'Tywin Lannister' } 9 | fixture.circle = fixture 10 | const expected = s({ circle: '[Circular]', name: 'Tywin Lannister' }) 11 | const actual = fss(fixture) 12 | assert.equal(actual, expected) 13 | assert.end() 14 | }) 15 | 16 | test('circular getter reference to root', function (assert) { 17 | const fixture = { 18 | name: 'Tywin Lannister', 19 | get circle () { 20 | return fixture 21 | } 22 | } 23 | 24 | const expected = s({ circle: '[Circular]', name: 'Tywin Lannister' }) 25 | const actual = fss(fixture) 26 | assert.equal(actual, expected) 27 | assert.end() 28 | }) 29 | 30 | test('nested circular reference to root', function (assert) { 31 | const fixture = { name: 'Tywin Lannister' } 32 | fixture.id = { circle: fixture } 33 | const expected = s({ id: { circle: '[Circular]' }, name: 'Tywin Lannister' }) 34 | const actual = fss(fixture) 35 | assert.equal(actual, expected) 36 | assert.end() 37 | }) 38 | 39 | test('child circular reference', function (assert) { 40 | const fixture = { 41 | name: 'Tywin Lannister', 42 | child: { name: 'Tyrion Lannister' } 43 | } 44 | fixture.child.dinklage = fixture.child 45 | const expected = s({ 46 | child: { 47 | dinklage: '[Circular]', 48 | name: 'Tyrion Lannister' 49 | }, 50 | name: 'Tywin Lannister' 51 | }) 52 | const actual = fss(fixture) 53 | assert.equal(actual, expected) 54 | assert.end() 55 | }) 56 | 57 | test('nested child circular reference', function (assert) { 58 | const fixture = { 59 | name: 'Tywin Lannister', 60 | child: { name: 'Tyrion Lannister' } 61 | } 62 | fixture.child.actor = { dinklage: fixture.child } 63 | const expected = s({ 64 | child: { 65 | actor: { dinklage: '[Circular]' }, 66 | name: 'Tyrion Lannister' 67 | }, 68 | name: 'Tywin Lannister' 69 | }) 70 | const actual = fss(fixture) 71 | assert.equal(actual, expected) 72 | assert.end() 73 | }) 74 | 75 | test('circular objects in an array', function (assert) { 76 | const fixture = { name: 'Tywin Lannister' } 77 | fixture.hand = [fixture, fixture] 78 | const expected = s({ 79 | hand: ['[Circular]', '[Circular]'], 80 | name: 'Tywin Lannister' 81 | }) 82 | const actual = fss(fixture) 83 | assert.equal(actual, expected) 84 | assert.end() 85 | }) 86 | 87 | test('nested circular references in an array', function (assert) { 88 | const fixture = { 89 | name: 'Tywin Lannister', 90 | offspring: [{ name: 'Tyrion Lannister' }, { name: 'Cersei Lannister' }] 91 | } 92 | fixture.offspring[0].dinklage = fixture.offspring[0] 93 | fixture.offspring[1].headey = fixture.offspring[1] 94 | 95 | const expected = s({ 96 | name: 'Tywin Lannister', 97 | offspring: [ 98 | { dinklage: '[Circular]', name: 'Tyrion Lannister' }, 99 | { headey: '[Circular]', name: 'Cersei Lannister' } 100 | ] 101 | }) 102 | const actual = fss(fixture) 103 | assert.equal(actual, expected) 104 | assert.end() 105 | }) 106 | 107 | test('circular arrays', function (assert) { 108 | const fixture = [] 109 | fixture.push(fixture, fixture) 110 | const expected = s(['[Circular]', '[Circular]']) 111 | const actual = fss(fixture) 112 | assert.equal(actual, expected) 113 | assert.end() 114 | }) 115 | 116 | test('nested circular arrays', function (assert) { 117 | const fixture = [] 118 | fixture.push( 119 | { name: 'Jon Snow', bastards: fixture }, 120 | { name: 'Ramsay Bolton', bastards: fixture } 121 | ) 122 | const expected = s([ 123 | { bastards: '[Circular]', name: 'Jon Snow' }, 124 | { bastards: '[Circular]', name: 'Ramsay Bolton' } 125 | ]) 126 | const actual = fss(fixture) 127 | assert.equal(actual, expected) 128 | assert.end() 129 | }) 130 | 131 | test('repeated non-circular references in objects', function (assert) { 132 | const daenerys = { name: 'Daenerys Targaryen' } 133 | const fixture = { 134 | motherOfDragons: daenerys, 135 | queenOfMeereen: daenerys 136 | } 137 | const expected = s(fixture) 138 | const actual = fss(fixture) 139 | assert.equal(actual, expected) 140 | assert.end() 141 | }) 142 | 143 | test('repeated non-circular references in arrays', function (assert) { 144 | const daenerys = { name: 'Daenerys Targaryen' } 145 | const fixture = [daenerys, daenerys] 146 | const expected = s(fixture) 147 | const actual = fss(fixture) 148 | assert.equal(actual, expected) 149 | assert.end() 150 | }) 151 | 152 | test('double child circular reference', function (assert) { 153 | // create circular reference 154 | const child = { name: 'Tyrion Lannister' } 155 | child.dinklage = child 156 | 157 | // include it twice in the fixture 158 | const fixture = { name: 'Tywin Lannister', childA: child, childB: child } 159 | const cloned = clone(fixture) 160 | const expected = s({ 161 | childA: { 162 | dinklage: '[Circular]', 163 | name: 'Tyrion Lannister' 164 | }, 165 | childB: { 166 | dinklage: '[Circular]', 167 | name: 'Tyrion Lannister' 168 | }, 169 | name: 'Tywin Lannister' 170 | }) 171 | const actual = fss(fixture) 172 | assert.equal(actual, expected) 173 | 174 | // check if the fixture has not been modified 175 | assert.same(fixture, cloned) 176 | assert.end() 177 | }) 178 | 179 | test('child circular reference with toJSON', function (assert) { 180 | // Create a test object that has an overridden `toJSON` property 181 | TestObject.prototype.toJSON = function () { 182 | return { special: 'case' } 183 | } 184 | function TestObject (content) {} 185 | 186 | // Creating a simple circular object structure 187 | const parentObject = {} 188 | parentObject.childObject = new TestObject() 189 | parentObject.childObject.parentObject = parentObject 190 | 191 | // Creating a simple circular object structure 192 | const otherParentObject = new TestObject() 193 | otherParentObject.otherChildObject = {} 194 | otherParentObject.otherChildObject.otherParentObject = otherParentObject 195 | 196 | // Making sure our original tests work 197 | assert.same(parentObject.childObject.parentObject, parentObject) 198 | assert.same( 199 | otherParentObject.otherChildObject.otherParentObject, 200 | otherParentObject 201 | ) 202 | 203 | // Should both be idempotent 204 | assert.equal(fss(parentObject), '{"childObject":{"special":"case"}}') 205 | assert.equal(fss(otherParentObject), '{"special":"case"}') 206 | 207 | // Therefore the following assertion should be `true` 208 | assert.same(parentObject.childObject.parentObject, parentObject) 209 | assert.same( 210 | otherParentObject.otherChildObject.otherParentObject, 211 | otherParentObject 212 | ) 213 | 214 | assert.end() 215 | }) 216 | 217 | test('null object', function (assert) { 218 | const expected = s(null) 219 | const actual = fss(null) 220 | assert.equal(actual, expected) 221 | assert.end() 222 | }) 223 | 224 | test('null property', function (assert) { 225 | const expected = s({ f: null }) 226 | const actual = fss({ f: null }) 227 | assert.equal(actual, expected) 228 | assert.end() 229 | }) 230 | 231 | test('nested child circular reference in toJSON', function (assert) { 232 | var circle = { some: 'data' } 233 | circle.circle = circle 234 | var a = { 235 | b: { 236 | toJSON: function () { 237 | a.b = 2 238 | return '[Redacted]' 239 | } 240 | }, 241 | baz: { 242 | circle, 243 | toJSON: function () { 244 | a.baz = circle 245 | return '[Redacted]' 246 | } 247 | } 248 | } 249 | var o = { 250 | a, 251 | bar: a 252 | } 253 | 254 | const expected = s({ 255 | a: { 256 | b: '[Redacted]', 257 | baz: '[Redacted]' 258 | }, 259 | bar: { 260 | // TODO: This is a known limitation of the current implementation. 261 | // The ideal result would be: 262 | // 263 | // b: 2, 264 | // baz: { 265 | // circle: '[Circular]', 266 | // some: 'data' 267 | // } 268 | // 269 | b: '[Redacted]', 270 | baz: '[Redacted]' 271 | } 272 | }) 273 | const actual = fss(o) 274 | assert.equal(actual, expected) 275 | assert.end() 276 | }) 277 | 278 | test('circular getters are restored when stringified', function (assert) { 279 | const fixture = { 280 | name: 'Tywin Lannister', 281 | get circle () { 282 | return fixture 283 | } 284 | } 285 | fss(fixture) 286 | 287 | assert.equal(fixture.circle, fixture) 288 | assert.end() 289 | }) 290 | 291 | test('non-configurable circular getters use a replacer instead of markers', function (assert) { 292 | const fixture = { name: 'Tywin Lannister' } 293 | Object.defineProperty(fixture, 'circle', { 294 | configurable: false, 295 | get: function () { 296 | return fixture 297 | }, 298 | enumerable: true 299 | }) 300 | 301 | fss(fixture) 302 | 303 | assert.equal(fixture.circle, fixture) 304 | assert.end() 305 | }) 306 | 307 | test('getter child circular reference', function (assert) { 308 | const fixture = { 309 | name: 'Tywin Lannister', 310 | child: { 311 | name: 'Tyrion Lannister', 312 | get dinklage () { 313 | return fixture.child 314 | } 315 | }, 316 | get self () { 317 | return fixture 318 | } 319 | } 320 | 321 | const expected = s({ 322 | child: { 323 | dinklage: '[Circular]', 324 | name: 'Tyrion Lannister' 325 | }, 326 | name: 'Tywin Lannister', 327 | self: '[Circular]' 328 | }) 329 | const actual = fss(fixture) 330 | assert.equal(actual, expected) 331 | assert.end() 332 | }) 333 | 334 | test('Proxy throwing', function (assert) { 335 | assert.plan(1) 336 | const s = new stream.PassThrough() 337 | s.resume() 338 | s.write('', () => { 339 | assert.end() 340 | }) 341 | const actual = fss({ s, p: new Proxy({}, { get () { throw new Error('kaboom') } }) }) 342 | assert.equal(actual, '"[unable to serialize, circular reference is too complex to analyze]"') 343 | }) 344 | 345 | test('depthLimit option - will replace deep objects', function (assert) { 346 | const fixture = { 347 | name: 'Tywin Lannister', 348 | child: { 349 | name: 'Tyrion Lannister' 350 | }, 351 | get self () { 352 | return fixture 353 | } 354 | } 355 | 356 | const expected = s({ 357 | child: '[...]', 358 | name: 'Tywin Lannister', 359 | self: '[Circular]' 360 | }) 361 | const actual = fss(fixture, undefined, undefined, { 362 | depthLimit: 1, 363 | edgesLimit: 1 364 | }) 365 | assert.equal(actual, expected) 366 | assert.end() 367 | }) 368 | 369 | test('edgesLimit option - will replace deep objects', function (assert) { 370 | const fixture = { 371 | object: { 372 | 1: { test: 'test' }, 373 | 2: { test: 'test' }, 374 | 3: { test: 'test' }, 375 | 4: { test: 'test' } 376 | }, 377 | array: [ 378 | { test: 'test' }, 379 | { test: 'test' }, 380 | { test: 'test' }, 381 | { test: 'test' } 382 | ], 383 | get self () { 384 | return fixture 385 | } 386 | } 387 | 388 | const expected = s({ 389 | array: [{ test: 'test' }, { test: 'test' }, { test: 'test' }, '[...]'], 390 | object: { 391 | 1: { test: 'test' }, 392 | 2: { test: 'test' }, 393 | 3: { test: 'test' }, 394 | 4: '[...]' 395 | }, 396 | self: '[Circular]' 397 | }) 398 | const actual = fss(fixture, undefined, undefined, { 399 | depthLimit: 3, 400 | edgesLimit: 3 401 | }) 402 | assert.equal(actual, expected) 403 | assert.end() 404 | }) 405 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const fss = require('./') 3 | const clone = require('clone') 4 | const s = JSON.stringify 5 | const stream = require('stream') 6 | 7 | test('circular reference to root', function (assert) { 8 | const fixture = { name: 'Tywin Lannister' } 9 | fixture.circle = fixture 10 | const expected = s({ name: 'Tywin Lannister', circle: '[Circular]' }) 11 | const actual = fss(fixture) 12 | assert.equal(actual, expected) 13 | assert.end() 14 | }) 15 | 16 | test('circular getter reference to root', function (assert) { 17 | const fixture = { 18 | name: 'Tywin Lannister', 19 | get circle () { 20 | return fixture 21 | } 22 | } 23 | const expected = s({ name: 'Tywin Lannister', circle: '[Circular]' }) 24 | const actual = fss(fixture) 25 | assert.equal(actual, expected) 26 | assert.end() 27 | }) 28 | 29 | test('nested circular reference to root', function (assert) { 30 | const fixture = { name: 'Tywin Lannister' } 31 | fixture.id = { circle: fixture } 32 | const expected = s({ name: 'Tywin Lannister', id: { circle: '[Circular]' } }) 33 | const actual = fss(fixture) 34 | assert.equal(actual, expected) 35 | assert.end() 36 | }) 37 | 38 | test('child circular reference', function (assert) { 39 | const fixture = { 40 | name: 'Tywin Lannister', 41 | child: { name: 'Tyrion Lannister' } 42 | } 43 | fixture.child.dinklage = fixture.child 44 | const expected = s({ 45 | name: 'Tywin Lannister', 46 | child: { 47 | name: 'Tyrion Lannister', 48 | dinklage: '[Circular]' 49 | } 50 | }) 51 | const actual = fss(fixture) 52 | assert.equal(actual, expected) 53 | assert.end() 54 | }) 55 | 56 | test('nested child circular reference', function (assert) { 57 | const fixture = { 58 | name: 'Tywin Lannister', 59 | child: { name: 'Tyrion Lannister' } 60 | } 61 | fixture.child.actor = { dinklage: fixture.child } 62 | const expected = s({ 63 | name: 'Tywin Lannister', 64 | child: { 65 | name: 'Tyrion Lannister', 66 | actor: { dinklage: '[Circular]' } 67 | } 68 | }) 69 | const actual = fss(fixture) 70 | assert.equal(actual, expected) 71 | assert.end() 72 | }) 73 | 74 | test('circular objects in an array', function (assert) { 75 | const fixture = { name: 'Tywin Lannister' } 76 | fixture.hand = [fixture, fixture] 77 | const expected = s({ 78 | name: 'Tywin Lannister', 79 | hand: ['[Circular]', '[Circular]'] 80 | }) 81 | const actual = fss(fixture) 82 | assert.equal(actual, expected) 83 | assert.end() 84 | }) 85 | 86 | test('nested circular references in an array', function (assert) { 87 | const fixture = { 88 | name: 'Tywin Lannister', 89 | offspring: [{ name: 'Tyrion Lannister' }, { name: 'Cersei Lannister' }] 90 | } 91 | fixture.offspring[0].dinklage = fixture.offspring[0] 92 | fixture.offspring[1].headey = fixture.offspring[1] 93 | 94 | const expected = s({ 95 | name: 'Tywin Lannister', 96 | offspring: [ 97 | { name: 'Tyrion Lannister', dinklage: '[Circular]' }, 98 | { name: 'Cersei Lannister', headey: '[Circular]' } 99 | ] 100 | }) 101 | const actual = fss(fixture) 102 | assert.equal(actual, expected) 103 | assert.end() 104 | }) 105 | 106 | test('circular arrays', function (assert) { 107 | const fixture = [] 108 | fixture.push(fixture, fixture) 109 | const expected = s(['[Circular]', '[Circular]']) 110 | const actual = fss(fixture) 111 | assert.equal(actual, expected) 112 | assert.end() 113 | }) 114 | 115 | test('nested circular arrays', function (assert) { 116 | const fixture = [] 117 | fixture.push( 118 | { name: 'Jon Snow', bastards: fixture }, 119 | { name: 'Ramsay Bolton', bastards: fixture } 120 | ) 121 | const expected = s([ 122 | { name: 'Jon Snow', bastards: '[Circular]' }, 123 | { name: 'Ramsay Bolton', bastards: '[Circular]' } 124 | ]) 125 | const actual = fss(fixture) 126 | assert.equal(actual, expected) 127 | assert.end() 128 | }) 129 | 130 | test('repeated non-circular references in objects', function (assert) { 131 | const daenerys = { name: 'Daenerys Targaryen' } 132 | const fixture = { 133 | motherOfDragons: daenerys, 134 | queenOfMeereen: daenerys 135 | } 136 | const expected = s(fixture) 137 | const actual = fss(fixture) 138 | assert.equal(actual, expected) 139 | assert.end() 140 | }) 141 | 142 | test('repeated non-circular references in arrays', function (assert) { 143 | const daenerys = { name: 'Daenerys Targaryen' } 144 | const fixture = [daenerys, daenerys] 145 | const expected = s(fixture) 146 | const actual = fss(fixture) 147 | assert.equal(actual, expected) 148 | assert.end() 149 | }) 150 | 151 | test('double child circular reference', function (assert) { 152 | // create circular reference 153 | const child = { name: 'Tyrion Lannister' } 154 | child.dinklage = child 155 | 156 | // include it twice in the fixture 157 | const fixture = { name: 'Tywin Lannister', childA: child, childB: child } 158 | const cloned = clone(fixture) 159 | const expected = s({ 160 | name: 'Tywin Lannister', 161 | childA: { 162 | name: 'Tyrion Lannister', 163 | dinklage: '[Circular]' 164 | }, 165 | childB: { 166 | name: 'Tyrion Lannister', 167 | dinklage: '[Circular]' 168 | } 169 | }) 170 | const actual = fss(fixture) 171 | assert.equal(actual, expected) 172 | 173 | // check if the fixture has not been modified 174 | assert.same(fixture, cloned) 175 | assert.end() 176 | }) 177 | 178 | test('child circular reference with toJSON', function (assert) { 179 | // Create a test object that has an overridden `toJSON` property 180 | TestObject.prototype.toJSON = function () { 181 | return { special: 'case' } 182 | } 183 | function TestObject (content) {} 184 | 185 | // Creating a simple circular object structure 186 | const parentObject = {} 187 | parentObject.childObject = new TestObject() 188 | parentObject.childObject.parentObject = parentObject 189 | 190 | // Creating a simple circular object structure 191 | const otherParentObject = new TestObject() 192 | otherParentObject.otherChildObject = {} 193 | otherParentObject.otherChildObject.otherParentObject = otherParentObject 194 | 195 | // Making sure our original tests work 196 | assert.same(parentObject.childObject.parentObject, parentObject) 197 | assert.same( 198 | otherParentObject.otherChildObject.otherParentObject, 199 | otherParentObject 200 | ) 201 | 202 | // Should both be idempotent 203 | assert.equal(fss(parentObject), '{"childObject":{"special":"case"}}') 204 | assert.equal(fss(otherParentObject), '{"special":"case"}') 205 | 206 | // Therefore the following assertion should be `true` 207 | assert.same(parentObject.childObject.parentObject, parentObject) 208 | assert.same( 209 | otherParentObject.otherChildObject.otherParentObject, 210 | otherParentObject 211 | ) 212 | 213 | assert.end() 214 | }) 215 | 216 | test('null object', function (assert) { 217 | const expected = s(null) 218 | const actual = fss(null) 219 | assert.equal(actual, expected) 220 | assert.end() 221 | }) 222 | 223 | test('null property', function (assert) { 224 | const expected = s({ f: null }) 225 | const actual = fss({ f: null }) 226 | assert.equal(actual, expected) 227 | assert.end() 228 | }) 229 | 230 | test('nested child circular reference in toJSON', function (assert) { 231 | const circle = { some: 'data' } 232 | circle.circle = circle 233 | const a = { 234 | b: { 235 | toJSON: function () { 236 | a.b = 2 237 | return '[Redacted]' 238 | } 239 | }, 240 | baz: { 241 | circle, 242 | toJSON: function () { 243 | a.baz = circle 244 | return '[Redacted]' 245 | } 246 | } 247 | } 248 | const o = { 249 | a, 250 | bar: a 251 | } 252 | 253 | const expected = s({ 254 | a: { 255 | b: '[Redacted]', 256 | baz: '[Redacted]' 257 | }, 258 | bar: { 259 | b: 2, 260 | baz: { 261 | some: 'data', 262 | circle: '[Circular]' 263 | } 264 | } 265 | }) 266 | const actual = fss(o) 267 | assert.equal(actual, expected) 268 | assert.end() 269 | }) 270 | 271 | test('circular getters are restored when stringified', function (assert) { 272 | const fixture = { 273 | name: 'Tywin Lannister', 274 | get circle () { 275 | return fixture 276 | } 277 | } 278 | fss(fixture) 279 | 280 | assert.equal(fixture.circle, fixture) 281 | assert.end() 282 | }) 283 | 284 | test('non-configurable circular getters use a replacer instead of markers', function (assert) { 285 | const fixture = { name: 'Tywin Lannister' } 286 | Object.defineProperty(fixture, 'circle', { 287 | configurable: false, 288 | get: function () { 289 | return fixture 290 | }, 291 | enumerable: true 292 | }) 293 | 294 | fss(fixture) 295 | 296 | assert.equal(fixture.circle, fixture) 297 | assert.end() 298 | }) 299 | 300 | test('getter child circular reference are replaced instead of marked', function (assert) { 301 | const fixture = { 302 | name: 'Tywin Lannister', 303 | child: { 304 | name: 'Tyrion Lannister', 305 | get dinklage () { 306 | return fixture.child 307 | } 308 | }, 309 | get self () { 310 | return fixture 311 | } 312 | } 313 | 314 | const expected = s({ 315 | name: 'Tywin Lannister', 316 | child: { 317 | name: 'Tyrion Lannister', 318 | dinklage: '[Circular]' 319 | }, 320 | self: '[Circular]' 321 | }) 322 | const actual = fss(fixture) 323 | assert.equal(actual, expected) 324 | assert.end() 325 | }) 326 | 327 | test('Proxy throwing', function (assert) { 328 | assert.plan(1) 329 | const s = new stream.PassThrough() 330 | s.resume() 331 | s.write('', () => { 332 | assert.end() 333 | }) 334 | const actual = fss({ s, p: new Proxy({}, { get () { throw new Error('kaboom') } }) }) 335 | assert.equal(actual, '"[unable to serialize, circular reference is too complex to analyze]"') 336 | }) 337 | 338 | test('depthLimit option - will replace deep objects', function (assert) { 339 | const fixture = { 340 | name: 'Tywin Lannister', 341 | child: { 342 | name: 'Tyrion Lannister' 343 | }, 344 | get self () { 345 | return fixture 346 | } 347 | } 348 | 349 | const expected = s({ 350 | name: 'Tywin Lannister', 351 | child: '[...]', 352 | self: '[Circular]' 353 | }) 354 | const actual = fss(fixture, undefined, undefined, { 355 | depthLimit: 1, 356 | edgesLimit: 1 357 | }) 358 | assert.equal(actual, expected) 359 | assert.end() 360 | }) 361 | 362 | test('edgesLimit option - will replace deep objects', function (assert) { 363 | const fixture = { 364 | object: { 365 | 1: { test: 'test' }, 366 | 2: { test: 'test' }, 367 | 3: { test: 'test' }, 368 | 4: { test: 'test' } 369 | }, 370 | array: [ 371 | { test: 'test' }, 372 | { test: 'test' }, 373 | { test: 'test' }, 374 | { test: 'test' } 375 | ], 376 | get self () { 377 | return fixture 378 | } 379 | } 380 | 381 | const expected = s({ 382 | object: { 383 | 1: { test: 'test' }, 384 | 2: { test: 'test' }, 385 | 3: { test: 'test' }, 386 | 4: '[...]' 387 | }, 388 | array: [{ test: 'test' }, { test: 'test' }, { test: 'test' }, '[...]'], 389 | self: '[Circular]' 390 | }) 391 | const actual = fss(fixture, undefined, undefined, { 392 | depthLimit: 3, 393 | edgesLimit: 3 394 | }) 395 | assert.equal(actual, expected) 396 | assert.end() 397 | }) 398 | --------------------------------------------------------------------------------