├── .npmrc ├── .gitignore ├── index.d.ts ├── .travis.yml ├── index.mjs ├── index.test-d.ts ├── test ├── property-order.js ├── readme-usage.js ├── option-objects.js ├── symbol-keys.js ├── undefined-values.js ├── prototype-pollution.js ├── readme-api.js ├── user-extended-natives.js ├── arrays.js └── option-values.js ├── license ├── package.json ├── readme.md ├── index.js └── tsconfig.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | *.log 5 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function mergeOptions(...options: any[]): any; 2 | export = mergeOptions; 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'stable' 5 | - '14' 6 | - '12' 7 | - '10' 8 | after_success: 9 | - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' 10 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Thin ESM wrapper for CJS named exports. 3 | * 4 | * Ref: https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1 5 | */ 6 | 7 | import mergeOptions from './index.js'; 8 | export default mergeOptions; 9 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import mergeOptions from '.'; 3 | 4 | expectType<(...options: any[]) => any>(mergeOptions); 5 | expectType<(...options: any[]) => any>(mergeOptions.bind({ignoreUndefined: true})); 6 | 7 | expectType(mergeOptions({}, {})); 8 | 9 | console.log(mergeOptions({answer: 'The Number of the Beast'}, {answer: 42})); 10 | -------------------------------------------------------------------------------- /test/property-order.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | test('preserve property order', t => { 5 | const letters = 'abcdefghijklmnopqrst'; 6 | const source = {}; 7 | letters.split('').forEach(letter => { 8 | source[letter] = letter; 9 | }); 10 | const target = mergeOptions({}, source); 11 | t.is(Object.keys(target).join(''), letters); 12 | }); 13 | -------------------------------------------------------------------------------- /test/readme-usage.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | test('basic examples', t => { 5 | t.deepEqual( 6 | mergeOptions({foo: 0}, {bar: 1}, {baz: 2}, {bar: 3}), 7 | {foo: 0, bar: 3, baz: 2} 8 | ); 9 | t.deepEqual( 10 | mergeOptions({nested: {unicorns: 'none'}}, {nested: {unicorns: 'many'}}), 11 | {nested: {unicorns: 'many'}} 12 | ); 13 | t.deepEqual( 14 | mergeOptions({[Symbol.for('key')]: 0}, {[Symbol.for('key')]: 42}), 15 | {[Symbol.for('key')]: 42} 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/option-objects.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | test('ignore `undefined` Option Objects', t => { 5 | t.deepEqual(mergeOptions(undefined), {}); 6 | t.deepEqual(mergeOptions(undefined, {foo: true}, {foo: false}), {foo: false}); 7 | t.deepEqual(mergeOptions({foo: true}, undefined, {foo: false}), {foo: false}); 8 | }); 9 | 10 | test('support Object.create(null) Option Objects', t => { 11 | const option1 = Object.create(null); 12 | option1.foo = Object.create(null); 13 | t.deepEqual(mergeOptions(option1, {bar: Object.create(null)}), {foo: Object.create(null), bar: Object.create(null)}); 14 | }); 15 | -------------------------------------------------------------------------------- /test/symbol-keys.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | test('return new option objects', t => { 5 | const fooKey = Symbol('foo'); 6 | const source1 = {}; 7 | const source2 = {}; 8 | source1[fooKey] = {bar: false}; 9 | source2[fooKey] = {bar: true}; 10 | const fooRef1 = source1[fooKey]; 11 | const fooRef2 = source2[fooKey]; 12 | const result = mergeOptions(source1, source2); 13 | t.deepEqual(result, source2); 14 | t.not(result, source2); 15 | t.not(result[fooKey], source1[fooKey]); 16 | t.not(result[fooKey], source2[fooKey]); 17 | t.not(result[fooKey], fooRef1); 18 | t.not(result[fooKey], fooRef2); 19 | }); 20 | -------------------------------------------------------------------------------- /test/undefined-values.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | test('undefined values', t => { 5 | t.deepEqual( 6 | mergeOptions.call({ignoreUndefined: true}, {foo: 0}, {foo: undefined}), 7 | {foo: 0} 8 | ); 9 | }); 10 | 11 | test('deep undefined values', t => { 12 | t.deepEqual( 13 | mergeOptions.call({ignoreUndefined: true}, {nested: {unicorns: 'none'}}, {nested: {unicorns: undefined}}), 14 | {nested: {unicorns: 'none'}} 15 | ); 16 | }); 17 | 18 | test('undefined options objects', t => { 19 | t.deepEqual( 20 | mergeOptions.call({ignoreUndefined: true}, {nested: {unicorns: 'none'}}, {nested: undefined}), 21 | {nested: {unicorns: 'none'}} 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /test/prototype-pollution.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | const defineProtoProperty = (options, value) => Object.defineProperty(options, '__proto__', { 5 | value, 6 | writable: true, 7 | enumerable: true, 8 | configurable: true 9 | }); 10 | 11 | test('PoC by HoLyVieR', t => { 12 | const maliciousPayload = '{"__proto__":{"oops":"It works !"}}'; 13 | const a = {}; 14 | t.is(undefined, a.oops); 15 | mergeOptions({}, JSON.parse(maliciousPayload)); 16 | t.is(undefined, a.oops); 17 | }); 18 | 19 | test('array values (regression test)', t => { 20 | const array1 = []; 21 | const array2 = []; 22 | const pristine = []; 23 | defineProtoProperty(array2, {oops: 'It works !'}); 24 | t.is(undefined, pristine.oops); 25 | mergeOptions({array: array1}, {array: array2}); 26 | t.is(undefined, pristine.oops); 27 | }); 28 | 29 | test('recusive merge', t => { 30 | const a = {}; 31 | const b = defineProtoProperty({a}, {oops: 'It works !'}); 32 | t.is(undefined, b.a.oops); 33 | mergeOptions({a: {}}, b); 34 | t.is(undefined, b.a.oops); 35 | }); 36 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Michael Mayer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/readme-api.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | test('cloning example', async t => { 5 | const defaultPromise = Promise.reject(new Error()); 6 | const optionsPromise = Promise.resolve('bar'); 7 | const defaultOptions = { 8 | fn: () => false, 9 | promise: defaultPromise, 10 | array: ['foo'], 11 | nested: {unicorns: 'none'} 12 | }; 13 | const options = { 14 | fn: () => true, 15 | promise: optionsPromise, 16 | array: ['baz'], 17 | nested: {unicorns: 'many'} 18 | }; 19 | const result = mergeOptions(defaultOptions, options); 20 | t.deepEqual(result, options); 21 | t.is(result.fn, options.fn); 22 | t.is(result.promise, options.promise); 23 | t.not(result.array, options.array); 24 | t.not(result.nested, options.nested); 25 | await t.throwsAsync(defaultPromise); 26 | await t.notThrowsAsync(optionsPromise); 27 | }); 28 | 29 | test('array.concat example', t => { 30 | t.deepEqual( 31 | mergeOptions({patterns: ['src/**']}, {patterns: ['test/**']}), 32 | {patterns: ['test/**']} 33 | ); 34 | t.deepEqual( 35 | mergeOptions.call({concatArrays: true}, {patterns: ['src/**']}, {patterns: ['test/**']}), 36 | {patterns: ['src/**', 'test/**']} 37 | ); 38 | t.deepEqual( 39 | mergeOptions.apply({concatArrays: true}, [{patterns: ['src/**']}, {patterns: ['test/**']}]), 40 | {patterns: ['src/**', 'test/**']} 41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /test/user-extended-natives.js: -------------------------------------------------------------------------------- 1 | /* eslint no-extend-native:0, no-use-extend-native/no-use-extend-native:0 */ 2 | const test = require('ava'); 3 | const fn = require('..'); 4 | 5 | test('ignore non-own properties', t => { 6 | const optionObject = {foo: 'bar'}; 7 | 8 | Object.defineProperty(Object.prototype, 'TEST_NonOwnButEnumerable', { 9 | value: optionObject, 10 | configurable: true, 11 | enumerable: true 12 | }); 13 | const result = fn({}, optionObject, {baz: true}); 14 | 15 | t.true(result.baz); 16 | t.is(result.TEST_NonOwnButEnumerable, optionObject); 17 | t.false(Object.hasOwnProperty.call(result, 'TEST_NonOwnButEnumerable')); 18 | delete Object.prototype.TEST_NonOwnButEnumerable; 19 | t.false('TEST_NonOwnButEnumerable' in result); 20 | }); 21 | 22 | test('ignore non-enumerable properties', t => { 23 | const optionObject = Object.create(null); 24 | const key = Symbol('TEST_NonEnumerableButOwn'); 25 | 26 | Object.defineProperty(optionObject, key, { 27 | value: 42, 28 | configurable: true, 29 | enumerable: false 30 | }); 31 | const result = fn({}, optionObject, {baz: true}); 32 | 33 | if (Object.getOwnPropertySymbols) { 34 | const ownPropertySymbols = Object.getOwnPropertySymbols(result); 35 | t.deepEqual(ownPropertySymbols, []); 36 | } else { 37 | t.false(key in result); 38 | } 39 | 40 | t.not(result, optionObject); 41 | t.true(result.baz); 42 | }); 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "merge-options", 3 | "version": "3.0.4", 4 | "description": "Merge Option Objects", 5 | "license": "MIT", 6 | "repository": "schnittstabil/merge-options", 7 | "author": { 8 | "name": "Michael Mayer", 9 | "email": "michael@schnittstabil.de" 10 | }, 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "test": "xo && tsd && nyc ava", 16 | "lint": "xo", 17 | "unit": "ava", 18 | "typecheck": "tsd", 19 | "clean": "rimraf .nyc_output/ coverage/", 20 | "coverage-html": "nyc ava && nyc report --reporter=html" 21 | }, 22 | "main": "./index.js", 23 | "exports": { 24 | "require": "./index.js", 25 | "import": "./index.mjs" 26 | }, 27 | "files": [ 28 | "index.d.ts", 29 | "index.js", 30 | "index.mjs" 31 | ], 32 | "keywords": [ 33 | "merge", 34 | "options", 35 | "deep", 36 | "plain", 37 | "object", 38 | "extend", 39 | "clone" 40 | ], 41 | "devDependencies": { 42 | "ava": "^3.11.1", 43 | "coveralls": "^3.1.0", 44 | "nyc": "^15.1.0", 45 | "rimraf": "^3.0.2", 46 | "tsd": "^0.13.1", 47 | "xo": "^0.33.0" 48 | }, 49 | "dependencies": { 50 | "is-plain-obj": "^2.1.0" 51 | }, 52 | "xo": { 53 | "rules": { 54 | "import/extensions": "off", 55 | "import/no-useless-path-segments": "off", 56 | "unicorn/import-index": "off" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/arrays.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | test('support array values', t => { 5 | const array1 = ['foo', 'bar']; 6 | const array2 = ['baz']; 7 | const result = mergeOptions({array: array1}, {array: array2}); 8 | t.deepEqual(result, {array: array2}); 9 | t.not(result.array, array1); 10 | t.not(result.array, array2); 11 | }); 12 | 13 | test('support concatenation', t => { 14 | const array1 = ['foo']; 15 | const array2 = ['bar']; 16 | const result = mergeOptions.call({concatArrays: true}, {array: array1}, {array: array2}); 17 | t.deepEqual(result.array, ['foo', 'bar']); 18 | t.not(result.array, array1); 19 | t.not(result.array, array2); 20 | }); 21 | 22 | test('support concatenation via apply', t => { 23 | const array1 = ['foo']; 24 | const array2 = ['bar']; 25 | const result = mergeOptions.apply({concatArrays: true}, [{array: array1}, {array: array2}]); 26 | t.deepEqual(result.array, ['foo', 'bar']); 27 | t.not(result.array, array1); 28 | t.not(result.array, array2); 29 | }); 30 | 31 | test('support concatenation of sparsed arrays', t => { 32 | const sparseArray1 = []; 33 | const sparseArray2 = []; 34 | sparseArray1[2] = 42; 35 | sparseArray2[5] = 'unicorns'; 36 | const result = mergeOptions.call({concatArrays: true}, {foo: sparseArray1}, {foo: sparseArray2}); 37 | t.deepEqual(result.foo, [42, 'unicorns']); 38 | t.not(result.array, sparseArray1); 39 | t.not(result.array, sparseArray2); 40 | }); 41 | 42 | test('support concatenation of sparsed arrays via apply', t => { 43 | const sparseArray1 = []; 44 | const sparseArray2 = []; 45 | sparseArray1[2] = 42; 46 | sparseArray2[5] = 'unicorns'; 47 | const result = mergeOptions.apply({concatArrays: true}, [{foo: sparseArray1}, {foo: sparseArray2}]); 48 | t.deepEqual(result.foo, [42, 'unicorns']); 49 | t.not(result.array, sparseArray1); 50 | t.not(result.array, sparseArray2); 51 | }); 52 | 53 | test('clone option objects', t => { 54 | const plainObject1 = {value: 'foo'}; 55 | const plainObject2 = {value: 'bar'}; 56 | const result = mergeOptions({array: [plainObject1]}, {array: [plainObject2]}); 57 | t.deepEqual(result.array, [plainObject2]); 58 | t.not(result.array[0], plainObject1); 59 | t.not(result.array[0], plainObject2); 60 | }); 61 | -------------------------------------------------------------------------------- /test/option-values.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const mergeOptions = require('..'); 3 | 4 | function toString(value) { 5 | try { 6 | return String(value); 7 | } catch { 8 | return typeof value; 9 | } 10 | } 11 | 12 | test('throw TypeError on non-option-objects', async t => { 13 | const promise = Promise.reject(new Error()); 14 | [ 15 | 42, 16 | 'unicorn', 17 | new Date(), 18 | promise, 19 | Symbol('unicorn'), 20 | /regexp/, 21 | function () {}, 22 | null 23 | ].forEach(value => { 24 | t.throws(() => mergeOptions(value), {instanceOf: TypeError}, toString(value)); 25 | t.throws(() => mergeOptions({}, value), {instanceOf: TypeError}, toString(value)); 26 | t.throws(() => mergeOptions({foo: 'bar'}, value), {instanceOf: TypeError}, toString(value)); 27 | t.throws(() => mergeOptions(Object.create(null), value), {instanceOf: TypeError}, toString(value)); 28 | }); 29 | 30 | await t.throwsAsync(promise); 31 | }); 32 | 33 | test('support `undefined` Option Values', t => { 34 | t.deepEqual(mergeOptions({foo: true}, {foo: undefined}), {foo: undefined}); 35 | }); 36 | 37 | test('support undefined as target, null as source', t => { 38 | const result = mergeOptions({foo: undefined}, {foo: null}); 39 | t.is(result.foo, null); 40 | }); 41 | 42 | test('support null as target, undefined as source', t => { 43 | const result = mergeOptions({foo: null}, {foo: undefined}); 44 | t.is(result.foo, undefined); 45 | }); 46 | 47 | test('support Date as target, Number as source', t => { 48 | const result = mergeOptions({date: new Date()}, {date: 990741600000}); 49 | t.is(result.date, 990741600000); 50 | t.is(result.date.constructor, Number); 51 | }); 52 | 53 | test('support Date as target, Date as source', t => { 54 | const result = mergeOptions({date: new Date()}, {date: new Date(990741600000)}); 55 | t.is(result.date.getTime(), 990741600000); 56 | t.is(result.date.constructor, Date); 57 | }); 58 | 59 | test('support RegExp as target, String as source', t => { 60 | const result = mergeOptions({regexp: /reg/}, {regexp: 'string'}); 61 | t.is(result.regexp.constructor, String); 62 | t.is(result.regexp, 'string'); 63 | }); 64 | 65 | test('support RegExp as target, RegExp as source', t => { 66 | const result = mergeOptions({regexp: /reg/}, {regexp: /new/}); 67 | t.is(result.regexp.constructor, RegExp); 68 | t.is(result.regexp.test('new'), true); 69 | }); 70 | 71 | test('support Promise as target, Number as source', t => { 72 | const promise1 = Promise.resolve(666); 73 | const promise2 = 42; 74 | const result = mergeOptions({promise: promise1}, {promise: promise2}); 75 | t.is(result.promise.constructor, Number); 76 | t.is(result.promise, 42); 77 | }); 78 | 79 | test('support Promise as target, Promise as source', async t => { 80 | const promise1 = Promise.resolve(666); 81 | const promise2 = Promise.resolve(42); 82 | const result = mergeOptions({promise: promise1}, {promise: promise2}); 83 | t.is(result.promise.constructor, Promise); 84 | t.is(await result.promise, 42); 85 | }); 86 | 87 | test('support user-defined object as target, user-defined object as source', t => { 88 | function User(firstName) { 89 | this.firstName = firstName; 90 | } 91 | 92 | const alice = new User('Alice'); 93 | const bob = new User('Bob'); 94 | const result = mergeOptions({user: alice}, {user: bob}); 95 | t.is(result.user.constructor, User); 96 | t.is(result.user, bob); 97 | t.is(result.user.firstName, 'Bob'); 98 | }); 99 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # merge-options [![Build Status](https://travis-ci.org/schnittstabil/merge-options.svg?branch=master)](https://travis-ci.org/schnittstabil/merge-options) [![Coverage Status](https://coveralls.io/repos/schnittstabil/merge-options/badge.svg?branch=master&service=github)](https://coveralls.io/github/schnittstabil/merge-options?branch=master) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 2 | 3 | 4 | > Merge Option Objects 5 | 6 | `merge-options` considers [plain objects](https://github.com/sindresorhus/is-plain-obj) as *Option Objects*, everything else as *Option Values*. 7 | 8 | ## Install 9 | 10 | ``` 11 | $ npm install --save merge-options 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```js 17 | const mergeOptions = require('merge-options'); 18 | 19 | mergeOptions({foo: 0}, {bar: 1}, {baz: 2}, {bar: 3}) 20 | //=> {foo: 0, bar: 3, baz: 2} 21 | 22 | mergeOptions({nested: {unicorns: 'none'}}, {nested: {unicorns: 'many'}}) 23 | //=> {nested: {unicorns: 'many'}} 24 | 25 | mergeOptions({[Symbol.for('key')]: 0}, {[Symbol.for('key')]: 42}) 26 | //=> {Symbol(key): 42} 27 | ``` 28 | 29 | ### Usage with custom config 30 | 31 | ```js 32 | const mergeOptions = require('merge-options').bind({ignoreUndefined: true}); 33 | 34 | mergeOptions({foo: 'bar'}, {foo: undefined}) 35 | //=> {foo: 'bar'} 36 | ``` 37 | 38 | ## API 39 | 40 | ### mergeOptions(option1, ...options)
mergeOptions.call(config, option1, ...options)
mergeOptions.apply(config, [option1, ...options]) 41 | 42 | `mergeOptions` recursively merges one or more *Option Objects* into a new one and returns that. The `options` are merged in order, thus *Option Values* of additional `options` take precedence over previous ones. 43 | 44 | The merging does not alter the passed `option` arguments, taking roughly the following steps: 45 | * recursively cloning[1] *Option Objects* and [arrays](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) until reaching *Option Values* 46 | * copying[1] references to *Option Values* to the result object 47 | 48 | 49 | ```js 50 | const defaultOpts = { 51 | fn: () => false, // functions are Option Values 52 | promise: Promise.reject(new Error()), // all non-plain objects are Option Values 53 | array: ['foo'], // arrays are Option Values 54 | nested: {unicorns: 'none'} // {…} is plain, therefore an Option Object 55 | }; 56 | 57 | const opts = { 58 | fn: () => true, // [1] 59 | promise: Promise.resolve('bar'), // [2] 60 | array: ['baz'], // [3] 61 | nested: {unicorns: 'many'} // [4] 62 | }; 63 | 64 | mergeOptions(defaultOpts, opts) 65 | //=> 66 | { 67 | fn: [Function], // === [1] 68 | promise: Promise { 'bar' }, // === [2] 69 | array: ['baz'], // !== [3] (arrays are cloned) 70 | nested: {unicorns: 'many'} // !== [4] (Option Objects are cloned) 71 | } 72 | ``` 73 | 74 | #### config 75 | 76 | Type: `object` 77 | 78 | ##### config.concatArrays 79 | 80 | Type: `boolean`
Default: `false` 81 | 82 | Concatenate arrays: 83 | 84 | ```js 85 | mergeOptions({src: ['src/**']}, {src: ['test/**']}) 86 | //=> {src: ['test/**']} 87 | 88 | // Via call 89 | mergeOptions.call({concatArrays: true}, {src: ['src/**']}, {src: ['test/**']}) 90 | //=> {src: ['src/**', 'test/**']} 91 | 92 | // Via apply 93 | mergeOptions.apply({concatArrays: true}, [{src: ['src/**']}, {src: ['test/**']}]) 94 | //=> {src: ['src/**', 'test/**']} 95 | ``` 96 | 97 | ##### config.ignoreUndefined 98 | 99 | Type: `boolean`
Default: `false` 100 | 101 | Ignore undefined values: 102 | 103 | ```js 104 | mergeOptions({foo: 'bar'}, {foo: undefined}) 105 | //=> {foo: undefined} 106 | 107 | // Via call 108 | mergeOptions.call({ignoreUndefined: true}, {foo: 'bar'}, {foo: undefined}) 109 | //=> {foo: 'bar'} 110 | 111 | // Via apply 112 | mergeOptions.apply({ignoreUndefined: true}, [{foo: 'bar'}, {foo: undefined}]) 113 | //=> {foo: 'bar'} 114 | ``` 115 | 116 | 117 | ## Related 118 | 119 | * See [object-assign](https://github.com/sindresorhus/object-assign) if you need a ES2015 Object.assign() ponyfill 120 | * See [deep-assign](https://github.com/sindresorhus/deep-assign) if you need to do Object.assign() recursively 121 | 122 | ## Notes 123 | 124 |
    125 |
  1. copying and cloning take only enumerable own properties into account
  2. 126 |
127 | 128 | ## License 129 | 130 | MIT © [Michael Mayer](http://schnittstabil.de) 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const isOptionObject = require('is-plain-obj'); 3 | 4 | const {hasOwnProperty} = Object.prototype; 5 | const {propertyIsEnumerable} = Object; 6 | const defineProperty = (object, name, value) => Object.defineProperty(object, name, { 7 | value, 8 | writable: true, 9 | enumerable: true, 10 | configurable: true 11 | }); 12 | 13 | const globalThis = this; 14 | const defaultMergeOptions = { 15 | concatArrays: false, 16 | ignoreUndefined: false 17 | }; 18 | 19 | const getEnumerableOwnPropertyKeys = value => { 20 | const keys = []; 21 | 22 | for (const key in value) { 23 | if (hasOwnProperty.call(value, key)) { 24 | keys.push(key); 25 | } 26 | } 27 | 28 | /* istanbul ignore else */ 29 | if (Object.getOwnPropertySymbols) { 30 | const symbols = Object.getOwnPropertySymbols(value); 31 | 32 | for (const symbol of symbols) { 33 | if (propertyIsEnumerable.call(value, symbol)) { 34 | keys.push(symbol); 35 | } 36 | } 37 | } 38 | 39 | return keys; 40 | }; 41 | 42 | function clone(value) { 43 | if (Array.isArray(value)) { 44 | return cloneArray(value); 45 | } 46 | 47 | if (isOptionObject(value)) { 48 | return cloneOptionObject(value); 49 | } 50 | 51 | return value; 52 | } 53 | 54 | function cloneArray(array) { 55 | const result = array.slice(0, 0); 56 | 57 | getEnumerableOwnPropertyKeys(array).forEach(key => { 58 | defineProperty(result, key, clone(array[key])); 59 | }); 60 | 61 | return result; 62 | } 63 | 64 | function cloneOptionObject(object) { 65 | const result = Object.getPrototypeOf(object) === null ? Object.create(null) : {}; 66 | 67 | getEnumerableOwnPropertyKeys(object).forEach(key => { 68 | defineProperty(result, key, clone(object[key])); 69 | }); 70 | 71 | return result; 72 | } 73 | 74 | /** 75 | * @param {*} merged already cloned 76 | * @param {*} source something to merge 77 | * @param {string[]} keys keys to merge 78 | * @param {Object} config Config Object 79 | * @returns {*} cloned Object 80 | */ 81 | const mergeKeys = (merged, source, keys, config) => { 82 | keys.forEach(key => { 83 | if (typeof source[key] === 'undefined' && config.ignoreUndefined) { 84 | return; 85 | } 86 | 87 | // Do not recurse into prototype chain of merged 88 | if (key in merged && merged[key] !== Object.getPrototypeOf(merged)) { 89 | defineProperty(merged, key, merge(merged[key], source[key], config)); 90 | } else { 91 | defineProperty(merged, key, clone(source[key])); 92 | } 93 | }); 94 | 95 | return merged; 96 | }; 97 | 98 | /** 99 | * @param {*} merged already cloned 100 | * @param {*} source something to merge 101 | * @param {Object} config Config Object 102 | * @returns {*} cloned Object 103 | * 104 | * see [Array.prototype.concat ( ...arguments )](http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.concat) 105 | */ 106 | const concatArrays = (merged, source, config) => { 107 | let result = merged.slice(0, 0); 108 | let resultIndex = 0; 109 | 110 | [merged, source].forEach(array => { 111 | const indices = []; 112 | 113 | // `result.concat(array)` with cloning 114 | for (let k = 0; k < array.length; k++) { 115 | if (!hasOwnProperty.call(array, k)) { 116 | continue; 117 | } 118 | 119 | indices.push(String(k)); 120 | 121 | if (array === merged) { 122 | // Already cloned 123 | defineProperty(result, resultIndex++, array[k]); 124 | } else { 125 | defineProperty(result, resultIndex++, clone(array[k])); 126 | } 127 | } 128 | 129 | // Merge non-index keys 130 | result = mergeKeys(result, array, getEnumerableOwnPropertyKeys(array).filter(key => !indices.includes(key)), config); 131 | }); 132 | 133 | return result; 134 | }; 135 | 136 | /** 137 | * @param {*} merged already cloned 138 | * @param {*} source something to merge 139 | * @param {Object} config Config Object 140 | * @returns {*} cloned Object 141 | */ 142 | function merge(merged, source, config) { 143 | if (config.concatArrays && Array.isArray(merged) && Array.isArray(source)) { 144 | return concatArrays(merged, source, config); 145 | } 146 | 147 | if (!isOptionObject(source) || !isOptionObject(merged)) { 148 | return clone(source); 149 | } 150 | 151 | return mergeKeys(merged, source, getEnumerableOwnPropertyKeys(source), config); 152 | } 153 | 154 | module.exports = function (...options) { 155 | const config = merge(clone(defaultMergeOptions), (this !== globalThis && this) || {}, defaultMergeOptions); 156 | let merged = {_: {}}; 157 | 158 | for (const option of options) { 159 | if (option === undefined) { 160 | continue; 161 | } 162 | 163 | if (!isOptionObject(option)) { 164 | throw new TypeError('`' + option + '` is not an Option Object'); 165 | } 166 | 167 | merged = merge(merged, {_: option}, config); 168 | } 169 | 170 | return merged._; 171 | }; 172 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | } 69 | } 70 | --------------------------------------------------------------------------------