From 299231a6a6dd37b26de9cff9a7fbd32fb5dbd358 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 22 Nov 2019 08:46:16 +0800 Subject: [PATCH 1/7] feat(deepMerge): bring up --- README.md | 13 ++++++++ lib/deep_merge.js | 17 +++++++++++ lib/index.js | 1 + test/deep_merge.spec.js | 68 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 lib/deep_merge.js create mode 100644 test/deep_merge.spec.js diff --git a/README.md b/README.md index 8e3c5be0..73c95ac3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Utilities for [Hexo]. - [camelCaseKeys](#camelcasekeysobj-options) - [createSha1Hash](#createsha1hash) - [decodeURL](#decodeurlstr) +- [deepMerge](#deepmergetarget-source) - [encodeURL](#encodeurlstr) - [escapeDiacritic](#escapediacriticstr) - [escapeHTML](#escapehtmlstr) @@ -111,6 +112,18 @@ decodeURI(format(new URL('http://xn--br-mia.com.com/b%C3%A1r'), {unicode: true}) // http://bár.com/báz ``` +### deepMerge(target, source) + +Merges the enumerable properties of two objects deeply. + +``` js +const obj1 = {a: {b: 1, c: 1, d: {e: 1, f: 1}}}; +const obj2 = {a: {b: 2, d: {f: 'f'} }}; + +deepMerge(obj1, obj2); +// {a: {b: 2, c: 1, d: {e: 1, f: 'f'} }} +``` + ### encodeURL(str) Encode URL or path into a [safe format](https://en.wikipedia.org/wiki/Percent-encoding). diff --git a/lib/deep_merge.js b/lib/deep_merge.js new file mode 100644 index 00000000..c95e0c43 --- /dev/null +++ b/lib/deep_merge.js @@ -0,0 +1,17 @@ +'use strict'; + +function deepMerge(target, source) { + target = Object.assign({}, target); + source = Object.assign({}, source); + + // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties + for (const key of Object.keys(source)) { + if (source[key] instanceof Object) Object.assign(source[key], deepMerge(target[key], source[key])); + } + + // Join `target` and modified `source` + Object.assign(target || {}, source); + return target; +} + +module.exports = deepMerge; diff --git a/lib/index.js b/lib/index.js index 7f03eb86..4b9c2514 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,7 @@ exports.camelCaseKeys = require('./camel_case_keys'); exports.Color = require('./color'); exports.createSha1Hash = hash.createSha1Hash; exports.decodeURL = require('./decode_url'); +exports.deepMerge = require('./deep_merge'); exports.encodeURL = require('./encode_url'); exports.escapeDiacritic = require('./escape_diacritic'); exports.escapeHTML = require('./escape_html'); diff --git a/test/deep_merge.spec.js b/test/deep_merge.spec.js new file mode 100644 index 00000000..3eaa5786 --- /dev/null +++ b/test/deep_merge.spec.js @@ -0,0 +1,68 @@ +'use strict'; + +require('chai').should(); + +// The test is modified based on https://github.com/jonschlinkert/merge-deep/blob/master/test.js + +describe('deepMerge()', () => { + const deepMerge = require('../lib/deep_merge'); + + it('should merge object properties without affecting any object', () => { + const obj1 = {a: 0, b: 1}; + const obj2 = {c: 2, b: 3}; + + const result = deepMerge(obj1, obj2); + const expected = {a: 0, b: 3, c: 2 }; + + result.should.eql(expected); + result.should.not.eql(obj1); + result.should.not.eql(obj2); + }); + + it('should do a deep merge', () => { + const obj1 = {a: {b: 1, c: 1, d: {e: 1, f: 1}}}; + const obj2 = {a: {b: 2, d: {f: 'f'} }}; + + const expected = {a: {b: 2, c: 1, d: {e: 1, f: 'f'} }}; + + deepMerge(obj1, obj2).should.eql(expected); + }); + + it('should not merge strings', () => { + const obj1 = {a: 'fooo'}; + const obj2 = {a: {b: 2, d: {f: 'f'} }}; + const obj3 = {a: 'bar'}; + + const result = deepMerge(deepMerge(obj1, obj2), obj3); + result.a.should.eql('bar'); + }); + + it('should shallow clone objects during merge', () => { + const obj1 = {a: {b: 1}}; + const obj2 = {a: {c: 2}}; + + const result = deepMerge(obj1, obj2); + + result.should.eql({a: {b: 1, c: 2}}); + result.a.should.not.eql(obj1.a); + result.a.should.eql(obj2.a); + }); + + it('should not merge an objects into an array', () => { + const obj1 = {a: {b: 1}}; + const obj2 = {a: ['foo', 'bar']}; + + deepMerge(obj1, obj2).should.eql({a: ['foo', 'bar']}); + }); + + it('should deep clone arrays during merge', () => { + const obj1 = {a: [1, 2, [3, 4]]}; + const obj2 = {b: [5, 6]}; + + const result = deepMerge(obj1, obj2); + + result.a.should.eql([1, 2, [3, 4]]); + result.a[2].should.eql([3, 4]); + result.b.should.eql(obj2.b); + }); +}); From d652ff12092c96b34908a90a2207731bbba30865 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 22 Nov 2019 08:51:39 +0800 Subject: [PATCH 2/7] test(deepMerge): add lodash.merge example --- test/deep_merge.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/deep_merge.spec.js b/test/deep_merge.spec.js index 3eaa5786..5907f4d0 100644 --- a/test/deep_merge.spec.js +++ b/test/deep_merge.spec.js @@ -7,6 +7,14 @@ require('chai').should(); describe('deepMerge()', () => { const deepMerge = require('../lib/deep_merge'); + it('should act as lodash.merge', () => { + const obj1 = { 'a': [{ 'b': 2 }, { 'd': 4 }] }; + + const obj2 = { 'a': [{ 'c': 3 }, { 'e': 5 }] }; + + deepMerge(obj1, obj2).should.eql({ 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }); + }); + it('should merge object properties without affecting any object', () => { const obj1 = {a: 0, b: 1}; const obj2 = {c: 2, b: 3}; From 2141f3beb5fd11e9c4341911deef945085347437 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Mon, 25 Nov 2019 21:17:33 +0800 Subject: [PATCH 3/7] test(deepMerge): only affect target --- README.md | 2 +- lib/deep_merge.js | 3 --- test/deep_merge.spec.js | 28 ++++++++++------------------ 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 73c95ac3..8cafdf96 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ decodeURI(format(new URL('http://xn--br-mia.com.com/b%C3%A1r'), {unicode: true}) ### deepMerge(target, source) -Merges the enumerable properties of two objects deeply. +Merges the enumerable properties of two objects deeply. Only `target` object will be changed. ``` js const obj1 = {a: {b: 1, c: 1, d: {e: 1, f: 1}}}; diff --git a/lib/deep_merge.js b/lib/deep_merge.js index c95e0c43..598b7db0 100644 --- a/lib/deep_merge.js +++ b/lib/deep_merge.js @@ -1,9 +1,6 @@ 'use strict'; function deepMerge(target, source) { - target = Object.assign({}, target); - source = Object.assign({}, source); - // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties for (const key of Object.keys(source)) { if (source[key] instanceof Object) Object.assign(source[key], deepMerge(target[key], source[key])); diff --git a/test/deep_merge.spec.js b/test/deep_merge.spec.js index 5907f4d0..2bfafaaf 100644 --- a/test/deep_merge.spec.js +++ b/test/deep_merge.spec.js @@ -9,22 +9,25 @@ describe('deepMerge()', () => { it('should act as lodash.merge', () => { const obj1 = { 'a': [{ 'b': 2 }, { 'd': 4 }] }; - const obj2 = { 'a': [{ 'c': 3 }, { 'e': 5 }] }; - deepMerge(obj1, obj2).should.eql({ 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }); + const expected = { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }; + + deepMerge(obj1, obj2).should.eql(expected); }); - it('should merge object properties without affecting any object', () => { - const obj1 = {a: 0, b: 1}; - const obj2 = {c: 2, b: 3}; + it('should merge object properties with target object affected', () => { + const obj1 = {a: 0, b: 1, c: {d: 1}, e: 4}; + const obj2 = {b: 3, c: {d: 2}}; const result = deepMerge(obj1, obj2); - const expected = {a: 0, b: 3, c: 2 }; + const expected = {a: 0, b: 3, c: {d: 2}, e: 4}; result.should.eql(expected); - result.should.not.eql(obj1); + obj1.should.eql(expected); + result.should.not.eql(obj2); + obj2.should.eql({b: 3, c: {d: 2}}); }); it('should do a deep merge', () => { @@ -45,17 +48,6 @@ describe('deepMerge()', () => { result.a.should.eql('bar'); }); - it('should shallow clone objects during merge', () => { - const obj1 = {a: {b: 1}}; - const obj2 = {a: {c: 2}}; - - const result = deepMerge(obj1, obj2); - - result.should.eql({a: {b: 1, c: 2}}); - result.a.should.not.eql(obj1.a); - result.a.should.eql(obj2.a); - }); - it('should not merge an objects into an array', () => { const obj1 = {a: {b: 1}}; const obj2 = {a: ['foo', 'bar']}; From f68177c391d239f3a99287073fba7417a3d24292 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 1 Dec 2019 13:33:37 +0800 Subject: [PATCH 4/7] refactor(deepMerge): use deepmerge --- README.md | 12 +++++++++++- lib/deep_merge.js | 27 +++++++++++++++++++-------- package.json | 1 + test/deep_merge.spec.js | 30 ++++++++++++++++-------------- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8cafdf96..d80e5182 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,10 @@ decodeURI(format(new URL('http://xn--br-mia.com.com/b%C3%A1r'), {unicode: true}) ### deepMerge(target, source) -Merges the enumerable properties of two objects deeply. Only `target` object will be changed. +Merges the enumerable properties of two objects deeply while neither `target` or `source` is modified. ``` js +// Merge deeply const obj1 = {a: {b: 1, c: 1, d: {e: 1, f: 1}}}; const obj2 = {a: {b: 2, d: {f: 'f'} }}; @@ -124,6 +125,15 @@ deepMerge(obj1, obj2); // {a: {b: 2, c: 1, d: {e: 1, f: 'f'} }} ``` +``` js +// Objects will be combined at the same index in the two arrays, just as lodash.merge +const obj1 = { 'a': [{ 'b': 2 }, { 'd': 4 }] }; +const obj2 = { 'a': [{ 'c': 3 }, { 'e': 5 }] }; + +deepMerge(obj1, obj2); +// { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }; +``` + ### encodeURL(str) Encode URL or path into a [safe format](https://en.wikipedia.org/wiki/Percent-encoding). diff --git a/lib/deep_merge.js b/lib/deep_merge.js index 598b7db0..92288448 100644 --- a/lib/deep_merge.js +++ b/lib/deep_merge.js @@ -1,14 +1,25 @@ 'use strict'; +const deepmerge = require('deepmerge'); + +const arrayMerge = (target, source, options) => { + const destination = [...target]; + + source.forEach((item, index) => { + if (typeof destination[index] === 'undefined') { + destination[index] = options.cloneUnlessOtherwiseSpecified(item, options); + } else if (options.isMergeableObject(item)) { + destination[index] = deepmerge(target[index], item, options); + } else if (!target.includes(item)) { + destination.push(item); + } + }); + return destination; +}; + + function deepMerge(target, source) { - // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties - for (const key of Object.keys(source)) { - if (source[key] instanceof Object) Object.assign(source[key], deepMerge(target[key], source[key])); - } - - // Join `target` and modified `source` - Object.assign(target || {}, source); - return target; + return deepmerge(target, source, { arrayMerge }); } module.exports = deepMerge; diff --git a/package.json b/package.json index 2065be23..ddaa3fbf 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "bluebird": "^3.5.2", "camel-case": "^3.0.0", "cross-spawn": "^7.0.0", + "deepmerge": "^4.2.2", "highlight.js": "^9.13.1", "striptags": "^3.1.1" }, diff --git a/test/deep_merge.spec.js b/test/deep_merge.spec.js index 2bfafaaf..d1ec5268 100644 --- a/test/deep_merge.spec.js +++ b/test/deep_merge.spec.js @@ -16,20 +16,6 @@ describe('deepMerge()', () => { deepMerge(obj1, obj2).should.eql(expected); }); - it('should merge object properties with target object affected', () => { - const obj1 = {a: 0, b: 1, c: {d: 1}, e: 4}; - const obj2 = {b: 3, c: {d: 2}}; - - const result = deepMerge(obj1, obj2); - const expected = {a: 0, b: 3, c: {d: 2}, e: 4}; - - result.should.eql(expected); - obj1.should.eql(expected); - - result.should.not.eql(obj2); - obj2.should.eql({b: 3, c: {d: 2}}); - }); - it('should do a deep merge', () => { const obj1 = {a: {b: 1, c: 1, d: {e: 1, f: 1}}}; const obj2 = {a: {b: 2, d: {f: 'f'} }}; @@ -55,6 +41,22 @@ describe('deepMerge()', () => { deepMerge(obj1, obj2).should.eql({a: ['foo', 'bar']}); }); + it('should not affect target & source', () => { + const obj1 = {a: 0, b: 1, c: {d: 1}, e: 4}; + const obj2 = {b: 3, c: {d: 2}}; + + const result = deepMerge(obj1, obj2); + const expected = {a: 0, b: 3, c: {d: 2}, e: 4}; + + result.should.eql(expected); + + result.should.not.eql(obj1); + obj1.should.eql({a: 0, b: 1, c: {d: 1}, e: 4}); + + result.should.not.eql(obj2); + obj2.should.eql({b: 3, c: {d: 2}}); + }); + it('should deep clone arrays during merge', () => { const obj1 = {a: [1, 2, [3, 4]]}; const obj2 = {b: [5, 6]}; From 392d62b9ce9bd3ef8c5d0dc4fb48372aba20ce3b Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 1 Dec 2019 13:41:36 +0800 Subject: [PATCH 5/7] refactor(deepMerge): slice() is faster than spread --- lib/deep_merge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/deep_merge.js b/lib/deep_merge.js index 92288448..cfe0455f 100644 --- a/lib/deep_merge.js +++ b/lib/deep_merge.js @@ -3,7 +3,7 @@ const deepmerge = require('deepmerge'); const arrayMerge = (target, source, options) => { - const destination = [...target]; + const destination = target.slice(); source.forEach((item, index) => { if (typeof destination[index] === 'undefined') { From bbce4f826550c2b41024803ba465f4cabbef19b6 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 1 Dec 2019 14:48:58 +0800 Subject: [PATCH 6/7] test(deepMerge): coverage --- test/deep_merge.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/deep_merge.spec.js b/test/deep_merge.spec.js index d1ec5268..d49bdc3d 100644 --- a/test/deep_merge.spec.js +++ b/test/deep_merge.spec.js @@ -34,6 +34,16 @@ describe('deepMerge()', () => { result.a.should.eql('bar'); }); + it('should merge simple array', () => { + const obj1 = {a: [1, [2, 3], 4]}; + const obj2 = {a: [1, [3, 4], [5, 6], 6]}; + + const result = deepMerge(obj1, obj2); + const expected = {a: [1, [2, 3, 4], [5, 6], 6]}; + + result.should.eql(expected); + }); + it('should not merge an objects into an array', () => { const obj1 = {a: {b: 1}}; const obj2 = {a: ['foo', 'bar']}; From 1bb1053c55b8c71f4df2900da2cf6638eaa5d9e7 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Mon, 2 Dec 2019 18:53:15 +0800 Subject: [PATCH 7/7] docs(deepMerge): update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d80e5182..7cf66de0 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ decodeURI(format(new URL('http://xn--br-mia.com.com/b%C3%A1r'), {unicode: true}) ### deepMerge(target, source) -Merges the enumerable properties of two objects deeply while neither `target` or `source` is modified. +Merges the enumerable properties of two objects deeply. `target` and `source` remain untouched. ``` js // Merge deeply @@ -126,7 +126,7 @@ deepMerge(obj1, obj2); ``` ``` js -// Objects will be combined at the same index in the two arrays, just as lodash.merge +// Arrays will be combined in the same property, similar to lodash.merge const obj1 = { 'a': [{ 'b': 2 }, { 'd': 4 }] }; const obj2 = { 'a': [{ 'c': 3 }, { 'e': 5 }] };