From 85222d93593f0ca9832f94b5ea60b1e423d139f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 08:34:55 +0200 Subject: [PATCH 1/9] fix(476): Next.js 15 / React 19 compatibility --- package-lock.json | 6 +++--- package.json | 2 +- src/Store/Store.jsx | 7 ++++--- src/Store/WithStore.jsx | 3 ++- src/Store/mergeOptions.js | 14 ++++++++++++++ 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 src/Store/mergeOptions.js 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..783f0eca 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..7ad853e7 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 './mergeOptions'; 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..5ef9876d 100644 --- a/src/Store/WithStore.jsx +++ b/src/Store/WithStore.jsx @@ -3,6 +3,7 @@ import equal from 'equals'; import deepMerge from 'deepmerge'; import { CarouselPropTypes } from '../helpers'; import { CarouselContext } from '../CarouselProvider'; +import { safeMergeOptions } from './mergeOptions'; export default function WithStore( WrappedComponent, @@ -43,7 +44,7 @@ export default function WithStore( } render() { - const props = deepMerge(this.state, this.props); + const props = deepMerge(this.state, this.props, safeMergeOptions); return ( source; + +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; + }, +}; + +export { safeArrayMerge, safeMergeOptions }; From 789c6a28303cac02b38db9e6c9887fb95aa380a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 08:54:21 +0200 Subject: [PATCH 2/9] fix(476): Next.js 15 / React 19 compatibility --- src/Store/Store.jsx | 2 +- src/Store/WithStore.jsx | 3 +-- src/Store/mergeOptions.js | 14 -------------- src/helpers/index.js | 13 +++++++++++++ 4 files changed, 15 insertions(+), 17 deletions(-) delete mode 100644 src/Store/mergeOptions.js diff --git a/src/Store/Store.jsx b/src/Store/Store.jsx index 7ad853e7..90e5d972 100644 --- a/src/Store/Store.jsx +++ b/src/Store/Store.jsx @@ -1,6 +1,6 @@ import deepMerge from 'deepmerge'; import deepFreeze from 'deep-freeze'; -import { safeMergeOptions } from './mergeOptions'; +import { safeMergeOptions } from '../helpers'; const DEFAULT_STATE = { masterSpinnerFinished: false, diff --git a/src/Store/WithStore.jsx b/src/Store/WithStore.jsx index 5ef9876d..a3a7f132 100644 --- a/src/Store/WithStore.jsx +++ b/src/Store/WithStore.jsx @@ -1,9 +1,8 @@ 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'; -import { safeMergeOptions } from './mergeOptions'; export default function WithStore( WrappedComponent, diff --git a/src/Store/mergeOptions.js b/src/Store/mergeOptions.js deleted file mode 100644 index 2afb497f..00000000 --- a/src/Store/mergeOptions.js +++ /dev/null @@ -1,14 +0,0 @@ -const safeArrayMerge = (destination, source) => source; - -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; - }, -}; - -export { safeArrayMerge, safeMergeOptions }; 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; + }, +}; From bad98aa2c13f1e780c8e38f8cf7473bb2818fee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 08:54:25 +0200 Subject: [PATCH 3/9] fix(476): Next.js 15 / React 19 compatibility --- src/helpers/__tests__/helpers.test.js | 160 ++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/helpers/__tests__/helpers.test.js diff --git a/src/helpers/__tests__/helpers.test.js b/src/helpers/__tests__/helpers.test.js new file mode 100644 index 00000000..59010e88 --- /dev/null +++ b/src/helpers/__tests__/helpers.test.js @@ -0,0 +1,160 @@ +import deepMerge from 'deepmerge'; +import { + cn, + randomHexColor, + slideUnit, + slideSize, + slideTraySize, + pct, + boundedRange, + 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'); + expect(result._owner).toBe('source-owner'); + 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'); + }); + }); + + // Test other helper functions to ensure they still work + describe('existing helper functions', () => { + it('cn should join class names correctly', () => { + expect(cn(['class1', 'class2'])).toBe('class1 class2'); + expect(cn(['class1', false, 'class2'])).toBe('class1 class2'); + }); + + it('slideUnit should calculate correctly', () => { + expect(slideUnit(1)).toBe(100); + expect(slideUnit(2)).toBe(50); + expect(slideUnit(4)).toBe(25); + }); + + it('pct should format percentages', () => { + expect(pct(50)).toBe('50%'); + expect(pct(100)).toBe('100%'); + }); + + it('boundedRange should constrain values', () => { + expect(boundedRange({ min: 0, max: 10, x: 5 })).toBe(5); + expect(boundedRange({ min: 0, max: 10, x: -5 })).toBe(0); + expect(boundedRange({ min: 0, max: 10, x: 15 })).toBe(10); + }); + }); +}); \ No newline at end of file From f6b3402ae29b1e1b70796771c74e517d6be8fff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 13:07:47 +0200 Subject: [PATCH 4/9] fix(476): Next.js 15 / React 19 compatibility --- src/Store/WithStore.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Store/WithStore.jsx b/src/Store/WithStore.jsx index a3a7f132..acd2474a 100644 --- a/src/Store/WithStore.jsx +++ b/src/Store/WithStore.jsx @@ -43,7 +43,8 @@ export default function WithStore( } render() { - const props = deepMerge(this.state, this.props, safeMergeOptions); + const { children, ...propsWithoutChildren } = this.props; + const props = deepMerge(this.state, propsWithoutChildren, safeMergeOptions); return ( - {this.props.children} + {children} ); } From e98ab412428ba1c7845b1a0b98ad1171526d298c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 13:12:58 +0200 Subject: [PATCH 5/9] fix(476): Next.js 15 / React 19 compatibility --- src/helpers/__tests__/helpers.test.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/helpers/__tests__/helpers.test.js b/src/helpers/__tests__/helpers.test.js index 59010e88..ea68ba15 100644 --- a/src/helpers/__tests__/helpers.test.js +++ b/src/helpers/__tests__/helpers.test.js @@ -1,10 +1,7 @@ import deepMerge from 'deepmerge'; import { cn, - randomHexColor, slideUnit, - slideSize, - slideTraySize, pct, boundedRange, safeArrayMerge, @@ -55,10 +52,10 @@ describe('helpers', () => { it('should return source merger for React internal keys', () => { const reactKeys = ['$$typeof', '_owner', '_store', 'ref', 'key']; - reactKeys.forEach(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); @@ -69,7 +66,7 @@ describe('helpers', () => { it('should return undefined for non-React keys', () => { const normalKeys = ['prop', 'className', 'children', 'data']; - normalKeys.forEach(key => { + normalKeys.forEach((key) => { const merger = safeMergeOptions.customMerge(key); expect(merger).toBeUndefined(); }); @@ -90,7 +87,7 @@ describe('helpers', () => { const source = { $$typeof: 'source-type', _owner: 'source-owner', - _store: 'source-store', + _store: 'source-store', ref: 'source-ref', key: 'source-key', className: 'source-class', @@ -101,7 +98,9 @@ describe('helpers', () => { // 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'); @@ -116,14 +115,14 @@ describe('helpers', () => { props: { $$typeof: 'target-type', className: 'target-class', - } + }, }; const source = { props: { $$typeof: 'source-type', className: 'source-class', - } + }, }; const result = deepMerge(target, source, safeMergeOptions); @@ -157,4 +156,4 @@ describe('helpers', () => { expect(boundedRange({ min: 0, max: 10, x: 15 })).toBe(10); }); }); -}); \ No newline at end of file +}); From d64421689f8eec17c00f0c43e5951466a0661945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 13:13:19 +0200 Subject: [PATCH 6/9] fix(476): Next.js 15 / React 19 compatibility --- src/helpers/__tests__/helpers.test.js | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/helpers/__tests__/helpers.test.js b/src/helpers/__tests__/helpers.test.js index ea68ba15..61f695ee 100644 --- a/src/helpers/__tests__/helpers.test.js +++ b/src/helpers/__tests__/helpers.test.js @@ -131,29 +131,4 @@ describe('helpers', () => { expect(result.props.className).toBe('source-class'); }); }); - - // Test other helper functions to ensure they still work - describe('existing helper functions', () => { - it('cn should join class names correctly', () => { - expect(cn(['class1', 'class2'])).toBe('class1 class2'); - expect(cn(['class1', false, 'class2'])).toBe('class1 class2'); - }); - - it('slideUnit should calculate correctly', () => { - expect(slideUnit(1)).toBe(100); - expect(slideUnit(2)).toBe(50); - expect(slideUnit(4)).toBe(25); - }); - - it('pct should format percentages', () => { - expect(pct(50)).toBe('50%'); - expect(pct(100)).toBe('100%'); - }); - - it('boundedRange should constrain values', () => { - expect(boundedRange({ min: 0, max: 10, x: 5 })).toBe(5); - expect(boundedRange({ min: 0, max: 10, x: -5 })).toBe(0); - expect(boundedRange({ min: 0, max: 10, x: 15 })).toBe(10); - }); - }); }); From 6bf2498f62b7fe23f1b73633eb1ad362e8847835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 13:13:36 +0200 Subject: [PATCH 7/9] fix(476): Next.js 15 / React 19 compatibility --- src/helpers/__tests__/helpers.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/helpers/__tests__/helpers.test.js b/src/helpers/__tests__/helpers.test.js index 61f695ee..f2f0a094 100644 --- a/src/helpers/__tests__/helpers.test.js +++ b/src/helpers/__tests__/helpers.test.js @@ -1,9 +1,5 @@ import deepMerge from 'deepmerge'; import { - cn, - slideUnit, - pct, - boundedRange, safeArrayMerge, safeMergeOptions, } from '../index'; From b62c0bf05171eef0d270eb7e781d95228b30a10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 13:16:56 +0200 Subject: [PATCH 8/9] fix(476): Next.js 15 / React 19 compatibility --- src/helpers/__tests__/.eslintrc | 4 ++++ src/helpers/__tests__/helpers.test.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/helpers/__tests__/.eslintrc 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 index f2f0a094..ee0f0d85 100644 --- a/src/helpers/__tests__/helpers.test.js +++ b/src/helpers/__tests__/helpers.test.js @@ -47,7 +47,7 @@ describe('helpers', () => { 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'); @@ -61,7 +61,7 @@ describe('helpers', () => { 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(); From 5e7a1d98f3a517de743d8794b1b18576902185d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20M=C3=BCller?= Date: Mon, 2 Jun 2025 13:21:46 +0200 Subject: [PATCH 9/9] fix(476): Next.js 15 / React 19 compatibility --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 783f0eca..a69b7ccd 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "deep-freeze": "0.0.1", - "deepmerge": "4.3.1", + "deepmerge": "^4.3.1", "equals": "^1.0.5", "prop-types": "^15.6.2" },