diff --git a/package-lock.json b/package-lock.json index 579d41ac..4853b0f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4901,9 +4901,9 @@ "dev": true }, "deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, "define-properties": { "version": "1.1.4", diff --git a/package.json b/package.json index 3071387d..a69b7ccd 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "deep-freeze": "0.0.1", - "deepmerge": "^2.2.1", + "deepmerge": "^4.3.1", "equals": "^1.0.5", "prop-types": "^15.6.2" }, diff --git a/src/Store/Store.jsx b/src/Store/Store.jsx index da70388a..90e5d972 100644 --- a/src/Store/Store.jsx +++ b/src/Store/Store.jsx @@ -1,5 +1,6 @@ import deepMerge from 'deepmerge'; import deepFreeze from 'deep-freeze'; +import { safeMergeOptions } from '../helpers'; const DEFAULT_STATE = { masterSpinnerFinished: false, @@ -7,7 +8,7 @@ const DEFAULT_STATE = { const Store = class Store { constructor(initialState) { - this.state = deepFreeze(deepMerge(DEFAULT_STATE, initialState)); + this.state = deepFreeze(deepMerge(DEFAULT_STATE, initialState, safeMergeOptions)); this.subscriptions = []; this.masterSpinnerSubscriptions = {}; this.setStoreState = this.setStoreState.bind(this); @@ -23,12 +24,12 @@ const Store = class Store { } setStoreState(newState, cb) { - this.state = deepFreeze(deepMerge(this.state, newState)); + this.state = deepFreeze(deepMerge(this.state, newState, safeMergeOptions)); this.updateSubscribers(cb); } getStoreState() { - return deepMerge({}, this.state); + return deepMerge({}, this.state, safeMergeOptions); } subscribe(func) { diff --git a/src/Store/WithStore.jsx b/src/Store/WithStore.jsx index c9cb37d4..acd2474a 100644 --- a/src/Store/WithStore.jsx +++ b/src/Store/WithStore.jsx @@ -1,7 +1,7 @@ import React from 'react'; import equal from 'equals'; import deepMerge from 'deepmerge'; -import { CarouselPropTypes } from '../helpers'; +import { CarouselPropTypes, safeMergeOptions } from '../helpers'; import { CarouselContext } from '../CarouselProvider'; export default function WithStore( @@ -43,7 +43,8 @@ export default function WithStore( } render() { - const props = deepMerge(this.state, this.props); + const { children, ...propsWithoutChildren } = this.props; + const props = deepMerge(this.state, propsWithoutChildren, safeMergeOptions); return ( - {this.props.children} + {children} ); } diff --git a/src/helpers/__tests__/.eslintrc b/src/helpers/__tests__/.eslintrc new file mode 100644 index 00000000..1f8fba47 --- /dev/null +++ b/src/helpers/__tests__/.eslintrc @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": "mrb3k-jest" +} diff --git a/src/helpers/__tests__/helpers.test.js b/src/helpers/__tests__/helpers.test.js new file mode 100644 index 00000000..ee0f0d85 --- /dev/null +++ b/src/helpers/__tests__/helpers.test.js @@ -0,0 +1,130 @@ +import deepMerge from 'deepmerge'; +import { + safeArrayMerge, + safeMergeOptions, +} from '../index'; + +describe('helpers', () => { + describe('safeArrayMerge', () => { + it('should return source array, ignoring destination', () => { + const destination = [1, 2, 3]; + const source = [4, 5, 6]; + const result = safeArrayMerge(destination, source); + expect(result).toBe(source); + expect(result).toEqual([4, 5, 6]); + }); + + it('should work with empty arrays', () => { + const destination = [1, 2, 3]; + const source = []; + const result = safeArrayMerge(destination, source); + expect(result).toBe(source); + expect(result).toEqual([]); + }); + + it('should work with different array types', () => { + const destination = ['a', 'b']; + const source = ['c', 'd', 'e']; + const result = safeArrayMerge(destination, source); + expect(result).toBe(source); + expect(result).toEqual(['c', 'd', 'e']); + }); + }); + + describe('safeMergeOptions', () => { + it('should have correct arrayMerge function', () => { + expect(safeMergeOptions.arrayMerge).toBe(safeArrayMerge); + }); + + it('should have clone set to false', () => { + expect(safeMergeOptions.clone).toBe(false); + }); + + it('should have customMerge function', () => { + expect(typeof safeMergeOptions.customMerge).toBe('function'); + }); + + describe('customMerge function', () => { + it('should return source merger for React internal keys', () => { + const reactKeys = ['$$typeof', '_owner', '_store', 'ref', 'key']; + + reactKeys.forEach((key) => { + const merger = safeMergeOptions.customMerge(key); + expect(typeof merger).toBe('function'); + + const target = 'target'; + const source = 'source'; + const result = merger(target, source); + expect(result).toBe(source); + }); + }); + + it('should return undefined for non-React keys', () => { + const normalKeys = ['prop', 'className', 'children', 'data']; + + normalKeys.forEach((key) => { + const merger = safeMergeOptions.customMerge(key); + expect(merger).toBeUndefined(); + }); + }); + }); + + it('should work correctly with deepMerge for React props', () => { + const target = { + $$typeof: 'target-type', + _owner: 'target-owner', + _store: 'target-store', + ref: 'target-ref', + key: 'target-key', + className: 'target-class', + children: ['target-child'], + }; + + const source = { + $$typeof: 'source-type', + _owner: 'source-owner', + _store: 'source-store', + ref: 'source-ref', + key: 'source-key', + className: 'source-class', + children: ['source-child'], + }; + + const result = deepMerge(target, source, safeMergeOptions); + + // React internal keys should use source values + expect(result.$$typeof).toBe('source-type'); + // eslint-disable-next-line no-underscore-dangle + expect(result._owner).toBe('source-owner'); + // eslint-disable-next-line no-underscore-dangle + expect(result._store).toBe('source-store'); + expect(result.ref).toBe('source-ref'); + expect(result.key).toBe('source-key'); + + // Regular props should be merged normally + expect(result.className).toBe('source-class'); + expect(result.children).toEqual(['source-child']); // Arrays use safeArrayMerge + }); + + it('should handle nested objects with React keys', () => { + const target = { + props: { + $$typeof: 'target-type', + className: 'target-class', + }, + }; + + const source = { + props: { + $$typeof: 'source-type', + className: 'source-class', + }, + }; + + const result = deepMerge(target, source, safeMergeOptions); + + expect(result.props.$$typeof).toBe('source-type'); + expect(result.props.className).toBe('source-class'); + }); + }); +}); diff --git a/src/helpers/index.js b/src/helpers/index.js index 704ba296..ef19f0cc 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -66,3 +66,16 @@ export const boundedRange = ({ min, max, x }) => Math.min( max, Math.max(min, x), ); + +export const safeArrayMerge = (destination, source) => source; + +export const safeMergeOptions = { + arrayMerge: safeArrayMerge, + clone: false, + customMerge: (key) => { + if (key === '$$typeof' || key === '_owner' || key === '_store' || key === 'ref' || key === 'key') { + return (target, source) => source; + } + return undefined; + }, +};