diff --git a/packages/shared/src/messages.ts b/packages/shared/src/messages.ts index e01ea2294..6eeb1ce3f 100644 --- a/packages/shared/src/messages.ts +++ b/packages/shared/src/messages.ts @@ -12,8 +12,15 @@ export function deepCopy(src: any, des: any): void { while (stack.length) { const { src, des } = stack.pop()! + // using `Object.keys` which skips prototype properties Object.keys(src).forEach(key => { - if (isNotObjectOrIsArray(src[key]) || isNotObjectOrIsArray(des[key])) { + // if src[key] is an object/array, set des[key] + // to empty object/array to prevent setting by reference + if (isObject(src[key]) && !isObject(des[key])) { + des[key] = Array.isArray(src[key]) ? [] : {} + } + + if (isNotObjectOrIsArray(des[key]) || isNotObjectOrIsArray(src[key])) { // replace with src[key] when: // src[key] or des[key] is not an object, or // src[key] or des[key] is an array diff --git a/packages/shared/test/messages.test.ts b/packages/shared/test/messages.test.ts new file mode 100644 index 000000000..1380dd10f --- /dev/null +++ b/packages/shared/test/messages.test.ts @@ -0,0 +1,50 @@ +import { deepCopy } from '../src/index' + +test('deepCopy merges without mutating src argument', () => { + const msg1 = { + hello: 'Greetings', + about: { + title: 'About us' + }, + overwritten: 'Original text', + fruit: [{ name: 'Apple' }] + } + const copy1 = structuredClone(msg1) + + const msg2 = { + bye: 'Goodbye', + about: { + content: 'Some text' + }, + overwritten: 'New text', + fruit: [{ name: 'Strawberry' }], + // @ts-ignore + car: ({ plural }) => plural(['car', 'cars']) + } + + const merged = {} + + deepCopy(msg1, merged) + deepCopy(msg2, merged) + + expect(merged).toMatchInlineSnapshot(` + { + "about": { + "content": "Some text", + "title": "About us", + }, + "bye": "Goodbye", + "car": [Function], + "fruit": [ + { + "name": "Strawberry", + }, + ], + "hello": "Greetings", + "overwritten": "New text", + } + `) + + // should not mutate source object + expect(msg1).toStrictEqual(copy1) +})