Skip to content
Open
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
25 changes: 25 additions & 0 deletions packages/core/src/history/CommandHistory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
});
10 changes: 9 additions & 1 deletion packages/core/src/history/CommandHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/motion/src/interpolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
11 changes: 11 additions & 0 deletions packages/store/src/immutable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
40 changes: 37 additions & 3 deletions packages/store/src/immutable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,47 @@ export function setIn<T>(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;
Expand Down
Loading