mergekit
Version:
Uniquely flexible and light-weight utility for cloning and deep (recursive) merging of JavaScript objects. Supports descriptor values, accessor functions, and custom prototypes. Provides advanced options for customizing the clone/merge process.
368 lines (367 loc) • 12 kB
JavaScript
// src/utils.ts
function countOccurrences(...arrays) {
const countObj = {};
arrays.forEach((array) => {
array.forEach((v) => {
const key = String(v);
countObj[key] = key in countObj ? ++countObj[key] : 1;
});
});
return countObj;
}
function getInAll(...arrays) {
return arrays.reduce(
(acc, curr) => acc.filter((value) => new Set(curr).has(value))
);
}
function getInMultiple(...arrays) {
const countObj = countOccurrences(...arrays);
return Object.keys(countObj).filter((v) => countObj[v] > 1).map((key) => parseValue(key));
}
function getNotInAll(...arrays) {
const countObj = countOccurrences(...arrays);
return Object.keys(countObj).filter((v) => countObj[v] < arrays.length).map((key) => parseValue(key));
}
function getNotInMultiple(...arrays) {
const countObj = countOccurrences(...arrays);
return Object.keys(countObj).filter((v) => countObj[v] === 1).map((key) => parseValue(key));
}
function getObjectKeys(obj, hoistEnumerable = false) {
const keys = Object.getOwnPropertyNames(obj);
if (hoistEnumerable) {
for (const key in obj) {
if (!keys.includes(key)) {
keys.push(key);
}
}
}
return keys;
}
function isObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isPropDescriptor(obj) {
if (!isObject(obj)) {
return false;
}
const hasFlagKey = ["writable", "enumerable", "configurable"].some(
(key) => key in obj
);
const hasMethod = ["get", "set"].some((key) => typeof obj[key] === "function");
const hasMethodKeys = ["get", "set"].every((key) => key in obj);
let isDescriptor = "value" in obj && hasFlagKey || hasMethod && (hasMethodKeys || hasFlagKey);
if (isDescriptor) {
const validKeys = /* @__PURE__ */ new Set([
"configurable",
"get",
"set",
"enumerable",
"value",
"writable"
]);
isDescriptor = Object.keys(obj).every((key) => validKeys.has(key));
}
return isDescriptor;
}
function parseValue(key) {
try {
return JSON.parse(key);
} catch {
return key;
}
}
// src/index.ts
var defaults = {
// Keys
onlyKeys: [],
skipKeys: [],
onlyCommonKeys: false,
onlyUniversalKeys: false,
skipCommonKeys: false,
skipUniversalKeys: false,
onlyObjectWithKeyValues: [],
// Values
invokeGetters: false,
skipSetters: false,
// Arrays
appendArrays: false,
prependArrays: false,
dedupArrays: false,
sortArrays: false,
// Prototype
hoistEnumerable: false,
hoistProto: false,
skipProto: false,
// Callbacks
onCircular: () => {
}
};
function mergekit(objects, options = defaults) {
const settings = { ...defaults, ...options };
const dedupArrayMap = /* @__PURE__ */ new Map();
const sortArrayMap = /* @__PURE__ */ new Map();
const sortArrayFn = typeof settings.sortArrays === "function" ? settings.sortArrays : void 0;
const circularRefs = /* @__PURE__ */ new WeakMap();
let mergeDepth = 0;
function _getObjectKeys(obj) {
return getObjectKeys(obj, settings.hoistEnumerable);
}
function _mergekit(...objects2) {
let mergeKeyList;
if (objects2.length > 1) {
if (settings.onlyCommonKeys) {
mergeKeyList = getInMultiple(
...objects2.map((obj) => _getObjectKeys(obj))
);
} else if (settings.onlyUniversalKeys) {
mergeKeyList = getInAll(...objects2.map((obj) => _getObjectKeys(obj)));
} else if (settings.skipCommonKeys) {
mergeKeyList = getNotInMultiple(
...objects2.map((obj) => _getObjectKeys(obj))
);
} else if (settings.skipUniversalKeys) {
mergeKeyList = getNotInAll(...objects2.map((obj) => _getObjectKeys(obj)));
}
}
if (!mergeKeyList && settings.onlyKeys.length) {
mergeKeyList = settings.onlyKeys;
}
if (mergeKeyList && mergeKeyList !== settings.onlyKeys && settings.onlyKeys.length) {
mergeKeyList = mergeKeyList.filter(
(key) => settings.onlyKeys.includes(key)
);
}
const newObjProps = objects2.reduce((targetObj, srcObj) => {
circularRefs.set(srcObj, targetObj);
let keys = mergeKeyList || _getObjectKeys(srcObj);
if (settings.skipKeys.length) {
keys = keys.filter((key) => settings.skipKeys.indexOf(key) === -1);
}
if (settings.onlyObjectWithKeyValues.length > 0) {
const hasValue = settings.onlyObjectWithKeyValues.every(
({ key, value }) => {
if (!Object.keys(srcObj).includes(key)) {
return true;
}
return srcObj[key] === value;
}
);
if (!hasValue) {
return targetObj;
}
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const targetVal = targetObj[key];
const mergeDescriptor = {
configurable: true,
enumerable: true
};
if (key in srcObj === false) {
continue;
}
let isReturnVal = false;
let mergeVal = srcObj[key];
const srcDescriptor = Object.getOwnPropertyDescriptor(srcObj, key);
const isSetterOnly = srcDescriptor && typeof srcDescriptor.set === "function" && typeof srcDescriptor.get !== "function";
if (isSetterOnly) {
if (!settings.skipSetters) {
Object.defineProperty(targetObj, key, srcDescriptor);
}
continue;
}
if (settings.filter !== defaults.filter) {
const returnVal = settings.filter({
depth: mergeDepth,
key,
srcObj,
srcVal: mergeVal,
targetObj,
targetVal
});
if (returnVal !== void 0 && !returnVal) {
continue;
}
}
if (settings.beforeEach !== defaults.beforeEach) {
const returnVal = settings.beforeEach({
depth: mergeDepth,
key,
srcObj,
srcVal: mergeVal,
targetObj,
targetVal
});
if (returnVal !== void 0) {
isReturnVal = true;
mergeVal = returnVal;
}
}
if (settings.onCircular && typeof mergeVal === "object" && mergeVal !== null) {
if (circularRefs.has(srcObj[key])) {
const returnVal = settings.onCircular({
depth: mergeDepth,
key,
srcObj,
srcVal: srcObj[key],
targetObj,
targetVal
});
if (returnVal === void 0) {
mergeVal = circularRefs.get(srcObj[key]);
targetObj[key] = mergeVal;
continue;
}
isReturnVal = true;
mergeVal = returnVal;
}
}
if (Array.isArray(mergeVal)) {
mergeVal = [...mergeVal];
if (Array.isArray(targetVal)) {
if (settings.appendArrays) {
mergeVal = [...targetVal, ...mergeVal];
} else if (settings.prependArrays) {
mergeVal = [...mergeVal, ...targetVal];
}
}
if (settings.dedupArrays) {
if (settings.afterEach !== defaults.afterEach) {
mergeVal = [...new Set(mergeVal)];
} else {
const keyArray = dedupArrayMap.get(targetObj);
if (keyArray && !keyArray.includes(key)) {
keyArray.push(key);
} else {
dedupArrayMap.set(targetObj, [key]);
}
}
}
if (settings.sortArrays) {
if (settings.afterEach !== defaults.afterEach) {
mergeVal = mergeVal.sort(sortArrayFn);
} else {
const keyArray = sortArrayMap.get(targetObj);
if (keyArray && !keyArray.includes(key)) {
keyArray.push(key);
} else {
sortArrayMap.set(targetObj, [key]);
}
}
}
} else if (mergeVal instanceof Date) {
mergeVal = new Date(mergeVal);
} else if (Buffer.isBuffer(mergeVal)) {
mergeVal = mergeVal.toString("utf-8");
} else if (isObject(mergeVal) && (!isReturnVal || !isPropDescriptor(mergeVal))) {
mergeDepth++;
if (isObject(targetVal)) {
mergeVal = _mergekit(targetVal, mergeVal);
} else {
mergeVal = _mergekit(mergeVal);
}
mergeDepth--;
}
if (settings.afterEach !== defaults.afterEach) {
const returnVal = settings.afterEach({
depth: mergeDepth,
key,
mergeVal,
srcObj,
targetObj
});
if (returnVal !== void 0) {
isReturnVal = true;
mergeVal = returnVal;
}
}
if (isReturnVal) {
const returnDescriptor = isPropDescriptor(mergeVal) ? mergeVal : {
configurable: true,
enumerable: true,
value: mergeVal,
writable: true
};
Object.defineProperty(targetObj, key, returnDescriptor);
continue;
}
if (srcDescriptor) {
const { configurable, enumerable, get, set, writable } = srcDescriptor;
Object.assign(mergeDescriptor, {
configurable,
enumerable
});
if (typeof get === "function") {
if (settings.invokeGetters) {
mergeDescriptor.value = mergeVal;
} else {
mergeDescriptor.get = get;
}
}
if (!settings.skipSetters && typeof set === "function" && !("value" in mergeDescriptor)) {
mergeDescriptor.set = set;
}
if (!("get" in mergeDescriptor) && !("set" in mergeDescriptor)) {
mergeDescriptor.writable = Boolean(writable);
}
}
if (!mergeDescriptor.get && !mergeDescriptor.set && !("value" in mergeDescriptor)) {
mergeDescriptor.value = mergeVal;
mergeDescriptor.writable = srcDescriptor && "writable" in srcDescriptor ? srcDescriptor.writable : true;
}
Object.defineProperty(targetObj, key, mergeDescriptor);
}
return targetObj;
}, {});
for (const [obj, keyArray] of dedupArrayMap.entries()) {
for (const key of keyArray) {
const propDescriptor = Object.getOwnPropertyDescriptor(obj, key);
const { configurable, enumerable, writable } = propDescriptor;
let value = [...new Set(obj[key])];
if (Array.isArray(obj[key]) && typeof obj[key][0] === "object") {
value = [...new Set(obj[key].map((item) => JSON.stringify(item)))];
value = value.map((item) => JSON.parse(item));
}
Object.defineProperty(obj, key, {
configurable,
enumerable,
value,
writable: writable !== void 0 ? writable : true
});
}
}
for (const [obj, keyArray] of sortArrayMap.entries()) {
for (const key of keyArray) {
obj[key].sort(sortArrayFn);
}
}
let newObj = newObjProps;
if (!settings.skipProto) {
const customProtos = objects2.reduce((protosArr, obj) => {
const proto = Object.getPrototypeOf(obj);
if (proto && proto !== Object.prototype) {
protosArr.push(proto);
}
return protosArr;
}, []);
if (customProtos.length) {
const newObjProto = _mergekit(...customProtos);
if (settings.hoistProto) {
newObj = _mergekit(newObjProto, newObjProps);
} else {
newObj = Object.create(
newObjProto,
Object.getOwnPropertyDescriptors(newObjProps)
);
}
}
}
return newObj;
}
const objectsArray = Array.isArray(objects) ? objects : [objects];
return _mergekit(...objectsArray);
}
export {
mergekit
};
//# sourceMappingURL=index.js.map