index.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. 'use strict';
  2. const isOptionObject = require('is-plain-obj');
  3. const hasOwnProperty = Object.prototype.hasOwnProperty;
  4. const propIsEnumerable = Object.propertyIsEnumerable;
  5. const defineProperty = (obj, name, value) => Object.defineProperty(obj, name, {
  6. value,
  7. writable: true,
  8. enumerable: true,
  9. configurable: true
  10. });
  11. const globalThis = this;
  12. const defaultMergeOpts = {
  13. concatArrays: false
  14. };
  15. const getEnumerableOwnPropertyKeys = value => {
  16. const keys = [];
  17. for (const key in value) {
  18. if (hasOwnProperty.call(value, key)) {
  19. keys.push(key);
  20. }
  21. }
  22. /* istanbul ignore else */
  23. if (Object.getOwnPropertySymbols) {
  24. const symbols = Object.getOwnPropertySymbols(value);
  25. for (let i = 0; i < symbols.length; i++) {
  26. if (propIsEnumerable.call(value, symbols[i])) {
  27. keys.push(symbols[i]);
  28. }
  29. }
  30. }
  31. return keys;
  32. };
  33. function clone(value) {
  34. if (Array.isArray(value)) {
  35. return cloneArray(value);
  36. }
  37. if (isOptionObject(value)) {
  38. return cloneOptionObject(value);
  39. }
  40. return value;
  41. }
  42. function cloneArray(array) {
  43. const result = array.slice(0, 0);
  44. getEnumerableOwnPropertyKeys(array).forEach(key => {
  45. defineProperty(result, key, clone(array[key]));
  46. });
  47. return result;
  48. }
  49. function cloneOptionObject(obj) {
  50. const result = Object.getPrototypeOf(obj) === null ? Object.create(null) : {};
  51. getEnumerableOwnPropertyKeys(obj).forEach(key => {
  52. defineProperty(result, key, clone(obj[key]));
  53. });
  54. return result;
  55. }
  56. /**
  57. * @param merged {already cloned}
  58. * @return {cloned Object}
  59. */
  60. const mergeKeys = (merged, source, keys, mergeOpts) => {
  61. keys.forEach(key => {
  62. // Do not recurse into prototype chain of merged
  63. if (key in merged && merged[key] !== Object.getPrototypeOf(merged)) {
  64. defineProperty(merged, key, merge(merged[key], source[key], mergeOpts));
  65. } else {
  66. defineProperty(merged, key, clone(source[key]));
  67. }
  68. });
  69. return merged;
  70. };
  71. /**
  72. * @param merged {already cloned}
  73. * @return {cloned Object}
  74. *
  75. * see [Array.prototype.concat ( ...arguments )](http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.concat)
  76. */
  77. const concatArrays = (merged, source, mergeOpts) => {
  78. let result = merged.slice(0, 0);
  79. let resultIndex = 0;
  80. [merged, source].forEach(array => {
  81. const indices = [];
  82. // `result.concat(array)` with cloning
  83. for (let k = 0; k < array.length; k++) {
  84. if (!hasOwnProperty.call(array, k)) {
  85. continue;
  86. }
  87. indices.push(String(k));
  88. if (array === merged) {
  89. // Already cloned
  90. defineProperty(result, resultIndex++, array[k]);
  91. } else {
  92. defineProperty(result, resultIndex++, clone(array[k]));
  93. }
  94. }
  95. // Merge non-index keys
  96. result = mergeKeys(result, array, getEnumerableOwnPropertyKeys(array).filter(key => {
  97. return indices.indexOf(key) === -1;
  98. }), mergeOpts);
  99. });
  100. return result;
  101. };
  102. /**
  103. * @param merged {already cloned}
  104. * @return {cloned Object}
  105. */
  106. function merge(merged, source, mergeOpts) {
  107. if (mergeOpts.concatArrays && Array.isArray(merged) && Array.isArray(source)) {
  108. return concatArrays(merged, source, mergeOpts);
  109. }
  110. if (!isOptionObject(source) || !isOptionObject(merged)) {
  111. return clone(source);
  112. }
  113. return mergeKeys(merged, source, getEnumerableOwnPropertyKeys(source), mergeOpts);
  114. }
  115. module.exports = function () {
  116. const mergeOpts = merge(clone(defaultMergeOpts), (this !== globalThis && this) || {}, defaultMergeOpts);
  117. let merged = {foobar: {}};
  118. for (let i = 0; i < arguments.length; i++) {
  119. const option = arguments[i];
  120. if (option === undefined) {
  121. continue;
  122. }
  123. if (!isOptionObject(option)) {
  124. throw new TypeError('`' + option + '` is not an Option Object');
  125. }
  126. merged = merge(merged, {foobar: option}, mergeOpts);
  127. }
  128. return merged.foobar;
  129. };