├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── tap-snapshots └── test │ └── basic.js.test.cjs └── test └── basic.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2018, 4 | "ecmaFeatures": {}, 5 | "sourceType": "script" 6 | }, 7 | 8 | "env": { 9 | "es6": true, 10 | "node": true 11 | }, 12 | 13 | "plugins": [ 14 | "import", 15 | "node", 16 | "promise", 17 | "standard" 18 | ], 19 | 20 | "globals": { 21 | "document": "readonly", 22 | "navigator": "readonly", 23 | "window": "readonly" 24 | }, 25 | 26 | "rules": { 27 | "accessor-pairs": "error", 28 | "array-bracket-spacing": ["error", "never"], 29 | "arrow-spacing": ["error", { "before": true, "after": true }], 30 | "block-spacing": ["error", "always"], 31 | "brace-style": ["error", "1tbs", { "allowSingleLine": false }], 32 | "camelcase": ["error", { "properties": "never" }], 33 | "comma-dangle": ["error", { 34 | "arrays": "always-multiline", 35 | "objects": "always-multiline", 36 | "imports": "always-multiline", 37 | "exports": "always-multiline", 38 | "functions": "never" 39 | }], 40 | "comma-spacing": ["error", { "before": false, "after": true }], 41 | "comma-style": ["error", "last"], 42 | "computed-property-spacing": ["error", "never"], 43 | "constructor-super": "error", 44 | "curly": ["error", "multi-or-nest"], 45 | "dot-location": ["error", "property"], 46 | "dot-notation": ["error", { "allowKeywords": true }], 47 | "eol-last": "error", 48 | "eqeqeq": ["error", "always", { "null": "ignore" }], 49 | "func-call-spacing": ["error", "never"], 50 | "generator-star-spacing": ["error", { "before": true, "after": true }], 51 | "handle-callback-err": ["error", "^(err|error)$" ], 52 | "indent": ["error", 2, { 53 | "SwitchCase": 1, 54 | "VariableDeclarator": 1, 55 | "outerIIFEBody": 1, 56 | "MemberExpression": 1, 57 | "FunctionDeclaration": { "parameters": 1, "body": 1 }, 58 | "FunctionExpression": { "parameters": 1, "body": 1 }, 59 | "CallExpression": { "arguments": 1 }, 60 | "ArrayExpression": 1, 61 | "ObjectExpression": 1, 62 | "ImportDeclaration": 1, 63 | "flatTernaryExpressions": true, 64 | "ignoreComments": false, 65 | "ignoredNodes": ["TemplateLiteral *"] 66 | }], 67 | "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], 68 | "keyword-spacing": ["error", { "before": true, "after": true }], 69 | "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], 70 | "new-cap": ["error", { "newIsCap": true, "capIsNew": false, "properties": true }], 71 | "new-parens": "error", 72 | "no-array-constructor": "error", 73 | "no-async-promise-executor": "error", 74 | "no-caller": "error", 75 | "no-case-declarations": "error", 76 | "no-class-assign": "error", 77 | "no-compare-neg-zero": "error", 78 | "no-cond-assign": "off", 79 | "no-const-assign": "error", 80 | "no-constant-condition": ["error", { "checkLoops": false }], 81 | "no-control-regex": "error", 82 | "no-debugger": "error", 83 | "no-delete-var": "error", 84 | "no-dupe-args": "error", 85 | "no-dupe-class-members": "error", 86 | "no-dupe-keys": "error", 87 | "no-duplicate-case": "error", 88 | "no-empty-character-class": "error", 89 | "no-empty-pattern": "error", 90 | "no-eval": "error", 91 | "no-ex-assign": "error", 92 | "no-extend-native": "error", 93 | "no-extra-bind": "error", 94 | "no-extra-boolean-cast": "error", 95 | "no-extra-parens": ["error", "functions"], 96 | "no-fallthrough": "error", 97 | "no-floating-decimal": "error", 98 | "no-func-assign": "error", 99 | "no-global-assign": "error", 100 | "no-implied-eval": "error", 101 | "no-inner-declarations": ["error", "functions"], 102 | "no-invalid-regexp": "error", 103 | "no-irregular-whitespace": "error", 104 | "no-iterator": "error", 105 | "no-labels": ["error", { "allowLoop": true, "allowSwitch": false }], 106 | "no-lone-blocks": "error", 107 | "no-misleading-character-class": "error", 108 | "no-prototype-builtins": "error", 109 | "no-useless-catch": "error", 110 | "no-mixed-operators": "off", 111 | "no-mixed-spaces-and-tabs": "error", 112 | "no-multi-spaces": "error", 113 | "no-multi-str": "error", 114 | "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], 115 | "no-negated-in-lhs": "error", 116 | "no-new": "off", 117 | "no-new-func": "error", 118 | "no-new-object": "error", 119 | "no-new-require": "error", 120 | "no-new-symbol": "error", 121 | "no-new-wrappers": "error", 122 | "no-obj-calls": "error", 123 | "no-octal": "error", 124 | "no-octal-escape": "error", 125 | "no-path-concat": "error", 126 | "no-proto": "error", 127 | "no-redeclare": ["error", { "builtinGlobals": false }], 128 | "no-regex-spaces": "error", 129 | "no-return-assign": "off", 130 | "no-self-assign": "off", 131 | "no-self-compare": "error", 132 | "no-sequences": "error", 133 | "no-shadow-restricted-names": "error", 134 | "no-sparse-arrays": "error", 135 | "no-tabs": "error", 136 | "no-template-curly-in-string": "error", 137 | "no-this-before-super": "error", 138 | "no-throw-literal": "off", 139 | "no-trailing-spaces": "error", 140 | "no-undef": "error", 141 | "no-undef-init": "error", 142 | "no-unexpected-multiline": "error", 143 | "no-unmodified-loop-condition": "error", 144 | "no-unneeded-ternary": ["error", { "defaultAssignment": false }], 145 | "no-unreachable": "error", 146 | "no-unsafe-finally": 0, 147 | "no-unsafe-negation": "error", 148 | "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], 149 | "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }], 150 | "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], 151 | "no-useless-call": "error", 152 | "no-useless-computed-key": "error", 153 | "no-useless-constructor": "error", 154 | "no-useless-escape": "error", 155 | "no-useless-rename": "error", 156 | "no-useless-return": "error", 157 | "no-void": "error", 158 | "no-whitespace-before-property": "error", 159 | "no-with": "error", 160 | "nonblock-statement-body-position": [2, "below"], 161 | "object-curly-newline": "off", 162 | "object-curly-spacing": "off", 163 | "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], 164 | "one-var": ["error", { "initialized": "never" }], 165 | "operator-linebreak": "off", 166 | "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], 167 | "prefer-const": ["error", {"destructuring": "all"}], 168 | "prefer-promise-reject-errors": "error", 169 | "quote-props": ["error", "as-needed"], 170 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 171 | "rest-spread-spacing": ["error", "never"], 172 | "semi": ["error", "never"], 173 | "semi-spacing": ["error", { "before": false, "after": true }], 174 | "space-before-blocks": ["error", "always"], 175 | "space-before-function-paren": ["error", "always"], 176 | "space-in-parens": ["error", "never"], 177 | "space-infix-ops": "error", 178 | "space-unary-ops": ["error", { "words": true, "nonwords": false }], 179 | "spaced-comment": ["error", "always", { 180 | "line": { "markers": ["*package", "!", "/", ",", "="] }, 181 | "block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] } 182 | }], 183 | "symbol-description": "error", 184 | "template-curly-spacing": ["error", "never"], 185 | "template-tag-spacing": ["error", "never"], 186 | "unicode-bom": ["error", "never"], 187 | "use-isnan": "error", 188 | "valid-typeof": ["error", { "requireStringLiterals": true }], 189 | "wrap-iife": ["error", "any", { "functionPrototypeMethods": true }], 190 | "yield-star-spacing": ["error", "both"], 191 | "yoda": ["error", "never"], 192 | 193 | "import/export": "error", 194 | "import/first": "error", 195 | "import/no-absolute-path": ["error", { "esmodule": true, "commonjs": true, "amd": false }], 196 | "import/no-duplicates": "error", 197 | "import/no-named-default": "error", 198 | "import/no-webpack-loader-syntax": "error", 199 | 200 | "node/no-deprecated-api": "error", 201 | "node/process-exit-as-throw": "error", 202 | 203 | "promise/param-names": "off", 204 | 205 | "standard/no-callback-literal": "error" 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [isaacs] 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [10.x, 12.x, 14.x, 16.x] 10 | platform: 11 | - os: ubuntu-latest 12 | - os: macos-latest 13 | - os: windows-latest 14 | fail-fast: false 15 | 16 | runs-on: ${{ matrix.platform.os }} 17 | 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v1.1.0 21 | 22 | - name: Use Nodejs ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Install dependencies 28 | run: npm install 29 | 30 | # Run for all environments 31 | - name: Run Tap Tests 32 | run: npm run test -- -c -t0 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore most things, include some others 2 | /* 3 | /.* 4 | 5 | !.eslintrc.json 6 | !.github 7 | !bin/ 8 | !lib/ 9 | !docs/ 10 | !package.json 11 | !package-lock.json 12 | !README.md 13 | !CONTRIBUTING.md 14 | !LICENSE 15 | !CHANGELOG.md 16 | !example/ 17 | !scripts/ 18 | !tap-snapshots/ 19 | !test/ 20 | !.travis.yml 21 | !.gitignore 22 | !.gitattributes 23 | !coverage-map.js 24 | !index.js 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) Isaac Z. Schlueter and Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-stringify-nice 2 | 3 | Stringify an object sorting scalars before objects, and defaulting to 4 | 2-space indent. 5 | 6 | Sometimes you want to stringify an object in a consistent way, and for 7 | human legibility reasons, you may want to put any non-object properties 8 | ahead of any object properties, so that it's easier to track the nesting 9 | level as you read through the object, but you don't want to have to be 10 | meticulous about maintaining object property order as you're building up 11 | the object, since it doesn't matter in code, it only matters in the output 12 | file. Also, it'd be nice to have it default to reasonable spacing without 13 | having to remember to add `, null, 2)` to all your `JSON.stringify()` 14 | calls. 15 | 16 | If that is what you want, then this module is for you, because it does 17 | all of that. 18 | 19 | ## USAGE 20 | 21 | ```js 22 | const stringify = require('json-stringify-nice') 23 | const obj = { 24 | z: 1, 25 | y: 'z', 26 | obj: { a: {}, b: 'x' }, 27 | a: { b: 1, a: { nested: true} }, 28 | yy: 'a', 29 | } 30 | 31 | console.log(stringify(obj)) 32 | /* output: 33 | { 34 | "y": "z", <-- alphabetical sorting like whoa! 35 | "yy": "a", 36 | "z": 1, 37 | "a": { <-- a sorted before obj, because alphabetical, and both objects 38 | "b": 1, 39 | "a": { <-- note that a comes after b, because it's an object 40 | "nested": true 41 | } 42 | }, 43 | "obj": { 44 | "b": "x", 45 | "a": {} 46 | } 47 | } 48 | */ 49 | 50 | // specify an array of keys if you have some that you prefer 51 | // to be sorted in a specific order. preferred keys come before 52 | // any other keys, and in the order specified, but objects are 53 | // still sorted AFTER scalars, so the preferences only apply 54 | // when both values are objects or both are non-objects. 55 | console.log(stringify(obj, ['z', 'yy', 'obj'])) 56 | /* output 57 | { 58 | "z": 1, <-- z comes before other scalars 59 | "yy": "a", <-- yy comes after z, but before other scalars 60 | "y": "z", <-- then all the other scalar values 61 | "obj": { <-- obj comes before other objects, but after scalars 62 | "b": "x", 63 | "a": {} 64 | }, 65 | "a": { 66 | "b": 1, 67 | "a": { 68 | "nested": true 69 | } 70 | } 71 | } 72 | */ 73 | 74 | // can also specify a replacer or indent value like with JSON.stringify 75 | // this turns all values with an 'a' key into a doggo meme from 2011 76 | const replacer = (key, val) => 77 | key === 'a' ? { hello: '📞 yes', 'this is': '🐕', ...val } : val 78 | 79 | console.log(stringify(obj, replacer, '📞🐶')) 80 | 81 | /* output: 82 | { 83 | 📞🐶"y": "z", 84 | 📞🐶"yy": "a", 85 | 📞🐶"z": 1, 86 | 📞🐶"a": { 87 | 📞🐶📞🐶"b": 1, 88 | 📞🐶📞🐶"hello": "📞 yes", 89 | 📞🐶📞🐶"this is": "🐕", 90 | 📞🐶📞🐶"a": { 91 | 📞🐶📞🐶📞🐶"hello": "📞 yes", 92 | 📞🐶📞🐶📞🐶"nested": true, 93 | 📞🐶📞🐶📞🐶"this is": "🐕" 94 | 📞🐶📞🐶} 95 | 📞🐶}, 96 | 📞🐶"obj": { 97 | 📞🐶📞🐶"b": "x", 98 | 📞🐶📞🐶"a": { 99 | 📞🐶📞🐶📞🐶"hello": "📞 yes", 100 | 📞🐶📞🐶📞🐶"this is": "🐕" 101 | 📞🐶📞🐶} 102 | 📞🐶} 103 | } 104 | */ 105 | ``` 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const isObj = val => !!val && !Array.isArray(val) && typeof val === 'object' 2 | 3 | const compare = (ak, bk, prefKeys) => 4 | prefKeys.includes(ak) && !prefKeys.includes(bk) ? -1 5 | : prefKeys.includes(bk) && !prefKeys.includes(ak) ? 1 6 | : prefKeys.includes(ak) && prefKeys.includes(bk) 7 | ? prefKeys.indexOf(ak) - prefKeys.indexOf(bk) 8 | : ak.localeCompare(bk, 'en') 9 | 10 | const sort = (replacer, seen) => (key, val) => { 11 | const prefKeys = Array.isArray(replacer) ? replacer : [] 12 | 13 | if (typeof replacer === 'function') 14 | val = replacer(key, val) 15 | 16 | if (!isObj(val)) 17 | return val 18 | 19 | if (seen.has(val)) 20 | return seen.get(val) 21 | 22 | const ret = Object.entries(val).sort( 23 | ([ak, av], [bk, bv]) => 24 | isObj(av) === isObj(bv) ? compare(ak, bk, prefKeys) 25 | : isObj(av) ? 1 26 | : -1 27 | ).reduce((set, [k, v]) => { 28 | set[k] = v 29 | return set 30 | }, {}) 31 | 32 | seen.set(val, ret) 33 | return ret 34 | } 35 | 36 | module.exports = (obj, replacer, space = 2) => 37 | JSON.stringify(obj, sort(replacer, new Map()), space) 38 | + (space ? '\n' : '') 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-stringify-nice", 3 | "version": "1.1.4", 4 | "description": "Stringify an object sorting scalars before objects, and defaulting to 2-space indent", 5 | "author": "Isaac Z. Schlueter (https://izs.me)", 6 | "license": "ISC", 7 | "scripts": { 8 | "test": "tap", 9 | "posttest": "npm run lint", 10 | "snap": "tap", 11 | "postsnap": "npm run lintfix", 12 | "eslint": "eslint", 13 | "lint": "npm run eslint -- index.js test/**/*.js", 14 | "lintfix": "npm run lint -- --fix", 15 | "preversion": "npm test", 16 | "postversion": "npm publish", 17 | "postpublish": "git push origin --follow-tags" 18 | }, 19 | "tap": { 20 | "test-env": [ 21 | "LC_ALL=sk" 22 | ], 23 | "check-coverage": true 24 | }, 25 | "devDependencies": { 26 | "eslint": "^7.25.0", 27 | "eslint-plugin-import": "^2.22.1", 28 | "eslint-plugin-node": "^11.1.0", 29 | "eslint-plugin-promise": "^5.1.0", 30 | "eslint-plugin-standard": "^5.0.0", 31 | "tap": "^15.0.6" 32 | }, 33 | "funding": { 34 | "url": "https://github.com/sponsors/isaacs" 35 | }, 36 | "repository": "https://github.com/isaacs/json-stringify-nice", 37 | "files": [ 38 | "index.js" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /tap-snapshots/test/basic.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/basic.js TAP basic sorting operation with default 2-space indent > mix of objects and out of order keys 1`] = ` 9 | { 10 | "c": 1, 11 | "ch": 2, 12 | "d": 3, 13 | "y": "z", 14 | "yy": "a", 15 | "z": 1, 16 | "a": { 17 | "a": 2, 18 | "b": 1 19 | }, 20 | "obj": { 21 | "b": "x", 22 | "a": {} 23 | } 24 | } 25 | 26 | ` 27 | 28 | exports[`test/basic.js TAP dont be confused by empty strings or other falsey values > sort alphabetically 1`] = ` 29 | { 30 | "w": false, 31 | "x": 0, 32 | "y": null, 33 | "z": "" 34 | } 35 | 36 | ` 37 | 38 | exports[`test/basic.js TAP replacer function is used > replace a val with phone doggo 1`] = ` 39 | { 40 | "y": "z", 41 | "yy": "a", 42 | "z": 1, 43 | "a": { 44 | "b": 1, 45 | "hello": "📞 yes", 46 | "this is": "🐕", 47 | "a": { 48 | "hello": "📞 yes", 49 | "nested": true, 50 | "this is": "🐕" 51 | } 52 | }, 53 | "obj": { 54 | "b": "x", 55 | "a": { 56 | "hello": "📞 yes", 57 | "this is": "🐕" 58 | } 59 | } 60 | } 61 | 62 | ` 63 | 64 | exports[`test/basic.js TAP sort keys explicitly with a preference list > replace a val with preferences 1`] = ` 65 | { 66 | "z": 1, 67 | "yy": "a", 68 | "y": "z", 69 | "obj": { 70 | "b": "x", 71 | "a": {} 72 | }, 73 | "a": { 74 | "b": 1, 75 | "a": { 76 | "nested": true 77 | } 78 | } 79 | } 80 | 81 | ` 82 | 83 | exports[`test/basic.js TAP spaces can be set > boolean false 1`] = ` 84 | {"y":"z","yy":"a","z":1,"a":{"a":2,"b":1},"obj":{"b":"x","a":{}}} 85 | ` 86 | 87 | exports[`test/basic.js TAP spaces can be set > empty string 1`] = ` 88 | {"y":"z","yy":"a","z":1,"a":{"a":2,"b":1},"obj":{"b":"x","a":{}}} 89 | ` 90 | 91 | exports[`test/basic.js TAP spaces can be set > space face 1`] = ` 92 | { 93 | ^_^ "y": "z", 94 | ^_^ "yy": "a", 95 | ^_^ "z": 1, 96 | ^_^ "a": { 97 | ^_^ ^_^ "a": 2, 98 | ^_^ ^_^ "b": 1 99 | ^_^ }, 100 | ^_^ "obj": { 101 | ^_^ ^_^ "b": "x", 102 | ^_^ ^_^ "a": {} 103 | ^_^ } 104 | } 105 | 106 | ` 107 | 108 | exports[`test/basic.js TAP spaces can be set > tab 1`] = ` 109 | { 110 | "y": "z", 111 | "yy": "a", 112 | "z": 1, 113 | "a": { 114 | "a": 2, 115 | "b": 1 116 | }, 117 | "obj": { 118 | "b": "x", 119 | "a": {} 120 | } 121 | } 122 | 123 | ` 124 | 125 | exports[`test/basic.js TAP spaces can be set > the number 3 1`] = ` 126 | { 127 | "y": "z", 128 | "yy": "a", 129 | "z": 1, 130 | "a": { 131 | "a": 2, 132 | "b": 1 133 | }, 134 | "obj": { 135 | "b": "x", 136 | "a": {} 137 | } 138 | } 139 | 140 | ` 141 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const stringify = require('../') 3 | 4 | t.test('basic sorting operation with default 2-space indent', t => { 5 | t.plan(1) 6 | t.matchSnapshot(stringify({ 7 | c: 1, 8 | ch: 2, 9 | d: 3, 10 | z: 1, 11 | y: 'z', 12 | obj: { a: {}, b: 'x' }, 13 | a: { b: 1, a: 2}, 14 | yy: 'a', 15 | }), 'mix of objects and out of order keys') 16 | }) 17 | 18 | t.test('throws same error on cycles as JSON.stringify', t => { 19 | t.plan(1) 20 | const cycle = { a: { b: { c: {} } } } 21 | cycle.a.b.c = cycle.a 22 | try { 23 | JSON.stringify(cycle) 24 | } catch (builtinEr) { 25 | t.throws(() => stringify(cycle), builtinEr, 'same error as builtin') 26 | } 27 | }) 28 | 29 | t.test('spaces can be set', t => { 30 | t.plan(5) 31 | const obj = { 32 | z: 1, 33 | y: 'z', 34 | obj: { a: {}, b: 'x' }, 35 | a: { b: 1, a: 2}, 36 | yy: 'a', 37 | } 38 | t.matchSnapshot(stringify(obj, 0, '\t'), 'tab') 39 | t.matchSnapshot(stringify(obj, null, ' ^_^ '), 'space face') 40 | t.matchSnapshot(stringify(obj, false, 3), 'the number 3') 41 | t.matchSnapshot(stringify(obj, false, ''), 'empty string') 42 | t.matchSnapshot(stringify(obj, false, false), 'boolean false') 43 | }) 44 | 45 | t.test('replacer function is used', t => { 46 | t.plan(1) 47 | const obj = { 48 | z: 1, 49 | y: 'z', 50 | obj: { a: {}, b: 'x' }, 51 | a: { b: 1, a: { nested: true} }, 52 | yy: 'a', 53 | } 54 | const replacer = (key, val) => 55 | key === 'a' ? { hello: '📞 yes', 'this is': '🐕', ...val } 56 | : val 57 | t.matchSnapshot(stringify(obj, replacer), 'replace a val with phone doggo') 58 | }) 59 | 60 | t.test('sort keys explicitly with a preference list', t => { 61 | t.plan(1) 62 | const obj = { 63 | z: 1, 64 | y: 'z', 65 | obj: { a: {}, b: 'x' }, 66 | a: { b: 1, a: { nested: true} }, 67 | yy: 'a', 68 | } 69 | const preference = ['obj', 'z', 'yy'] 70 | t.matchSnapshot(stringify(obj, preference), 'replace a val with preferences') 71 | }) 72 | 73 | t.test('dont be confused by empty strings or other falsey values', t => { 74 | const obj = { 75 | z: '', 76 | y: null, 77 | w: false, 78 | x: 0, 79 | } 80 | t.matchSnapshot(stringify(obj), 'sort alphabetically') 81 | t.end() 82 | }) 83 | --------------------------------------------------------------------------------