diff --git a/.editorconfig b/.editorconfig index 98a761d..1c6314a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[{package.json,*.yml}] +[*.yml] indent_style = space indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 391f0a4..6313b56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1 @@ -* text=auto -*.js text eol=lf +* text=auto eol=lf diff --git a/.github/funding.yml b/.github/funding.yml new file mode 100644 index 0000000..6d802f9 --- /dev/null +++ b/.github/funding.yml @@ -0,0 +1,2 @@ +github: sindresorhus +tidelift: npm/stringify-object diff --git a/.github/security.md b/.github/security.md new file mode 100644 index 0000000..5358dc5 --- /dev/null +++ b/.github/security.md @@ -0,0 +1,3 @@ +# Security Policy + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..6a82b18 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - 18 + - 16 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index 3c3629e..239ecff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +yarn.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b18bae5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -sudo: false -language: node_js -node_js: - - '6' - - '4' diff --git a/contributing.md b/contributing.md index 0be6f2f..4c91d7b 100644 --- a/contributing.md +++ b/contributing.md @@ -1 +1 @@ -See the [contributing docs](https://github.com/yeoman/yeoman/blob/master/contributing.md) +See the [contributing docs](https://github.com/yeoman/yeoman/blob/main/contributing.md) diff --git a/index.js b/index.js index 04a90bc..370d777 100644 --- a/index.js +++ b/index.js @@ -1,123 +1,135 @@ -'use strict'; -const isRegexp = require('is-regexp'); -const isObj = require('is-obj'); -const getOwnEnumPropSymbols = require('get-own-enumerable-property-symbols'); +import isRegexp from 'is-regexp'; +import isObject from 'is-obj'; +import getOwnEnumerableKeys from 'get-own-enumerable-keys'; -module.exports = (val, opts, pad) => { +export default function stringifyObject(input, options, pad) { const seen = []; - return (function stringify(val, opts, pad) { - opts = opts || {}; - opts.indent = opts.indent || '\t'; - pad = pad || ''; + return (function stringify(input, options = {}, pad = '') { + const indent = options.indent || '\t'; let tokens; - - if (opts.inlineCharacterLimit === undefined) { + if (options.inlineCharacterLimit === undefined) { tokens = { - newLine: '\n', - newLineOrSpace: '\n', + newline: '\n', + newlineOrSpace: '\n', pad, - indent: pad + opts.indent + indent: pad + indent, }; } else { tokens = { - newLine: '@@__STRINGIFY_OBJECT_NEW_LINE__@@', - newLineOrSpace: '@@__STRINGIFY_OBJECT_NEW_LINE_OR_SPACE__@@', + newline: '@@__STRINGIFY_OBJECT_NEW_LINE__@@', + newlineOrSpace: '@@__STRINGIFY_OBJECT_NEW_LINE_OR_SPACE__@@', pad: '@@__STRINGIFY_OBJECT_PAD__@@', - indent: '@@__STRINGIFY_OBJECT_INDENT__@@' + indent: '@@__STRINGIFY_OBJECT_INDENT__@@', }; } const expandWhiteSpace = string => { - if (opts.inlineCharacterLimit === undefined) { + if (options.inlineCharacterLimit === undefined) { return string; } const oneLined = string - .replace(new RegExp(tokens.newLine, 'g'), '') - .replace(new RegExp(tokens.newLineOrSpace, 'g'), ' ') + .replace(new RegExp(tokens.newline, 'g'), '') + .replace(new RegExp(tokens.newlineOrSpace, 'g'), ' ') .replace(new RegExp(tokens.pad + '|' + tokens.indent, 'g'), ''); - if (oneLined.length <= opts.inlineCharacterLimit) { + if (oneLined.length <= options.inlineCharacterLimit) { return oneLined; } return string - .replace(new RegExp(tokens.newLine + '|' + tokens.newLineOrSpace, 'g'), '\n') + .replace(new RegExp(tokens.newline + '|' + tokens.newlineOrSpace, 'g'), '\n') .replace(new RegExp(tokens.pad, 'g'), pad) - .replace(new RegExp(tokens.indent, 'g'), pad + opts.indent); + .replace(new RegExp(tokens.indent, 'g'), pad + indent); }; - if (seen.indexOf(val) !== -1) { + if (seen.includes(input)) { return '"[Circular]"'; } - if (val === null || - val === undefined || - typeof val === 'number' || - typeof val === 'boolean' || - typeof val === 'function' || - typeof val === 'symbol' || - isRegexp(val)) { - return String(val); + if ( + input === null + || input === undefined + || typeof input === 'number' + || typeof input === 'boolean' + || typeof input === 'function' + || typeof input === 'symbol' + || isRegexp(input) + ) { + return String(input); } - if (val instanceof Date) { - return `new Date('${val.toISOString()}')`; + if (input instanceof Date) { + return `new Date('${input.toISOString()}')`; } - if (Array.isArray(val)) { - if (val.length === 0) { + if (Array.isArray(input)) { + if (input.length === 0) { return '[]'; } - seen.push(val); + seen.push(input); + + const returnValue = '[' + tokens.newline + input.map((element, i) => { + const eol = input.length - 1 === i ? tokens.newline : ',' + tokens.newlineOrSpace; + + let value = stringify(element, options, pad + indent); + if (options.transform) { + value = options.transform(input, i, value); + } - const ret = '[' + tokens.newLine + val.map((el, i) => { - const eol = val.length - 1 === i ? tokens.newLine : ',' + tokens.newLineOrSpace; - return tokens.indent + stringify(el, opts, pad + opts.indent) + eol; + return tokens.indent + value + eol; }).join('') + tokens.pad + ']'; - seen.pop(val); + seen.pop(); - return expandWhiteSpace(ret); + return expandWhiteSpace(returnValue); } - if (isObj(val)) { - const objKeys = Object.keys(val).concat(getOwnEnumPropSymbols(val)); + if (isObject(input)) { + let objectKeys = getOwnEnumerableKeys(input); - if (objKeys.length === 0) { + if (options.filter) { + // eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument + objectKeys = objectKeys.filter(element => options.filter(input, element)); + } + + if (objectKeys.length === 0) { return '{}'; } - seen.push(val); + seen.push(input); + + const returnValue = '{' + tokens.newline + objectKeys.map((element, index) => { + const eol = objectKeys.length - 1 === index ? tokens.newline : ',' + tokens.newlineOrSpace; + const isSymbol = typeof element === 'symbol'; + const isClassic = !isSymbol && /^[a-z$_][$\w]*$/i.test(element); + const key = isSymbol || isClassic ? element : stringify(element, options); - const ret = '{' + tokens.newLine + objKeys.map((el, i) => { - if (opts.filter && !opts.filter(val, el)) { - return ''; + let value = stringify(input[element], options, pad + indent); + if (options.transform) { + value = options.transform(input, element, value); } - const eol = objKeys.length - 1 === i ? tokens.newLine : ',' + tokens.newLineOrSpace; - const isSymbol = typeof el === 'symbol'; - const isClassic = !isSymbol && /^[a-z$_][a-z$_0-9]*$/i.test(el); - const key = isSymbol || isClassic ? el : stringify(el, opts); - return tokens.indent + String(key) + ': ' + stringify(val[el], opts, pad + opts.indent) + eol; + return tokens.indent + String(key) + ': ' + value + eol; }).join('') + tokens.pad + '}'; - seen.pop(val); + seen.pop(); - return expandWhiteSpace(ret); + return expandWhiteSpace(returnValue); } - val = String(val).replace(/[\r\n]/g, x => x === '\n' ? '\\n' : '\\r'); + input = input.replace(/\\/g, '\\\\'); + input = String(input).replace(/[\r\n]/g, x => x === '\n' ? '\\n' : '\\r'); - if (opts.singleQuotes === false) { - val = val.replace(/"/g, '\\"'); - return `"${val}"`; + if (options.singleQuotes === false) { + input = input.replace(/"/g, '\\"'); + return `"${input}"`; } - val = val.replace(/\\?'/g, '\\\''); - return `'${val}'`; - })(val, opts, pad); -}; + input = input.replace(/'/g, '\\\''); + return `'${input}'`; + })(input, options, pad); +} diff --git a/package.json b/package.json index 5d87311..64c49b2 100644 --- a/package.json +++ b/package.json @@ -1,43 +1,48 @@ { - "name": "stringify-object", - "version": "3.1.1", - "description": "Stringify an object/array like JSON.stringify just without all the double-quotes", - "license": "BSD-2-Clause", - "repository": "yeoman/stringify-object", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "sindresorhus.com" - }, - "engines": { - "node": ">=4" - }, - "scripts": { - "test": "xo && mocha" - }, - "files": [ - "index.js" - ], - "keywords": [ - "object", - "stringify", - "pretty", - "print", - "dump", - "format", - "type", - "json" - ], - "dependencies": { - "get-own-enumerable-property-symbols": "^1.0.1", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "devDependencies": { - "mocha": "*", - "xo": "*" - }, - "xo": { - "esnext": true - } + "name": "stringify-object", + "version": "5.0.0", + "description": "Stringify an object/array like JSON.stringify just without all the double-quotes", + "license": "BSD-2-Clause", + "repository": "yeoman/stringify-object", + "funding": "https://github.com/yeoman/stringify-object?sponsor=1", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "https://sindresorhus.com" + }, + "type": "module", + "exports": "./index.js", + "engines": { + "node": ">=14.16" + }, + "scripts": { + "test": "xo && ava" + }, + "files": [ + "index.js" + ], + "keywords": [ + "object", + "stringify", + "pretty", + "print", + "dump", + "format", + "type", + "json" + ], + "dependencies": { + "get-own-enumerable-keys": "^1.0.0", + "is-obj": "^3.0.0", + "is-regexp": "^3.1.0" + }, + "devDependencies": { + "ava": "^5.1.1", + "xo": "^0.53.1" + }, + "xo": { + "ignores": [ + "test/fixtures/*.js" + ] + } } diff --git a/readme.md b/readme.md index 284129f..82b5969 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# stringify-object [![Build Status](https://secure.travis-ci.org/yeoman/stringify-object.svg?branch=master)](http://travis-ci.org/yeoman/stringify-object) +# stringify-object > Stringify an object/array like JSON.stringify just without all the double-quotes @@ -6,26 +6,26 @@ Useful for when you want to get the string representation of an object in a form It also handles circular references and lets you specify quote type. - ## Install +```sh +npm install stringify-object ``` -$ npm install --save stringify-object -``` - ## Usage ```js -const stringifyObject = require('stringify-object'); +import stringifyObject from 'stringify-object'; -const obj = { +const object = { foo: 'bar', 'arr': [1, 2, 3], - nested: { hello: "world" } + nested: { + hello: "world" + } }; -const pretty = stringifyObject(obj, { +const pretty = stringifyObject(object, { indent: ' ', singleQuotes: false }); @@ -33,51 +33,90 @@ const pretty = stringifyObject(obj, { console.log(pretty); /* { - foo: "bar", - arr: [ - 1, - 2, - 3 - ], - nested: { - hello: "world" - } + foo: "bar", + arr: [ + 1, + 2, + 3 + ], + nested: { + hello: "world" + } } */ ``` - ## API -### stringifyObject(input, [options]) +### stringifyObject(input, options?) Circular references will be replaced with `"[Circular]"`. +Object keys are only quoted when necessary, for example, `{'foo-bar': true}`. + #### input -Type: `Object` `Array` +Type: `object | Array` #### options +Type: `object` + ##### indent -Type: `string`
-Default: `'\t'` +Type: `string`\ +Default: `\t` Preferred indentation. ##### singleQuotes -Type: `boolean`
+Type: `boolean`\ Default: `true` Set to false to get double-quoted strings. -##### filter(obj, prop) +##### filter(object, property) Type: `Function` -Expected to return a `boolean` of whether to keep the object. +Expected to return a `boolean` of whether to include the property `property` of the object `object` in the output. + +##### transform(object, property, originalResult) + +Type: `Function`\ +Default: `undefined` + +Expected to return a `string` that transforms the string that resulted from stringifying `object[property]`. This can be used to detect special types of objects that need to be stringified in a particular way. The `transform` function might return an alternate string in this case, otherwise returning the `originalResult`. + +Here's an example that uses the `transform` option to mask fields named "password": + +```js +import stringifyObject from 'stringify-object'; + +const object = { + user: 'becky', + password: 'secret' +}; + +const pretty = stringifyObject(object, { + transform: (object, property, originalResult) => { + if (property === 'password') { + return originalResult.replace(/\w/g, '*'); + } + + return originalResult; + } +}); + +console.log(pretty); +/* +{ + user: 'becky', + password: '******' +} +*/ +``` ##### inlineCharacterLimit @@ -88,13 +127,17 @@ When set, will inline values up to `inlineCharacterLimit` length for the sake of For example, given the example at the top of the README: ```js -const obj = { +import stringifyObject from 'stringify-object'; + +const object = { foo: 'bar', 'arr': [1, 2, 3], - nested: { hello: "world" } + nested: { + hello: "world" + } }; -const pretty = stringifyObject(obj, { +const pretty = stringifyObject(object, { indent: ' ', singleQuotes: false, inlineCharacterLimit: 12 @@ -103,18 +146,13 @@ const pretty = stringifyObject(obj, { console.log(pretty); /* { - foo: "bar", - arr: [1, 2, 3], - nested: { - hello: "world" - } + foo: "bar", + arr: [1, 2, 3], + nested: { + hello: "world" + } } */ ``` As you can see, `arr` was printed as a one-liner because its string was shorter than 12 characters. - - -## License - -[BSD license](http://opensource.org/licenses/bsd-license.php) © Yeoman Team diff --git a/test.js b/test.js deleted file mode 100644 index 804ed97..0000000 --- a/test.js +++ /dev/null @@ -1,146 +0,0 @@ -/* eslint-env mocha */ -'use strict'; -const fs = require('fs'); -const assert = require('assert'); -const stringifyObject = require('./'); - -it('should stringify an object', () => { - /* eslint-disable quotes, object-shorthand */ - const obj = { - foo: 'bar \'bar\'', - foo2: [ - 'foo', - 'bar', - { - foo: "bar 'bar'" - } - ], - 'foo-foo': 'bar', - '2foo': 'bar', - '@#': "bar", - $el: 'bar', - _private: 'bar', - number: 1, - boolean: true, - date: new Date("2014-01-29T22:41:05.665Z"), - escapedString: "\"\"", - null: null, - undefined: undefined, - function: function () {}, - regexp: /./, - NaN: NaN, - Infinity: Infinity, - newlines: "foo\nbar\r\nbaz", - [Symbol()]: Symbol(), // eslint-disable-line symbol-description - [Symbol('foo')]: Symbol('foo'), - [Symbol.for('foo')]: Symbol.for('foo') - }; - /* eslint-enable */ - - obj.circular = obj; - - const actual = stringifyObject(obj, { - indent: ' ', - singleQuotes: false - }); - - assert.equal(actual + '\n', fs.readFileSync('fixture.js', 'utf8')); - assert.equal( - stringifyObject({foo: 'a \' b \' c \\\' d'}, {singleQuotes: true}), - '{\n\tfoo: \'a \\\' b \\\' c \\\' d\'\n}' - ); -}); - -it('should not detect reused object values as circular reference', () => { - const val = {val: 10}; - const obj = {foo: val, bar: val}; - assert.equal(stringifyObject(obj), '{\n\tfoo: {\n\t\tval: 10\n\t},\n\tbar: {\n\t\tval: 10\n\t}\n}'); -}); - -it('should not detect reused array values as false circular references', () => { - const val = [10]; - const obj = {foo: val, bar: val}; - assert.equal(stringifyObject(obj), '{\n\tfoo: [\n\t\t10\n\t],\n\tbar: [\n\t\t10\n\t]\n}'); -}); - -it('considering filter option to stringify an object', () => { - const val = {val: 10}; - const obj = {foo: val, bar: val}; - const actual = stringifyObject(obj, { - filter: (obj, prop) => prop !== 'foo' - }); - assert.equal(actual, '{\n\tbar: {\n\t\tval: 10\n\t}\n}'); -}); - -it('should not crash with circular references in arrays', () => { - const array = []; - array.push(array); - assert.doesNotThrow(() => { - stringifyObject(array); - }); - - const nestedArray = [[]]; - nestedArray[0][0] = nestedArray; - assert.doesNotThrow(() => { - stringifyObject(nestedArray); - }); -}); - -it('should handle circular references in arrays', () => { - const array2 = []; - const array = [array2]; - array2[0] = array2; - - assert.doesNotThrow(() => { - stringifyObject(array); - }); -}); - -it('should stringify complex circular arrays', () => { - const array = [[[]]]; - array[0].push(array); - array[0][0].push(array); - array[0][0].push(10); - array[0][0][0] = array; - assert.equal(stringifyObject(array), '[\n\t[\n\t\t[\n\t\t\t"[Circular]",\n\t\t\t10\n\t\t],\n\t\t"[Circular]"\n\t]\n]'); -}); - -it('allows short objects to be one-lined', () => { - const object = {id: 8, name: 'Jane'}; - - assert.equal(stringifyObject(object), '{\n\tid: 8,\n\tname: \'Jane\'\n}'); - assert.equal(stringifyObject(object, {inlineCharacterLimit: 21}), '{id: 8, name: \'Jane\'}'); - assert.equal(stringifyObject(object, {inlineCharacterLimit: 20}), '{\n\tid: 8,\n\tname: \'Jane\'\n}'); -}); - -it('allows short arrays to be one-lined', () => { - const array = ['foo', {id: 8, name: 'Jane'}, 42]; - - assert.equal(stringifyObject(array), '[\n\t\'foo\',\n\t{\n\t\tid: 8,\n\t\tname: \'Jane\'\n\t},\n\t42\n]'); - assert.equal(stringifyObject(array, {inlineCharacterLimit: 34}), '[\'foo\', {id: 8, name: \'Jane\'}, 42]'); - assert.equal(stringifyObject(array, {inlineCharacterLimit: 33}), '[\n\t\'foo\',\n\t{id: 8, name: \'Jane\'},\n\t42\n]'); -}); - -it('does not mess up indents for complex objects', () => { - const object = { - arr: [1, 2, 3], - nested: {hello: 'world'} - }; - - assert.equal(stringifyObject(object), '{\n\tarr: [\n\t\t1,\n\t\t2,\n\t\t3\n\t],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}'); - assert.equal(stringifyObject(object, {inlineCharacterLimit: 12}), '{\n\tarr: [1, 2, 3],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}'); -}); - -it('handles non-plain object', () => { - assert.notStrictEqual(stringifyObject(fs.statSync(__filename)), '[object Object]'); -}); - -it('should not stringify non-enumerable symbols', () => { - const obj = { - [Symbol('for enumerable key')]: undefined - }; - const symbol = Symbol('for non-enumerable key'); - Object.defineProperty(obj, symbol, {enumerable: false}); - - assert.equal(stringifyObject(obj), '{\n\tSymbol(for enumerable key): undefined\n}'); -}); diff --git a/fixture.js b/test/fixtures/object.js similarity index 94% rename from fixture.js rename to test/fixtures/object.js index a34acb4..4586040 100644 --- a/fixture.js +++ b/test/fixtures/object.js @@ -18,7 +18,7 @@ escapedString: "\"\"", null: null, undefined: undefined, - function: function () {}, + fn: function fn() {}, regexp: /./, NaN: NaN, Infinity: Infinity, diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..c2116f6 --- /dev/null +++ b/test/index.js @@ -0,0 +1,203 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import test from 'ava'; +import stringifyObject from '../index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +test('stringify an object', t => { + /* eslint-disable quotes, object-shorthand */ + const object = { + foo: 'bar \'bar\'', + foo2: [ + 'foo', + 'bar', + { + foo: "bar 'bar'", + }, + ], + 'foo-foo': 'bar', + '2foo': 'bar', + '@#': "bar", + $el: 'bar', + _private: 'bar', + number: 1, + boolean: true, + date: new Date("2014-01-29T22:41:05.665Z"), + escapedString: "\"\"", + null: null, + undefined: undefined, + fn: function fn() {}, // eslint-disable-line func-names + regexp: /./, + NaN: Number.NaN, + Infinity: Number.POSITIVE_INFINITY, + newlines: "foo\nbar\r\nbaz", + [Symbol()]: Symbol(), // eslint-disable-line symbol-description + [Symbol('foo')]: Symbol('foo'), + [Symbol.for('foo')]: Symbol.for('foo'), + }; + /* eslint-enable */ + + object.circular = object; + + const actual = stringifyObject(object, { + indent: ' ', + singleQuotes: false, + }); + + t.is(actual + '\n', fs.readFileSync(path.resolve(__dirname, 'fixtures/object.js'), 'utf8')); + t.is( + stringifyObject({foo: 'a \' b \' c \\\' d'}, {singleQuotes: true}), + '{\n\tfoo: \'a \\\' b \\\' c \\\\\\\' d\'\n}', + ); +}); + +test('string escaping works properly', t => { + t.is(stringifyObject('\\', {singleQuotes: true}), '\'\\\\\''); // \ + t.is(stringifyObject('\\\'', {singleQuotes: true}), '\'\\\\\\\'\''); // \' + t.is(stringifyObject('\\"', {singleQuotes: true}), '\'\\\\"\''); // \" + t.is(stringifyObject('\\', {singleQuotes: false}), '"\\\\"'); // \ + t.is(stringifyObject('\\\'', {singleQuotes: false}), '"\\\\\'"'); // \' + t.is(stringifyObject('\\"', {singleQuotes: false}), '"\\\\\\""'); // \" + /* eslint-disable no-eval */ + t.is(eval(stringifyObject('\\\'')), '\\\''); + t.is(eval(stringifyObject('\\\'', {singleQuotes: false})), '\\\''); + /* eslint-enable */ + // Regression test for #40 + t.is(stringifyObject("a'a"), '\'a\\\'a\''); // eslint-disable-line quotes +}); + +test('detect reused object values as circular reference', t => { + const value = {val: 10}; + const object = {foo: value, bar: value}; + t.is(stringifyObject(object), '{\n\tfoo: {\n\t\tval: 10\n\t},\n\tbar: {\n\t\tval: 10\n\t}\n}'); +}); + +test('detect reused array values as false circular references', t => { + const value = [10]; + const object = {foo: value, bar: value}; + t.is(stringifyObject(object), '{\n\tfoo: [\n\t\t10\n\t],\n\tbar: [\n\t\t10\n\t]\n}'); +}); + +test('considering filter option to stringify an object', t => { + const value = {val: 10}; + const object = {foo: value, bar: value}; + const actual = stringifyObject(object, { + filter: (object, prop) => prop !== 'foo', + }); + t.is(actual, '{\n\tbar: {\n\t\tval: 10\n\t}\n}'); + + const actual2 = stringifyObject(object, { + filter: (object, prop) => prop !== 'bar', + }); + t.is(actual2, '{\n\tfoo: {\n\t\tval: 10\n\t}\n}'); + + const actual3 = stringifyObject(object, { + filter: (object, prop) => prop !== 'val' && prop !== 'bar', + }); + t.is(actual3, '{\n\tfoo: {}\n}'); +}); + +test('allows an object to be transformed', t => { + const object = { + foo: { + val: 10, + }, + bar: 9, + baz: [8], + }; + + const actual = stringifyObject(object, { + transform(object, prop, result) { + if (prop === 'val') { + return String(object[prop] + 1); + } + + if (prop === 'bar') { + return '\'' + result + 'L\''; + } + + if (object[prop] === 8) { + return 'LOL'; + } + + return result; + }, + }); + + t.is(actual, '{\n\tfoo: {\n\t\tval: 11\n\t},\n\tbar: \'9L\',\n\tbaz: [\n\t\tLOL\n\t]\n}'); +}); + +test('doesn\'t crash with circular references in arrays', t => { + const array = []; + array.push(array); + t.notThrows(() => { + stringifyObject(array); + }); + + const nestedArray = [[]]; + nestedArray[0][0] = nestedArray; + t.notThrows(() => { + stringifyObject(nestedArray); + }); +}); + +test('handle circular references in arrays', t => { + const array2 = []; + const array = [array2]; + array2[0] = array2; + + t.notThrows(() => { + stringifyObject(array); + }); +}); + +test('stringify complex circular arrays', t => { + const array = [[[]]]; + array[0].push(array); + array[0][0].push(array, 10); + array[0][0][0] = array; + t.is(stringifyObject(array), '[\n\t[\n\t\t[\n\t\t\t"[Circular]",\n\t\t\t10\n\t\t],\n\t\t"[Circular]"\n\t]\n]'); +}); + +test('allows short objects to be one-lined', t => { + const object = {id: 8, name: 'Jane'}; + + t.is(stringifyObject(object), '{\n\tid: 8,\n\tname: \'Jane\'\n}'); + t.is(stringifyObject(object, {inlineCharacterLimit: 21}), '{id: 8, name: \'Jane\'}'); + t.is(stringifyObject(object, {inlineCharacterLimit: 20}), '{\n\tid: 8,\n\tname: \'Jane\'\n}'); +}); + +test('allows short arrays to be one-lined', t => { + const array = ['foo', {id: 8, name: 'Jane'}, 42]; + + t.is(stringifyObject(array), '[\n\t\'foo\',\n\t{\n\t\tid: 8,\n\t\tname: \'Jane\'\n\t},\n\t42\n]'); + t.is(stringifyObject(array, {inlineCharacterLimit: 34}), '[\'foo\', {id: 8, name: \'Jane\'}, 42]'); + t.is(stringifyObject(array, {inlineCharacterLimit: 33}), '[\n\t\'foo\',\n\t{id: 8, name: \'Jane\'},\n\t42\n]'); +}); + +test('does not mess up indents for complex objects', t => { + const object = { + arr: [1, 2, 3], + nested: {hello: 'world'}, + }; + + t.is(stringifyObject(object), '{\n\tarr: [\n\t\t1,\n\t\t2,\n\t\t3\n\t],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}'); + t.is(stringifyObject(object, {inlineCharacterLimit: 12}), '{\n\tarr: [1, 2, 3],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}'); +}); + +test('handles non-plain object', t => { + // TODO: It should work without `fileURLToPath` but currently it throws for an unknown reason. + t.not(stringifyObject(fs.statSync(fileURLToPath(import.meta.url))), '[object Object]'); +}); + +test('don\'t stringify non-enumerable symbols', t => { + const object = { + [Symbol('for enumerable key')]: undefined, + }; + const symbol = Symbol('for non-enumerable key'); + Object.defineProperty(object, symbol, {enumerable: false}); + + t.is(stringifyObject(object), '{\n\tSymbol(for enumerable key): undefined\n}'); +});