Skip to content

Commit 19054e0

Browse files
authored
fix: messages deepCopy mutates src arguments (#1947)
* fix: messages `deepCopy` mutates `src` arguments * fix: `deepCopy` should never merge arrays
1 parent 28a83ba commit 19054e0

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

packages/shared/src/messages.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ export function deepCopy(src: any, des: any): void {
1212
while (stack.length) {
1313
const { src, des } = stack.pop()!
1414

15+
// using `Object.keys` which skips prototype properties
1516
Object.keys(src).forEach(key => {
16-
if (isNotObjectOrIsArray(src[key]) || isNotObjectOrIsArray(des[key])) {
17+
// if src[key] is an object/array, set des[key]
18+
// to empty object/array to prevent setting by reference
19+
if (isObject(src[key]) && !isObject(des[key])) {
20+
des[key] = Array.isArray(src[key]) ? [] : {}
21+
}
22+
23+
if (isNotObjectOrIsArray(des[key]) || isNotObjectOrIsArray(src[key])) {
1724
// replace with src[key] when:
1825
// src[key] or des[key] is not an object, or
1926
// src[key] or des[key] is an array

packages/shared/test/messages.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { deepCopy } from '../src/index'
2+
3+
test('deepCopy merges without mutating src argument', () => {
4+
const msg1 = {
5+
hello: 'Greetings',
6+
about: {
7+
title: 'About us'
8+
},
9+
overwritten: 'Original text',
10+
fruit: [{ name: 'Apple' }]
11+
}
12+
const copy1 = structuredClone(msg1)
13+
14+
const msg2 = {
15+
bye: 'Goodbye',
16+
about: {
17+
content: 'Some text'
18+
},
19+
overwritten: 'New text',
20+
fruit: [{ name: 'Strawberry' }],
21+
// @ts-ignore
22+
car: ({ plural }) => plural(['car', 'cars'])
23+
}
24+
25+
const merged = {}
26+
27+
deepCopy(msg1, merged)
28+
deepCopy(msg2, merged)
29+
30+
expect(merged).toMatchInlineSnapshot(`
31+
{
32+
"about": {
33+
"content": "Some text",
34+
"title": "About us",
35+
},
36+
"bye": "Goodbye",
37+
"car": [Function],
38+
"fruit": [
39+
{
40+
"name": "Strawberry",
41+
},
42+
],
43+
"hello": "Greetings",
44+
"overwritten": "New text",
45+
}
46+
`)
47+
48+
// should not mutate source object
49+
expect(msg1).toStrictEqual(copy1)
50+
})

0 commit comments

Comments
 (0)