From 9818b17ab7ecf7822741dd2a91a951c3ba4fd09d Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 07:47:26 +0000 Subject: [PATCH 1/5] fix(motion): apply clamping when inMin equals inMax in mapRange --- packages/motion/src/interpolate.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/motion/src/interpolate.ts b/packages/motion/src/interpolate.ts index 0d1c4866c..c4794bdca 100644 --- a/packages/motion/src/interpolate.ts +++ b/packages/motion/src/interpolate.ts @@ -21,6 +21,9 @@ export function mapRange( const clamp = options?.clamp ?? true; if (inMin === inMax) { + if (clamp && outMin !== outMax) { + return Math.min(outMin, outMax); + } return outMin; } From 8fc986afc3078c1b331114789d3128dfe8b39947 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 07:48:39 +0000 Subject: [PATCH 2/5] fix(core): validate parsed JSON in CommandHistory.import() is an array --- packages/core/src/history/CommandHistory.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/history/CommandHistory.ts b/packages/core/src/history/CommandHistory.ts index 20fc4d216..9f420a828 100644 --- a/packages/core/src/history/CommandHistory.ts +++ b/packages/core/src/history/CommandHistory.ts @@ -99,7 +99,15 @@ export class CommandHistory { /** Restore history from a JSON string produced by {@link export}. */ import(data: string): void { - const parsed: string[] = JSON.parse(data); + let parsed: unknown; + try { + parsed = JSON.parse(data); + } catch { + return; + } + if (!Array.isArray(parsed) || !parsed.every(item => typeof item === 'string')) { + return; + } this.commands = parsed.slice(-this.maxSize); this.index = this.commands.length; } From 73a96879214a2479f3ec6fd8169ad51e05388991 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 07:49:27 +0000 Subject: [PATCH 3/5] test(core): add validation tests for CommandHistory.import() --- .../core/src/history/CommandHistory.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/core/src/history/CommandHistory.test.ts b/packages/core/src/history/CommandHistory.test.ts index 65906958a..52b2137c4 100644 --- a/packages/core/src/history/CommandHistory.test.ts +++ b/packages/core/src/history/CommandHistory.test.ts @@ -199,4 +199,29 @@ describe('CommandHistory', () => { expect(ch.getAll()).toHaveLength(3); expect(ch.getAll()).toEqual(smallHistory); }); + + it('import() is a no-op for malformed JSON', () => { + const ch = new CommandHistory(); + ch.add('existing'); + ch.import('not valid json {{{'); + expect(ch.getAll()).toEqual(['existing']); + }); + + it('import() is a no-op for non-array JSON values', () => { + const ch = new CommandHistory(); + ch.add('existing'); + ch.import(JSON.stringify({ commands: ['cmd1', 'cmd2'] })); + expect(ch.getAll()).toEqual(['existing']); + ch.import(JSON.stringify(42)); + expect(ch.getAll()).toEqual(['existing']); + ch.import(JSON.stringify(null)); + expect(ch.getAll()).toEqual(['existing']); + }); + + it('import() is a no-op for arrays containing non-string elements', () => { + const ch = new CommandHistory(); + ch.add('existing'); + ch.import(JSON.stringify(['valid', 123, 'also valid'] as unknown[])); + expect(ch.getAll()).toEqual(['existing']); + }); }); \ No newline at end of file From f65f818f72138e4881c49778aaf31fe7b7529bb8 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 08:00:18 +0000 Subject: [PATCH 4/5] fix(store): construct nested paths from null/empty objects in setIn --- packages/store/src/immutable.ts | 40 ++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/store/src/immutable.ts b/packages/store/src/immutable.ts index c0acdc5ae..d406a7775 100644 --- a/packages/store/src/immutable.ts +++ b/packages/store/src/immutable.ts @@ -24,13 +24,47 @@ export function setIn(obj: T, path: (string | number)[], value: any): T { clone = [...obj]; } else if (obj !== null && typeof obj === 'object') { clone = { ...obj }; + // If clone is effectively empty (from spreading {} or null-ish) and + // the next key is numeric, replace with an array so that clone[0] = x + // creates a real array element instead of a string-keyed property. + if (clone && typeof key === 'number' && Object.keys(clone).length === 0) { + clone = []; + } } else { + // obj is null or a primitive; create the appropriate container + // based on the type of the next key to construct the nested path clone = typeof key === 'number' ? [] : {}; } - const nextVal = rest.length > 0 - ? setIn(clone[key], rest, value) - : value; + // When recursing, determine the correct container for the next level. + // Reuse an existing non-empty object/array to preserve sibling values. + // Otherwise create a container matching the key type: array for numeric keys, + // object for string keys. + const existing = clone[key]; + const isNonEmptyContainer = existing !== undefined + && existing !== null + && typeof existing === 'object' + && Object.keys(existing).length > 0; + + // For the terminal step (rest.length === 0) with a string key and an empty + // container, set the value directly on clone. If clone is an array and the key + // is a string, convert to a plain object first so the property survives + // JSON serialization; for numeric keys the array form is correct. + if (rest.length === 0) { + if (typeof key !== 'number' && Array.isArray(clone)) { + // Convert array to plain object to preserve the string-keyed property + const converted: any = {}; + for (const k of Object.keys(clone)) converted[k] = clone[k as any]; + clone = converted; + } + clone[key] = value; + return clone as T; + } + + const container = isNonEmptyContainer + ? existing + : (typeof key === 'number' ? [] : {}); + const nextVal = setIn(container, rest, value); clone[key] = nextVal; return clone as T; From 7ea2f943ae07bbdc5d7a0b47730556457cccbcd1 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 08:00:42 +0000 Subject: [PATCH 5/5] test(store): add null/empty source cases for setIn --- packages/store/src/immutable.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/store/src/immutable.test.ts b/packages/store/src/immutable.test.ts index 47ed8762e..01afccbae 100644 --- a/packages/store/src/immutable.test.ts +++ b/packages/store/src/immutable.test.ts @@ -43,6 +43,17 @@ describe('immutable helpers', () => { expect(updated).toEqual({ list: [1, 20, 3] }); expect(original.list).toEqual([1, 2, 3]); // should not mutate }); + + it('setIn from null builds nested path correctly', () => { + const updated = setIn(null, ['a', 'b'], 'value'); + expect(updated).toEqual({ a: { b: 'value' } }); + }); + + it('setIn from null with numeric index creates array', () => { + const updated = setIn(null, ['a', 0, 'b'], 'value'); + expect(updated).toEqual({ a: [{ b: 'value' }] }); + expect(Array.isArray((updated as any).a)).toBe(true); + }); }); describe('updateIn', () => {