├── .editorconfig
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── index.js
├── lib
└── deep-extend.js
├── package.json
└── test
├── index.spec.js
└── mocha.opts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | trim_trailing_whitespace = true
5 | indent_style = tab
6 | end_of_line = lf
7 | insert_final_newline = true
8 | max_line_length = 100
9 |
10 | [package.json]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | max_line_length = 80
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache: yarn
3 | node_js:
4 | - "node"
5 | - "10"
6 | - "9"
7 | - "8"
8 | - "7"
9 | - "6"
10 | - "5"
11 | - "4"
12 | - "4.0.0" # minimal supported version
13 | before_install:
14 | - npm config set strict-ssl false
15 | - npm config set registry="http://registry.npmjs.org/"
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | v0.6.0
5 | ------
6 |
7 | - Updated "devDependencies" versions to fix vulnerability alerts
8 | - Dropped support of io.js and node.js v0.12.x and lower since new versions of
9 | "devDependencies" couldn't work with those old node.js versions
10 | (minimal supported version of node.js now is v4.0.0)
11 |
12 | v0.5.1
13 | ------
14 |
15 | - Fix prototype pollution vulnerability (thanks to @mwakerman for the PR)
16 | - Avoid using deprecated Buffer API (thanks to @ChALkeR for the PR)
17 |
18 | v0.5.0
19 | ------
20 |
21 | - Auto-testing provided by Travis CI;
22 | - Support older Node.JS versions (`v0.11.x` and `v0.10.x`);
23 | - Removed tests files from npm package.
24 |
25 | v0.4.2
26 | ------
27 |
28 | - Fix for `null` as an argument.
29 |
30 | v0.4.1
31 | ------
32 |
33 | - Removed test code from npm package
34 | ([see pull request #21](https://github.com/unclechu/node-deep-extend/pull/21));
35 | - Increased minimal version of Node from `0.4.0` to `0.12.0`
36 | (because can't run tests on lesser version anyway).
37 |
38 | v0.4.0
39 | ------
40 |
41 | - **WARNING!** Broken backward compatibility with `v0.3.x`;
42 | - Fixed bug with extending arrays instead of cloning;
43 | - Deep cloning for arrays;
44 | - Check for own property;
45 | - Fixed some documentation issues;
46 | - Strict JS mode.
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-2018, Viacheslav Lotsmanov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Deep Extend
2 | ===========
3 |
4 | Recursive object extending.
5 |
6 | [](https://travis-ci.org/unclechu/node-deep-extend)
7 |
8 | [](https://nodei.co/npm/deep-extend/)
9 |
10 | Install
11 | -------
12 |
13 | ```bash
14 | $ npm install deep-extend
15 | ```
16 |
17 | Usage
18 | -----
19 |
20 | ```javascript
21 | var deepExtend = require('deep-extend');
22 | var obj1 = {
23 | a: 1,
24 | b: 2,
25 | d: {
26 | a: 1,
27 | b: [],
28 | c: { test1: 123, test2: 321 }
29 | },
30 | f: 5,
31 | g: 123,
32 | i: 321,
33 | j: [1, 2]
34 | };
35 | var obj2 = {
36 | b: 3,
37 | c: 5,
38 | d: {
39 | b: { first: 'one', second: 'two' },
40 | c: { test2: 222 }
41 | },
42 | e: { one: 1, two: 2 },
43 | f: [],
44 | g: (void 0),
45 | h: /abc/g,
46 | i: null,
47 | j: [3, 4]
48 | };
49 |
50 | deepExtend(obj1, obj2);
51 |
52 | console.log(obj1);
53 | /*
54 | { a: 1,
55 | b: 3,
56 | d:
57 | { a: 1,
58 | b: { first: 'one', second: 'two' },
59 | c: { test1: 123, test2: 222 } },
60 | f: [],
61 | g: undefined,
62 | c: 5,
63 | e: { one: 1, two: 2 },
64 | h: /abc/g,
65 | i: null,
66 | j: [3, 4] }
67 | */
68 | ```
69 |
70 | Unit testing
71 | ------------
72 |
73 | ```bash
74 | $ npm test
75 | ```
76 |
77 | Changelog
78 | ---------
79 |
80 | [CHANGELOG.md](./CHANGELOG.md)
81 |
82 | Any issues?
83 | -----------
84 |
85 | Please, report about issues
86 | [here](https://github.com/unclechu/node-deep-extend/issues).
87 |
88 | License
89 | -------
90 |
91 | [MIT](./LICENSE)
92 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/deep-extend');
2 |
--------------------------------------------------------------------------------
/lib/deep-extend.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * @description Recursive object extending
3 | * @author Viacheslav Lotsmanov
4 | * @license MIT
5 | *
6 | * The MIT License (MIT)
7 | *
8 | * Copyright (c) 2013-2018 Viacheslav Lotsmanov
9 | *
10 | * Permission is hereby granted, free of charge, to any person obtaining a copy of
11 | * this software and associated documentation files (the "Software"), to deal in
12 | * the Software without restriction, including without limitation the rights to
13 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14 | * the Software, and to permit persons to whom the Software is furnished to do so,
15 | * subject to the following conditions:
16 | *
17 | * The above copyright notice and this permission notice shall be included in all
18 | * copies or substantial portions of the Software.
19 | *
20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 | */
27 |
28 | 'use strict';
29 |
30 | function isSpecificValue(val) {
31 | return (
32 | val instanceof Buffer
33 | || val instanceof Date
34 | || val instanceof RegExp
35 | ) ? true : false;
36 | }
37 |
38 | function cloneSpecificValue(val) {
39 | if (val instanceof Buffer) {
40 | var x = Buffer.alloc
41 | ? Buffer.alloc(val.length)
42 | : new Buffer(val.length);
43 | val.copy(x);
44 | return x;
45 | } else if (val instanceof Date) {
46 | return new Date(val.getTime());
47 | } else if (val instanceof RegExp) {
48 | return new RegExp(val);
49 | } else {
50 | throw new Error('Unexpected situation');
51 | }
52 | }
53 |
54 | /**
55 | * Recursive cloning array.
56 | */
57 | function deepCloneArray(arr) {
58 | var clone = [];
59 | arr.forEach(function (item, index) {
60 | if (typeof item === 'object' && item !== null) {
61 | if (Array.isArray(item)) {
62 | clone[index] = deepCloneArray(item);
63 | } else if (isSpecificValue(item)) {
64 | clone[index] = cloneSpecificValue(item);
65 | } else {
66 | clone[index] = deepExtend({}, item);
67 | }
68 | } else {
69 | clone[index] = item;
70 | }
71 | });
72 | return clone;
73 | }
74 |
75 | function safeGetProperty(object, property) {
76 | return property === '__proto__' ? undefined : object[property];
77 | }
78 |
79 | /**
80 | * Extening object that entered in first argument.
81 | *
82 | * Returns extended object or false if have no target object or incorrect type.
83 | *
84 | * If you wish to clone source object (without modify it), just use empty new
85 | * object as first argument, like this:
86 | * deepExtend({}, yourObj_1, [yourObj_N]);
87 | */
88 | var deepExtend = module.exports = function (/*obj_1, [obj_2], [obj_N]*/) {
89 | if (arguments.length < 1 || typeof arguments[0] !== 'object') {
90 | return false;
91 | }
92 |
93 | if (arguments.length < 2) {
94 | return arguments[0];
95 | }
96 |
97 | var target = arguments[0];
98 |
99 | // convert arguments to array and cut off target object
100 | var args = Array.prototype.slice.call(arguments, 1);
101 |
102 | var val, src, clone;
103 |
104 | args.forEach(function (obj) {
105 | // skip argument if isn't an object, is null, or is an array
106 | if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
107 | return;
108 | }
109 |
110 | Object.keys(obj).forEach(function (key) {
111 | src = safeGetProperty(target, key); // source value
112 | val = safeGetProperty(obj, key); // new value
113 |
114 | // recursion prevention
115 | if (val === target) {
116 | return;
117 |
118 | /**
119 | * if new value isn't object then just overwrite by new value
120 | * instead of extending.
121 | */
122 | } else if (typeof val !== 'object' || val === null) {
123 | target[key] = val;
124 | return;
125 |
126 | // just clone arrays (and recursive clone objects inside)
127 | } else if (Array.isArray(val)) {
128 | target[key] = deepCloneArray(val);
129 | return;
130 |
131 | // custom cloning and overwrite for specific objects
132 | } else if (isSpecificValue(val)) {
133 | target[key] = cloneSpecificValue(val);
134 | return;
135 |
136 | // overwrite by new value if source isn't object or array
137 | } else if (typeof src !== 'object' || src === null || Array.isArray(src)) {
138 | target[key] = deepExtend({}, val);
139 | return;
140 |
141 | // source value and new value is objects both, extending...
142 | } else {
143 | target[key] = deepExtend(src, val);
144 | return;
145 | }
146 | });
147 | });
148 |
149 | return target;
150 | };
151 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deep-extend",
3 | "description": "Recursive object extending",
4 | "license": "MIT",
5 | "version": "0.6.0",
6 | "homepage": "https://github.com/unclechu/node-deep-extend",
7 | "keywords": [
8 | "deep-extend",
9 | "extend",
10 | "deep",
11 | "recursive",
12 | "xtend",
13 | "clone",
14 | "merge",
15 | "json"
16 | ],
17 | "licenses": [
18 | {
19 | "type": "MIT",
20 | "url": "https://raw.githubusercontent.com/unclechu/node-deep-extend/master/LICENSE"
21 | }
22 | ],
23 | "repository": {
24 | "type": "git",
25 | "url": "git://github.com/unclechu/node-deep-extend.git"
26 | },
27 | "author": "Viacheslav Lotsmanov ",
28 | "bugs": "https://github.com/unclechu/node-deep-extend/issues",
29 | "contributors": [
30 | {
31 | "name": "Romain Prieto",
32 | "url": "https://github.com/rprieto"
33 | },
34 | {
35 | "name": "Max Maximov",
36 | "url": "https://github.com/maxmaximov"
37 | },
38 | {
39 | "name": "Marshall Bowers",
40 | "url": "https://github.com/maxdeviant"
41 | },
42 | {
43 | "name": "Misha Wakerman",
44 | "url": "https://github.com/mwakerman"
45 | }
46 | ],
47 | "main": "lib/deep-extend.js",
48 | "engines": {
49 | "node": ">=4.0.0"
50 | },
51 | "scripts": {
52 | "test": "./node_modules/.bin/mocha"
53 | },
54 | "devDependencies": {
55 | "mocha": "5.2.0",
56 | "should": "13.2.1"
57 | },
58 | "files": [
59 | "index.js",
60 | "lib/"
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var should = require('should');
4 | var extend = require('../index'); // it must be ./lib/deep-extend.js
5 |
6 | describe('deep-extend', function () {
7 |
8 | it('should ignore undefined', function () {
9 | var a = { hello: 1 };
10 | var b = undefined;
11 | extend(a, b);
12 | a.should.eql({
13 | hello: 1
14 | });
15 | });
16 |
17 | it('should ignore null', function () {
18 | var a = { hello: 1 };
19 | var b = null;
20 | extend(a, b);
21 | a.should.eql({
22 | hello: 1
23 | });
24 | });
25 |
26 | it('can extend on 1 level', function () {
27 | var a = { hello: 1 };
28 | var b = { world: 2 };
29 | extend(a, b);
30 | a.should.eql({
31 | hello: 1,
32 | world: 2
33 | });
34 | });
35 |
36 | it('can extend on 2 levels', function () {
37 | var a = { person: { name: 'John' } };
38 | var b = { person: { age: 30 } };
39 | extend(a, b);
40 | a.should.eql({
41 | person: { name: 'John', age: 30 }
42 | });
43 | });
44 |
45 | it('can extend with Buffer values', function () {
46 | var a = { hello: 1 };
47 | var b = { value: new Buffer('world') };
48 | extend(a, b);
49 | a.should.eql({
50 | hello: 1,
51 | value: new Buffer('world')
52 | });
53 | });
54 |
55 | it('Buffer is cloned', function () {
56 | var a = {};
57 | var b = { value: new Buffer('foo') };
58 | extend(a, b);
59 | a.value.write('bar');
60 | a.value.toString().should.eql('bar');
61 | b.value.toString().should.eql('foo');
62 | });
63 |
64 | it('Date objects', function () {
65 | var a = { d: new Date() };
66 | var b = extend({}, a);
67 | b.d.should.instanceOf(Date);
68 | });
69 |
70 | it('Date object is cloned', function () {
71 | var a = { d: new Date() };
72 | var b = extend({}, a);
73 | b.d.setTime((new Date()).getTime() + 100000);
74 | b.d.getTime().should.not.eql(a.d.getTime());
75 | });
76 |
77 | it('RegExp objects', function () {
78 | var a = { d: new RegExp() };
79 | var b = extend({}, a);
80 | b.d.should.instanceOf(RegExp);
81 | });
82 |
83 | it('RegExp object is cloned', function () {
84 | var a = { d: new RegExp('b', 'g') };
85 | var b = extend({}, a);
86 | b.d.test('abc');
87 | b.d.lastIndex.should.not.eql(a.d.lastIndex);
88 | });
89 |
90 | it('doesn\'t change sources', function () {
91 | var a = { a: [1] };
92 | var b = { a: [2] };
93 | var c = { c: 3 };
94 | var d = extend({}, a, b, c);
95 |
96 | a.should.eql({ a: [1] });
97 | b.should.eql({ a: [2] });
98 | c.should.eql({ c: 3 });
99 | });
100 |
101 | it('example from README.md', function () {
102 | var obj1 = {
103 | a: 1,
104 | b: 2,
105 | d: {
106 | a: 1,
107 | b: [],
108 | c: { test1: 123, test2: 321 }
109 | },
110 | f: 5,
111 | g: 123,
112 | i: 321,
113 | j: [1, 2]
114 | };
115 | var obj2 = {
116 | b: 3,
117 | c: 5,
118 | d: {
119 | b: { first: 'one', second: 'two' },
120 | c: { test2: 222 }
121 | },
122 | e: { one: 1, two: 2 },
123 | f: [],
124 | g: (void 0),
125 | h: /abc/g,
126 | i: null,
127 | j: [3, 4]
128 | };
129 |
130 | extend(obj1, obj2);
131 |
132 | obj1.should.eql({
133 | a: 1,
134 | b: 3,
135 | d: {
136 | a: 1,
137 | b: { first: 'one', second: 'two' },
138 | c: { test1: 123, test2: 222 }
139 | },
140 | f: [],
141 | g: undefined,
142 | c: 5,
143 | e: { one: 1, two: 2 },
144 | h: /abc/g,
145 | i: null,
146 | j: [3, 4]
147 | });
148 |
149 | ('g' in obj1).should.eql(true);
150 | ('x' in obj1).should.eql(false);
151 | });
152 |
153 | it('clone arrays instead of extend', function () {
154 | extend({ a: [1, 2, 3] }, { a: [2, 3] }).should.eql({ a: [2, 3] });
155 | });
156 |
157 | it('recursive clone objects and special objects in cloned arrays', function () {
158 | var obj1 = {
159 | x: 1,
160 | y: new Buffer('foo')
161 | };
162 | var b = new Buffer('bar');
163 | var obj2 = {
164 | x: 1,
165 | y: [2, 4, obj1, b],
166 | z: new Buffer('test')
167 | };
168 | var foo = {
169 | a: [obj2, obj2]
170 | };
171 | var bar = extend({}, foo);
172 | bar.a[0].x = 2;
173 | bar.a[0].z.write('text', 'utf-8');
174 | bar.a[1].x = 3;
175 | bar.a[1].z.write('lel', 'utf-8');
176 | bar.a[0].y[0] = 3;
177 | bar.a[0].y[2].x = 5;
178 | bar.a[0].y[2].y.write('heh', 'utf-8');
179 | bar.a[0].y[3].write('ho', 'utf-8');
180 | bar.a[1].y[1] = 3;
181 | bar.a[1].y[2].y.write('nah', 'utf-8');
182 | bar.a[1].y[3].write('he', 'utf-8');
183 |
184 | obj2.x.should.eql(1);
185 | obj2.z.toString().should.eql('test');
186 | bar.a[0].x.should.eql(2);
187 | bar.a[0].z.toString().should.eql('text');
188 | bar.a[1].x.should.eql(3);
189 | bar.a[1].z.toString().should.eql('lelt');
190 | obj1.x.should.eql(1);
191 | obj1.y.toString().should.eql('foo');
192 | b.toString().should.eql('bar');
193 |
194 | bar.a[0].y[0].should.eql(3);
195 | bar.a[0].y[1].should.eql(4);
196 | bar.a[0].y[2].x.should.eql(5);
197 | bar.a[0].y[2].y.toString().should.eql('heh');
198 | bar.a[0].y[3].toString().should.eql('hor');
199 |
200 | bar.a[1].y[0].should.eql(2);
201 | bar.a[1].y[1].should.eql(3);
202 | bar.a[1].y[2].x.should.eql(1);
203 | bar.a[1].y[2].y.toString().should.eql('nah');
204 | bar.a[1].y[3].toString().should.eql('her');
205 |
206 | foo.a.length.should.eql(2);
207 | bar.a.length.should.eql(2);
208 | Object.keys(obj2).should.eql(['x', 'y', 'z']);
209 | Object.keys(bar.a[0]).should.eql(['x', 'y', 'z']);
210 | Object.keys(bar.a[1]).should.eql(['x', 'y', 'z']);
211 | obj2.y.length.should.eql(4);
212 | bar.a[0].y.length.should.eql(4);
213 | bar.a[1].y.length.should.eql(4);
214 | Object.keys(obj2.y[2]).should.eql(['x', 'y']);
215 | Object.keys(bar.a[0].y[2]).should.eql(['x', 'y']);
216 | Object.keys(bar.a[1].y[2]).should.eql(['x', 'y']);
217 | });
218 |
219 | it('checking keys for hasOwnPrototype', function () {
220 | var A = function () {
221 | this.x = 1;
222 | this.y = 2;
223 | };
224 | A.prototype.z = 3;
225 | var foo = new A();
226 | extend({ x: 123 }, foo).should.eql({
227 | x: 1,
228 | y: 2
229 | });
230 | foo.z = 5;
231 | extend({ x: 123 }, foo, { y: 22 }).should.eql({
232 | x: 1,
233 | y: 22,
234 | z: 5
235 | });
236 | });
237 |
238 | describe('issue #33', function () {
239 |
240 | it('correct usage (cloning)', function () {
241 | var sharedObject = {foo: 'zero'};
242 | var objDef = {bar: sharedObject, baz: sharedObject};
243 | var obj = {bar: {foo: 'one'}, baz: {foo: 'two'}};
244 | obj = extend({}, {bar: objDef.bar, baz: objDef.baz}, obj);
245 | obj.should.eql({bar: {foo: 'one'}, baz: {foo: 'two'}});
246 | });
247 |
248 | it('incorrect usage (just extending)', function () {
249 | var sharedObject = {foo: 'serif'};
250 | var objDef = {bar: sharedObject, baz: sharedObject};
251 | var obj = {bar: {foo: 'one'}, baz: {foo: 'two'}};
252 | obj = extend({bar: objDef.bar, baz: objDef.baz}, obj);
253 | obj.should.eql({bar: {foo: 'two'}, baz: {foo: 'two'}});
254 | });
255 | });
256 |
257 | // Vulnerability reported via hacker1: https://hackerone.com/reports/311333
258 | // See https://github.com/unclechu/node-deep-extend/issues/39
259 | // See https://github.com/unclechu/node-deep-extend/pull/40
260 | it('should not modify Object prototype (hacker1 #311333)', function () {
261 | var a = {};
262 | extend({}, JSON.parse('{"__proto__":{"oops":"It works!"}}'))
263 | should.not.exist(a.oops);
264 | should.not.exist(Object.prototype.oops);
265 | });
266 |
267 | });
268 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 |
--------------------------------------------------------------------------------