xuxi
Version:
Dynamically utility for combining different types of values into a single value.
145 lines (144 loc) • 5.86 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.object = void 0;
exports.isPlainObject = isPlainObject;
exports.clean = clean;
/**
* Checks if a given value is a plain object (i.e., not an array or null).
* @param value - The value to check.
* @returns True if the value is a plain object, otherwise false.
*/
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
/**
* Merges multiple objects deeply, handling arrays and functions gracefully.
* @template T - The base object type.
* @param obj - One or more objects to merge.
* @returns The deeply merged object.
*/
function baseObject(...obj) {
const seen = new WeakMap(); // Use WeakMap to store processed objects
function merge(acc, input) {
if (!input)
return acc;
if (isPlainObject(input)) {
if (seen.has(input))
return seen.get(input); // If there is one, use previous result.
const newAcc = { ...acc }; // Copy acc so as not to change direct references
seen.set(input, newAcc); // Mark objects as processed
acc = newAcc; // Use copied version
}
if (Array.isArray(input))
return { ...acc, ...baseObject(...input) };
if (typeof input === 'function') {
const result = input(acc);
return isPlainObject(result) ? merge(acc, result) : { ...acc, ...baseObject(result) };
}
if (isPlainObject(input)) {
Reflect.ownKeys(input).forEach(key => {
const value = input[key];
if (isPlainObject(value) && isPlainObject(acc[key])) {
acc[key] = merge(acc[key], value);
}
else {
acc[key] = value;
}
});
return acc;
}
return acc;
}
return obj.reduce((acc, input) => merge(acc, input), {});
}
/**
* Merges multiple objects deeply, handling arrays and functions gracefully **without overwriting**.
* @template T - The base object type.
* @param obj - One or more objects to merge.
* @returns The deeply merged object **without overwriting** the value at the first key, only change the value if it does not exist.
*/
function preserveRoot(...obj) {
const seen = new WeakMap();
function merge(acc, input) {
if (!input)
return acc;
if (isPlainObject(input)) {
if (seen.has(input))
return seen.get(input);
const newAcc = { ...acc };
seen.set(input, newAcc);
acc = newAcc;
}
if (Array.isArray(input))
return { ...acc, ...preserveRoot(...input) };
if (typeof input === 'function') {
const result = input(acc);
return isPlainObject(result) ? merge(acc, result) : { ...acc, ...preserveRoot(result) };
}
if (isPlainObject(input)) {
Reflect.ownKeys(input).forEach(key => {
const value = input[key];
if (acc[key] === undefined) {
acc[key] = value; // Only change the value if it does not exist
}
else if (isPlainObject(value) && isPlainObject(acc[key])) {
acc[key] = merge(acc[key], value);
}
});
return acc;
}
return acc;
}
return obj.reduce((acc, input) => merge(acc, input), {});
}
/**
* Recursively removes falsy values from an object, except those specified in `exclude`.
* @template T - The object type.
* @param obj - The object to clean.
* @param exclude - An array of values to be preserved even if they are falsy (default: `[]`).
* @param seen - To detect cyclic references (default: `new WeakSet<object>()`).
* @returns A new object without the falsy values.
* @example
* @see {@link https://ilkhoeri.github.io/xuxi/clean Docs}
*/
function clean(obj, exclude = [], seen = new WeakSet()) {
const excludeSet = new Set(exclude);
if (seen.has(obj))
return obj; // Avoid infinite loops
seen.add(obj); // Mark object as visited
return Reflect.ownKeys(obj).reduce((acc, key) => {
const value = obj[key];
if (isPlainObject(value)) {
const cleanedObject = clean(value, exclude, seen); // Clean objects recursively
// Ensure the object is not empty before inserting
if (Object.keys(cleanedObject).length > 0 || typeof key === 'symbol')
acc[key] = cleanedObject;
}
else if (Array.isArray(value)) {
// Clear every element in the array, remove empty objects
const cleanedArray = value
.map(item => (isPlainObject(item) ? clean(item, exclude, seen) : item))
.filter(item => (item && !(isPlainObject(item) && Object.keys(item).length === 0)) || excludeSet.has(item));
if (cleanedArray.length > 0)
acc[key] = cleanedArray;
}
else if (value || excludeSet.has(value) || typeof key === 'symbol') {
// Save the value if it is not falsy or belongs to `excludeSet`
acc[key] = value;
}
return acc;
}, {});
}
/**
* Recursively merge objects with support for arrays, dynamic functions, and non falsy properties into a single object.
*
* Provides a chaining:
* - {@link baseObject raw} method to **get falsy values** from the result.
* - {@link preserveRoot preserve} method to join **without overwriting** first value.
* @example
* @see {@link https://ilkhoeri.github.io/xuxi/?id=object Docs}
*/
const object = (...obj) => clean(baseObject(...obj), [0]);
exports.object = object;
exports.object.raw = baseObject;
exports.object.preserve = preserveRoot;