Skip to content

Commit

Permalink
Add support for serializing ES6 sets & maps (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry authored and okuryu committed Apr 16, 2019
1 parent 35f6480 commit a5d6837
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 6 deletions.
7 changes: 5 additions & 2 deletions README.md
Expand Up @@ -11,12 +11,13 @@ Serialize JavaScript to a _superset_ of JSON that includes regular expressions,

The code in this package began its life as an internal module to [express-state][]. To expand its usefulness, it now lives as `serialize-javascript` — an independent package on npm.

You're probably wondering: **What about `JSON.stringify()`!?** We've found that sometimes we need to serialize JavaScript **functions**, **regexps** or **dates**. A great example is a web app that uses client-side URL routing where the route definitions are regexps that need to be shared from the server to the client. But this module is also great for communicating between node processes.
You're probably wondering: **What about `JSON.stringify()`!?** We've found that sometimes we need to serialize JavaScript **functions**, **regexps**, **dates**, **sets** or **maps**. A great example is a web app that uses client-side URL routing where the route definitions are regexps that need to be shared from the server to the client. But this module is also great for communicating between node processes.

The string returned from this package's single export function is literal JavaScript which can be saved to a `.js` file, or be embedded into an HTML document by making the content of a `<script>` element.

> **HTML characters and JavaScript line terminators are escaped automatically.**
Please note that serialization for ES6 Sets & Maps requires support for `Array.from` (not available in IE or Node < 0.12), or an `Array.from` polyfill.

## Installation

Expand All @@ -40,6 +41,8 @@ serialize({
nil : null,
undef: undefined,
date: new Date("Thu, 28 Apr 2016 22:02:17 GMT"),
map: new Map([['hello', 'world']]),
set: new Set([123, 456]),

fn: function echo(arg) { return arg; },
re: /([^\s]+)/g
Expand All @@ -49,7 +52,7 @@ serialize({
The above will produce the following string output:

```js
'{"str":"string","num":0,"obj":{"foo":"foo"},"arr":[1,2,3],"bool":true,"nil":null,date:new Date("2016-04-28T22:02:17.156Z"),"fn":function echo(arg) { return arg; },"re":/([^\\s]+)/g}'
'{"str":"string","num":0,"obj":{"foo":"foo"},"arr":[1,2,3],"bool":true,"nil":null,date:new Date("2016-04-28T22:02:17.156Z"),new Map([["hello", "world"]]),new Set([123,456]),"fn":function echo(arg) { return arg; },"re":/([^\\s]+)/g}'
```

Note: to produced a beautified string, you can pass an optional second argument to `serialize()` to define the number of spaces to be used for the indentation.
Expand Down
26 changes: 22 additions & 4 deletions index.js
Expand Up @@ -8,7 +8,7 @@ See the accompanying LICENSE file for terms.

// Generate an internal UID to make the regexp pattern harder to guess.
var UID = Math.floor(Math.random() * 0x10000000000).toString(16);
var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R|D)-' + UID + '-(\\d+)__@"', 'g');
var PLACE_HOLDER_REGEXP = new RegExp('"@__(F|R|D|M|S)-' + UID + '-(\\d+)__@"', 'g');

var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
var IS_PURE_FUNCTION = /function.*?\(/;
Expand Down Expand Up @@ -41,6 +41,8 @@ module.exports = function serialize(obj, options) {
var functions = [];
var regexps = [];
var dates = [];
var maps = [];
var sets = [];

// Returns placeholders for functions and regexps (identified by index)
// which are later replaced by their string representation.
Expand All @@ -62,6 +64,14 @@ module.exports = function serialize(obj, options) {
if(origValue instanceof Date) {
return '@__D-' + UID + '-' + (dates.push(origValue) - 1) + '__@';
}

if(origValue instanceof Map) {
return '@__M-' + UID + '-' + (maps.push(origValue) - 1) + '__@';
}

if(origValue instanceof Set) {
return '@__S-' + UID + '-' + (sets.push(origValue) - 1) + '__@';
}
}

if (type === 'function') {
Expand Down Expand Up @@ -126,11 +136,11 @@ module.exports = function serialize(obj, options) {
str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars);
}

if (functions.length === 0 && regexps.length === 0 && dates.length === 0) {
if (functions.length === 0 && regexps.length === 0 && dates.length === 0 && maps.length === 0 && sets.length === 0) {
return str;
}

// Replaces all occurrences of function, regexp and date placeholders in the
// Replaces all occurrences of function, regexp, date, map and set placeholders in the
// JSON string with their string representations. If the original value can
// not be found, then `undefined` is used.
return str.replace(PLACE_HOLDER_REGEXP, function (match, type, valueIndex) {
Expand All @@ -142,7 +152,15 @@ module.exports = function serialize(obj, options) {
return regexps[valueIndex].toString();
}

var fn = functions[valueIndex];
if (type === 'M') {
return "new Map(" + serialize(Array.from(maps[valueIndex].entries()), options) + ")";
}

if (type === 'S') {
return "new Set(" + serialize(Array.from(sets[valueIndex].values()), options) + ")";
}

var fn = functions[valueIndex];

return serializeFunc(fn);
});
Expand Down
44 changes: 44 additions & 0 deletions test/unit/serialize.js
Expand Up @@ -198,6 +198,50 @@ describe('serialize( obj )', function () {
});
});

describe('maps', function () {
it('should serialize maps', function () {
var regexKey = /.*/;
var m = new Map([
['a', 123],
[regexKey, 456]
]);
expect(serialize(m)).to.be.a('string').equal('new Map([["a",123],[/.*/,456]])');
expect(serialize({t: [m]})).to.be.a('string').equal('{"t":[new Map([["a",123],[/.*/,456]])]}');
});

it('should deserialize a map', function () {
var m = eval(serialize(new Map([
['a', 123],
[null, 456]
])));
expect(m).to.be.a('Map');
expect(m.get(null)).to.equal(456);
});
});

describe('sets', function () {
it('should serialize sets', function () {
var regex = /.*/;
var m = new Set([
'a',
123,
regex
]);
expect(serialize(m)).to.be.a('string').equal('new Set(["a",123,/.*/])');
expect(serialize({t: [m]})).to.be.a('string').equal('{"t":[new Set(["a",123,/.*/])]}');
});

it('should deserialize a set', function () {
var m = eval(serialize(new Set([
'a',
123,
null
])));
expect(m).to.be.a('Set');
expect(m.has(null)).to.equal(true);
});
});

describe('XSS', function () {
it('should encode unsafe HTML chars to Unicode', function () {
expect(serialize('</script>')).to.equal('"\\u003C\\u002Fscript\\u003E"');
Expand Down

0 comments on commit a5d6837

Please sign in to comment.