From df4b09f06ec5af8451b59543f33b606271a75ef3 Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Fri, 13 Sep 2024 11:52:07 +0200 Subject: [PATCH] fix: messages `deepCopy` mutates `src` arguments --- packages/shared/src/messages.ts | 14 +++++--- packages/shared/test/messages.test.ts | 50 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 packages/shared/test/messages.test.ts diff --git a/packages/shared/src/messages.ts b/packages/shared/src/messages.ts index d657eed02..719a0361a 100644 --- a/packages/shared/src/messages.ts +++ b/packages/shared/src/messages.ts @@ -13,14 +13,20 @@ export function deepCopy(src: any, des: any): void { const { src, des } = stack.pop()! 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 (isObject(des[key]) && isObject(src[key])) { + // src[key] and des[key] are both objects, merge them + stack.push({ src: src[key], des: des[key] }) + } else { // replace with src[key] when: // src[key] or des[key] is not an object, or // src[key] or des[key] is an array des[key] = src[key] - } else { - // src[key] and des[key] are both objects, merge them - stack.push({ src: src[key], des: des[key] }) } }) } 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) +})