From 435db27794a0936a5eb8ba51d4ca81e611a7ccb3 Mon Sep 17 00:00:00 2001 From: commenthol Date: Mon, 27 Jan 2020 20:08:16 +0100 Subject: [PATCH 1/2] chore: bump deps --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 1ba2a7a..8145c29 100644 --- a/package.json +++ b/package.json @@ -52,17 +52,17 @@ }, "dependencies": {}, "devDependencies": { - "@babel/cli": "^7.7.5", - "@babel/core": "^7.7.5", - "@babel/preset-env": "^7.7.6", - "eslint": "^6.7.2", + "@babel/cli": "^7.8.3", + "@babel/core": "^7.8.3", + "@babel/preset-env": "^7.8.3", + "eslint": "^6.8.0", "eslint-config-standard": "^14.1.0", - "eslint-plugin-import": "^2.19.1", - "eslint-plugin-node": "^10.0.0", + "eslint-plugin-import": "^2.20.0", + "eslint-plugin-node": "^11.0.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "mocha": "^6.2.2", - "nyc": "^14.1.1", + "mocha": "^7.0.1", + "nyc": "^15.0.0", "rimraf": "^3.0.0" }, "engines": { From 661c2fc2845f1075442c200312a3ebd1cdb9b2bd Mon Sep 17 00:00:00 2001 From: commenthol Date: Mon, 27 Jan 2020 20:15:03 +0100 Subject: [PATCH 2/2] feat: add support for Set Map - refactor: code - refactor: tests - chore: add mocha.opts to package.json - docs: update README.md --- README.md | 50 +++---- package.json | 5 +- src/index.js | 159 +++++++++++++-------- src/internal/utils.js | 69 ++------- test/fixtures.js | 189 ------------------------- test/index.test.js | 315 +++++++++++++++++++++++++++++++++++------- test/mocha.opts | 2 - 7 files changed, 393 insertions(+), 396 deletions(-) delete mode 100644 test/fixtures.js delete mode 100644 test/mocha.opts diff --git a/README.md b/README.md index b2eba2e..da03669 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,8 @@ The following Objects are supported - Int16Array, Uint16Array - Int32Array, Uint32Array, Float32Array - Float64Array - -> **Note:** Version >3.0.0 has moved the serializeToModule method into its own -> package at [serialize-to-module][] -> -> Migrating from 2.x to 3.x for serialize: -> ```js -> // v2.x -> const serialize = require('serialize-to-js').serialize -> // >v3.x -> const serialize = require('serialize-to-js') -> ``` +- Set +- Map ## Table of Contents @@ -40,7 +31,6 @@ The following Objects are supported * [Methods](#methods) * [serialize](#serialize) - * [serializeToModule](#serializetomodule) * [Contribution and License Agreement](#contribution-and-license-agreement) * [License](#license) @@ -57,8 +47,8 @@ serializes an object to javascript #### Example - serializing regex, date, buffer, ... ```js -var serialize = require('serialize-to-js') -var obj = { +const serialize = require('serialize-to-js') +const obj = { str: '', num: 3.1415, bool: true, @@ -68,10 +58,17 @@ var obj = { arr: [1, '2'], regexp: /^test?$/, date: new Date(), - buffer: Buffer.from('data'), + buffer: new Buffer('data'), + set: new Set([1, 2, 3]), + map: new Map([['a': 1],['b': 2]]) } console.log(serialize(obj)) -// > {str: "\u003Cscript\u003Evar a = 0 \u003E 1\u003C\u002Fscript\u003E", num: 3.1415, bool: true, nil: null, undef: undefined, obj: {foo: "bar"}, arr: [1, "2"], regexp: /^test?$/, date: new Date("2016-04-15T16:22:52.009Z"), buffer: new Buffer('ZGF0YQ==', 'base64')} +//> '{str: "\u003Cscript\u003Evar a = 0 \u003E 1\u003C\u002Fscript\u003E", +//> num: 3.1415, bool: true, nil: null, undef: undefined, +//> obj: {foo: "bar"}, arr: [1, "2"], regexp: new RegExp("^test?$", ""), +//> date: new Date("2019-12-29T10:37:36.613Z"), +//> buffer: Buffer.from("ZGF0YQ==", "base64"), set: new Set([1, 2, 3]), +//> map: new Map([["a", 1], ["b", 2]])}' ``` #### Example - serializing while respecting references @@ -89,24 +86,14 @@ console.log(opts.references); **Parameters** -**source**: `Object | Array | function | Any`, source to serialize - -**opts**: `Object`, options - -**opts.ignoreCircular**: `Boolean`, ignore circular objects - -**opts.reference**: `Boolean`, reference instead of a copy (requires post-processing of opts.references) - -**opts.unsafe**: `Boolean`, do not escape chars `<>/` - +**source**: `Object | Array | function | Any`, source to serialize +**opts**: `Object`, options +**opts.ignoreCircular**: `Boolean`, ignore circular objects +**opts.reference**: `Boolean`, reference instead of a copy (requires post-processing of opts.references) +**opts.unsafe**: `Boolean`, do not escape chars `<>/` **Returns**: `String`, serialized representation of `source` -### serializeToModule - -The `serializeToModule` has been moved to it\`s own repository at [serialize-to-module][]. - - ## Contribution and License Agreement If you contribute code to this project, you are implicitly allowing your @@ -121,4 +108,3 @@ Copyright (c) 2016- commenthol (MIT License) See [LICENSE][] for more info. [LICENSE]: ./LICENSE -[serialize-to-module]: https://npmjs.com/package/serialize-to-module diff --git a/package.json b/package.json index 8145c29..8ecbf80 100644 --- a/package.json +++ b/package.json @@ -68,5 +68,8 @@ "engines": { "node": ">=4.0.0" }, - "maintainers": "commenthol " + "maintainers": "commenthol ", + "mocha": { + "check-leaks": true + } } diff --git a/src/index.js b/src/index.js index 5fdf5b5..60168f6 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,7 @@ const Ref = require('./internal/reference') * serializes an object to javascript * * @example serializing regex, date, buffer, ... - * const serialize = require('serialize-to-js').serialize; + * const serialize = require('serialize-to-js') * const obj = { * str: '', * num: 3.1415, @@ -25,12 +25,19 @@ const Ref = require('./internal/reference') * regexp: /^test?$/, * date: new Date(), * buffer: new Buffer('data'), + * set: new Set([1, 2, 3]), + * map: new Map([['a': 1],['b': 2]]) * } * console.log(serialize(obj)) - * // > {str: "\u003Cscript\u003Evar a = 0 \u003E 1\u003C\u002Fscript\u003E", num: 3.1415, bool: true, nil: null, undef: undefined, obj: {foo: "bar"}, arr: [1, "2"], regexp: /^test?$/, date: new Date("2016-04-15T16:22:52.009Z"), buffer: new Buffer('ZGF0YQ==', 'base64')} + * //> '{str: "\u003Cscript\u003Evar a = 0 \u003E 1\u003C\u002Fscript\u003E", + * //> num: 3.1415, bool: true, nil: null, undef: undefined, + * //> obj: {foo: "bar"}, arr: [1, "2"], regexp: new RegExp("^test?$", ""), + * //> date: new Date("2019-12-29T10:37:36.613Z"), + * //> buffer: Buffer.from("ZGF0YQ==", "base64"), set: new Set([1, 2, 3]), + * //> map: new Map([["a", 1], ["b", 2]])}' * * @example serializing while respecting references - * const serialize = require('serialize-to-js').serialize; + * const serialize = require('serialize-to-js') * const obj = { object: { regexp: /^test?$/ } }; * obj.reference = obj.object; * const opts = { reference: true }; @@ -46,79 +53,109 @@ const Ref = require('./internal/reference') * @param {Boolean} opts.unsafe - do not escape chars `<>/` * @return {String} serialized representation of `source` */ -function serialize (source, opts) { - let type - +function serialize (source, opts = {}) { opts = opts || {} - if (!opts._visited) { - opts._visited = [] - } - if (!opts._refs) { - opts.references = [] - opts._refs = new Ref(opts.references, opts) - } - if (utils.isNull(source)) { - return 'null' - } else if (Array.isArray(source)) { - const tmp = source.map(item => serialize(item, opts)) - return `[${tmp.join(', ')}]` - } else if (utils.isFunction(source)) { - // serializes functions only in unsafe mode! - const _tmp = source.toString() - const tmp = opts.unsafe ? _tmp : utils.saferFunctionString(_tmp, opts) - // append function to es6 function within obj - return !/^\s*(function|\([^)]*?\)\s*=>)/m.test(tmp) ? 'function ' + tmp : tmp - } else if (utils.isObject(source)) { - if (utils.isRegExp(source)) { - return `new RegExp(${utils.quote(source.source, opts)}, "${source.flags}")` - } else if (utils.isDate(source)) { - return `new Date(${utils.quote(source.toJSON(), opts)})` - } else if (utils.isError(source)) { - return `new Error(${utils.quote(source.message, opts)})` - } else if (utils.isBuffer(source)) { - // check for buffer first otherwise tests fail on node@4.4 - // looks like buffers are accidentially detected as typed arrays - return `Buffer.from('${source.toString('base64')}', 'base64')` - } else if ((type = utils.isTypedArray(source))) { - const tmp = [] - for (let i = 0; i < source.length; i++) { - tmp.push(source[i]) + const visited = new Set() + opts.references = [] + const refs = new Ref(opts.references, opts) + + function stringify (source, opts) { + const type = utils.toType(source) + + if (visited.has(source)) { + if (opts.ignoreCircular) { + switch (type) { + case 'Array': + return '[/*[Circular]*/]' + case 'Object': + return '{/*[Circular]*/}' + default: + return 'undefined /*[Circular]*/' + } + } else { + throw new Error('can not convert circular structures.') + } + } + + switch (type) { + case 'Null': + return 'null' + case 'String': + return utils.quote(source, opts) + case 'Function': { + const _tmp = source.toString() + const tmp = opts.unsafe ? _tmp : utils.saferFunctionString(_tmp, opts) + // append function to es6 function within obj + return !/^\s*(function|\([^)]*?\)\s*=>)/m.test(tmp) ? 'function ' + tmp : tmp + } + case 'RegExp': + return `new RegExp(${utils.quote(source.source, opts)}, "${source.flags}")` + case 'Date': + if (utils.isInvalidDate(source)) return 'new Date("Invalid Date")' + return `new Date(${utils.quote(source.toJSON(), opts)})` + case 'Error': + return `new Error(${utils.quote(source.message, opts)})` + case 'Buffer': + return `Buffer.from("${source.toString('base64')}", "base64")` + case 'Array': { + visited.add(source) + const tmp = source.map(item => stringify(item, opts)) + visited.delete(source) + return `[${tmp.join(', ')}]` + } + case 'Int8Array': + case 'Uint8Array': + case 'Uint8ClampedArray': + case 'Int16Array': + case 'Uint16Array': + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + case 'Float64Array': { + const tmp = [] + for (let i = 0; i < source.length; i++) { + tmp.push(source[i]) + } + return `new ${type}([${tmp.join(', ')}])` + } + case 'Set': { + visited.add(source) + const tmp = Array.from(source).map(item => stringify(item, opts)) + visited.delete(source) + return `new ${type}([${tmp.join(', ')}])` } - return `new ${type}([${tmp.join(', ')}])` - } else { - const tmp = [] - // copy properties if not circular - if (!~opts._visited.indexOf(source)) { - opts._visited.push(source) + case 'Map': { + visited.add(source) + const tmp = Array.from(source).map(([key, value]) => `[${stringify(key, opts)}, ${stringify(value, opts)}]`) + visited.delete(source) + return `new ${type}([${tmp.join(', ')}])` + } + case 'Object': { + visited.add(source) + const tmp = [] for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (opts.reference && utils.isObject(source[key])) { - opts._refs.push(key) - if (!opts._refs.hasReference(source[key])) { - tmp.push(Ref.wrapkey(key, opts) + ': ' + serialize(source[key], opts)) + refs.push(key) + if (!refs.hasReference(source[key])) { + tmp.push(Ref.wrapkey(key, opts) + ': ' + stringify(source[key], opts)) } - opts._refs.pop() + refs.pop() } else { - tmp.push(Ref.wrapkey(key, opts) + ': ' + serialize(source[key], opts)) + tmp.push(Ref.wrapkey(key, opts) + ': ' + stringify(source[key], opts)) } } } - opts._visited.pop() + visited.delete(source) return `{${tmp.join(', ')}}` - } else { - if (opts.ignoreCircular) { - return '{/*[Circular]*/}' - } else { - throw new Error('can not convert circular structures.') - } } + default: + return '' + source } - } else if (utils.isString(source)) { - return utils.quote(source, opts) - } else { - return '' + source } + + return stringify(source, opts) } module.exports = serialize diff --git a/src/internal/utils.js b/src/internal/utils.js index 3f0fb97..377b727 100644 --- a/src/internal/utils.js +++ b/src/internal/utils.js @@ -38,65 +38,23 @@ function saferFunctionString (str, opts) { : str.replace(/(<\/?)([a-z][^>]*?>)/ig, (m, m1, m2) => safeString(m1) + m2) } -function objectToString (o) { - return Object.prototype.toString.call(o) -} - -function toType (o) { - const type = objectToString(o) - return type.substring(8, type.length - 1) -} - -function isString (arg) { - return typeof arg === 'string' -} - -function isNull (arg) { - return arg === null -} - -function isRegExp (re) { - return isObject(re) && objectToString(re) === '[object RegExp]' -} - function isObject (arg) { return typeof arg === 'object' && arg !== null } -function isDate (d) { - return isObject(d) && objectToString(d) === '[object Date]' -} - -function isError (e) { - return isObject(e) && - (objectToString(e) === '[object Error]' || e instanceof Error) -} - -function isFunction (arg) { - return typeof arg === 'function' -} - function isBuffer (arg) { return arg instanceof Buffer } -const TYPED_ARRAYS = [ - 'Int8Array', - 'Uint8Array', - 'Uint8ClampedArray', - 'Int16Array', - 'Uint16Array', - 'Int32Array', - 'Uint32Array', - 'Float32Array', - 'Float64Array' -] +function isInvalidDate (arg) { + return isNaN(arg.getTime()) +} -function isTypedArray (arg) { - const type = toType(arg) - if (TYPED_ARRAYS.indexOf(type) !== -1) { - return type - } +function toType (o) { + const _type = Object.prototype.toString.call(o) + const type = _type.substring(8, _type.length - 1) + if (type === 'Uint8Array' && isBuffer(o)) return 'Buffer' + return type } module.exports = { @@ -104,13 +62,8 @@ module.exports = { unsafeString, quote, saferFunctionString, - isString, - isNull, - isRegExp, - isObject, - isDate, - isError, - isFunction, isBuffer, - isTypedArray + isObject, + isInvalidDate, + toType } diff --git a/test/fixtures.js b/test/fixtures.js deleted file mode 100644 index 368495d..0000000 --- a/test/fixtures.js +++ /dev/null @@ -1,189 +0,0 @@ -'use strict' - -const isBrowser = (typeof window !== 'undefined') - -function log (arg) { - console.log(JSON.stringify(arg)) -} - -const isLessV12 = parseInt(process.versions.node.split('.')[0]) < 12 - -module.exports = { - string: [ - "string's\n\"new\" line", - '"string\'s\\n\\"new\\" line"' - ], - 'string with unsafe characters': [ - '', - '"\\u003Cscript type=\\"application\\u002Fjavascript\\"\\u003E\\u2028\\u2029\\nvar a = 0;\\nvar b = 1; a \\u003E 1;\\n\\u003C\\u002Fscript\\u003E"', - // unsafe ... - '""' - ], - number: [ - 3.1415, - '3.1415' - ], - boolean: [ - true, - 'true' - ], - undefined: [ - undefined, - 'undefined' - ], - null: [ - null, - 'null' - ], - regex: [ - /test(?:it)?/ig, - 'new RegExp("test(?:it)?", "gi")' - ], - object: [ - { a: 1, b: 2 }, - '{a: 1, b: 2}' - ], - 'empty object': [ - {}, - '{}' - ], - 'object with backslash': [ - { backslash: '\\' }, - '{backslash: "\\u005C"}' - ], - 'object of primitives': [ - { - one: true, - two: false, - 'thr-ee': undefined, - four: 1, - 5: 3.1415, - six: -17, - 'se ven': 'string' - }, - '{"5": 3.1415, one: true, two: false, "thr-ee": undefined, four: 1, six: -17, "se ven": "string"}' - ], - function: [ - log, - log.toString() - ], - 'arrow function': [ - (a) => a + 1, - '(a) => a + 1' - ], - 'shorthand method': [ - { key(a) { return a + 1 } }, // eslint-disable-line - '{key: function key(a) { return a + 1 }}' - ], - 'arrow function in object': [ - { key: (a) => a + 1 }, - '{key: (a) => a + 1}' - ], - date: [ - new Date(24 * 12 * 3600000), - 'new Date("1970-01-13T00:00:00.000Z")' - ], - error: [ - new Error('error'), - 'new Error("error")' - ], - 'empty error': [ - new Error(), - 'new Error()' - ], - 'array of primitives': [ - [true, false, undefined, 1, 3.1415, -17, 'string'], - '[true, false, undefined, 1, 3.1415, -17, "string"]' - ], - Int8Array: [ - new Int8Array([1, 2, 3, 4, 5]), - 'new Int8Array([1, 2, 3, 4, 5])' - ], - Uint8Array: [ - new Uint8Array([1, 2, 3, 4, 5]), - 'new Uint8Array([1, 2, 3, 4, 5])' - ], - Uint8ClampedArray: [ - new Uint8ClampedArray([1, 2, 3, 4, 5]), - 'new Uint8ClampedArray([1, 2, 3, 4, 5])' - ], - Int16Array: [ - new Int16Array([-1, 0, 2, 3, 4, 5]), - 'new Int16Array([-1, 0, 2, 3, 4, 5])' - ], - Uint16Array: [ - new Uint16Array([1, 2, 3, 4, 5]), - 'new Uint16Array([1, 2, 3, 4, 5])' - ], - Int32Array: [ - new Int32Array([1, 2, 3, 4, 5]), - 'new Int32Array([1, 2, 3, 4, 5])' - ], - Uint32Array: [ - new Uint32Array([1, 2, 3, 4, 5]), - 'new Uint32Array([1, 2, 3, 4, 5])' - ], - Float32Array: [ - new Float32Array([1e10, 2000000, 3.1415, -4.9e2, 5]), - 'new Float32Array([10000000000, 2000000, 3.1414999961853027, -490, 5])' - ], - Float64Array: [ - new Float64Array([1e12, 2000000, 3.1415, -4.9e2, 5]), - 'new Float64Array([1000000000000, 2000000, 3.1415, -490, 5])' - ], - regexXss: [ - /[', + '"\\u003Cscript type=\\"application\\u002Fjavascript\\"\\u003E\\u2028\\u2029\\nvar a = 0;\\nvar b = 1; a \\u003E 1;\\n\\u003C\\u002Fscript\\u003E"' + ) + test('string with all unsafe characters', '<>\\\\ \t\n/', '"\\u003C\\u003E\\u005C\\u005C \\t\\n\\u002F"') + test('empty object', {}, '{}') + test('object', { a: 1, b: 2 }, '{a: 1, b: 2}') + test('object with backslash', { backslash: '\\' }, '{backslash: "\\u005C"}') + test('object of primitives', + { one: true, two: false, 'thr-ee': undefined, four: 1, 5: 3.1415, six: -17, 'se ven': 'string' }, + '{"5": 3.1415, one: true, two: false, "thr-ee": undefined, four: 1, six: -17, "se ven": "string"}' + ) + test('object with unsafe property name', + { "', + '""', + true + ) + test('object with unsafe property name', + { "