Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/helpers/src/is.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,14 @@ export const isObject = (value) => value !== null && typeof value === 'object' &
* @returns {boolean} True if the value is undefined, false otherwise.
*/
export const isUndefined = (value) => typeof value === 'undefined';

/**
* Checks if the given value is a plain object (object literal).
* @param {*} obj - The value to check.
* @returns {boolean} True if the value is a plain object, false otherwise.
*/
export const isPlainObject = (obj) => {
if (Object.prototype.toString.call(obj) !== '[object Object]') return false;
const proto = Object.getPrototypeOf(obj);
return proto === null || proto === Object.prototype;
};
51 changes: 50 additions & 1 deletion packages/helpers/src/is.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isArray, isEqual, isObject, isUndefined } from './index.js';
import { isArray, isEqual, isObject, isPlainObject, isUndefined } from './index.js';

describe('helpers', () => {
describe('is', () => {
Expand Down Expand Up @@ -60,5 +60,54 @@ describe('helpers', () => {
expect(isUndefined('')).toBe(false);
});
});

describe('isPlainObject', () => {
it('should return true for plain objects', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ foo: 'bar' })).toBe(true);
expect(isPlainObject(Object.create(null))).toBe(true);
expect(isPlainObject(Object.create(Object.prototype))).toBe(true);
expect(isPlainObject(Object.assign({}, { a: 1 }))).toBe(true);
});

it('should return false for primitives', () => {
expect(isPlainObject(true)).toBe(false);
expect(isPlainObject(undefined)).toBe(false);
expect(isPlainObject(1)).toBe(false);
expect(isPlainObject('string')).toBe(false);
expect(isPlainObject(Symbol('s'))).toBe(false);
});

it('should return false for `null`', () => {
expect(isPlainObject(null)).toBe(false);
});

it('should return false for functions and instances', () => {
function Foo() {
this.abc = {};
}

expect(isPlainObject(Foo)).toBe(false);
expect(isPlainObject(new Foo())).toBe(false);
expect(isPlainObject(() => {})).toBe(false);
expect(isPlainObject(function () {})).toBe(false);
});

it('should return false for arrays', () => {
expect(isPlainObject([])).toBe(false);
expect(isPlainObject([1, 2, 3])).toBe(false);
});

it('should return false for built-in objects', () => {
expect(isPlainObject(new Date())).toBe(false);
expect(isPlainObject(new Map())).toBe(false);
expect(isPlainObject(new Set())).toBe(false);
expect(isPlainObject(/abc/)).toBe(false);
});

it('should return false for objects created with Object.create and a custom prototype', () => {
expect(isPlainObject(Object.create({}))).toBe(false);
});
});
});
});
4 changes: 2 additions & 2 deletions packages/helpers/src/merge.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEqual, isObject, isUndefined } from './is.js';
import { isEqual, isPlainObject, isUndefined } from './is.js';

/**
* Deeply merges two objects.
Expand All @@ -11,7 +11,7 @@ export function deepMerge(target, source) {
return target;
}

if (!isObject(source) || !isObject(target) || isEqual(target, source)) {
if (!isPlainObject(source) || !isPlainObject(target) || isEqual(target, source)) {
return source;
}

Expand Down
90 changes: 90 additions & 0 deletions packages/helpers/src/merge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,95 @@ describe('helpers', () => {

expect(result).toStrictEqual({ a: { b: { c: 1, d: 2 } } });
});

describe('Built-in objects', () => {
it('should overwrite RegExp objects instead of merging', () => {
const target = { r: /abc/i };
const source = { r: /def/g };
const result = deepMerge(target, source);

expect(result.r).toBeInstanceOf(RegExp);
expect(result.r).toStrictEqual(/def/g);
expect(result.r).toBe(source.r);
});

it('should overwrite Map objects instead of merging', () => {
const map1 = new Map([['a', 1]]);
const map2 = new Map([['b', 2]]);
const target = { m: map1 };
const source = { m: map2 };
const result = deepMerge(target, source);

expect(result.m).toBeInstanceOf(Map);
expect(Array.from(result.m.entries())).toStrictEqual([['b', 2]]);
expect(result.m).toBe(map2);
});

it('should overwrite Set objects instead of merging', () => {
const set1 = new Set([1, 2]);
const set2 = new Set([3, 4]);
const target = { s: set1 };
const source = { s: set2 };
const result = deepMerge(target, source);

expect(result.s).toBeInstanceOf(Set);
expect(Array.from(result.s)).toStrictEqual([3, 4]);
expect(result.s).toBe(set2);
});

describe('Date objects', () => {
it('should overwrite Date objects instead of merging', () => {
const date1 = new Date('2020-01-01T00:00:00Z');
const date2 = new Date('2021-01-01T00:00:00Z');
const target = { a: date1 };
const source = { a: date2 };
const result = deepMerge(target, source);

expect(result.a).toBeInstanceOf(Date);
expect(result.a).toStrictEqual(date2);
expect(result.a).toBe(date2);
});

it('should handle Date in nested objects', () => {
const date1 = new Date('2020-01-01T00:00:00Z');
const date2 = new Date('2021-01-01T00:00:00Z');
const target = { nested: { d: date1 } };
const source = { nested: { d: date2 } };
const result = deepMerge(target, source);

expect(result.nested.d).toBeInstanceOf(Date);
expect(result.nested.d).toStrictEqual(date2);
expect(result.nested.d).toBe(date2);
});

it('should return the source Date reference when both are Date objects with the same value', () => {
const date = '2022-05-05T12:00:00Z';
const target = { a: new Date(date) };
const source = { a: new Date(date) };
const result = deepMerge(target, source);

expect(result.a).toBe(source.a);
});

it('should overwrite Date object with primitive value', () => {
const date = new Date('2020-01-01T00:00:00Z');
const target = { a: date };
const source = { a: '2021-01-01' };
const result = deepMerge(target, source);

expect(result).toStrictEqual({ a: '2021-01-01' });
});

it('should overwrite primitive value with Date object', () => {
const date = new Date('2020-01-01T00:00:00Z');
const target = { a: '2021-01-01' };
const source = { a: date };
const result = deepMerge(target, source);

expect(result).toStrictEqual({ a: date });
expect(result.a).toBe(date);
});
});
});
});
});
Loading