@@ -14,44 +14,100 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
1414} ;
1515
1616/**
17- * Merge source array items into target array by index.
17+ * Clone a source item for safe assignment into a target array.
18+ * Plain objects are cloned via mergeRecursive, arrays via mergeArrayItemsByIndex,
19+ * and primitives (including undefined) are returned as-is.
20+ * Returns `{ skip: true }` only when a circular array reference is detected.
1821 *
19- * When both source and target items at the same index are plain objects,
20- * they are merged recursively. Otherwise, the source item replaces the target.
22+ * @internal
23+ */
24+ const cloneItem = (
25+ srcItem : unknown ,
26+ ancestors : object [ ]
27+ ) : { skip : true } | { skip : false ; value : unknown } => {
28+ if ( isPlainObject ( srcItem ) ) {
29+ const cloned : Record < string , unknown > = { } ;
30+ ancestors . push ( srcItem ) ;
31+ mergeRecursive ( cloned , srcItem , ancestors ) ;
32+ ancestors . pop ( ) ;
33+
34+ return { skip : false , value : cloned } ;
35+ }
36+
37+ if ( Array . isArray ( srcItem ) ) {
38+ if ( ancestors . includes ( srcItem ) ) return { skip : true } ;
39+ const cloned : unknown [ ] = [ ] ;
40+ ancestors . push ( srcItem ) ;
41+ mergeArrayItemsByIndex ( cloned , srcItem , ancestors ) ;
42+ ancestors . pop ( ) ;
43+
44+ return { skip : false , value : cloned } ;
45+ }
46+
47+ return { skip : false , value : srcItem } ;
48+ } ;
49+
50+ /**
51+ * Merge a single source item into a target array at the given index.
2152 *
2253 * @internal
2354 */
24- const mergeArrayItemsByIndex = (
55+ const mergeArrayItem = (
2556 targetArray : unknown [ ] ,
26- sourceArray : unknown [ ] ,
57+ index : number ,
58+ srcItem : unknown ,
2759 ancestors : object [ ]
2860) : void => {
29- for ( let i = 0 ; i < sourceArray . length ; i ++ ) {
30- const srcItem = sourceArray [ i ] ;
31- const tgtItem = targetArray [ i ] ;
61+ const tgtItem = targetArray [ index ] ;
62+ const isSrcPlainObject = isPlainObject ( srcItem ) ;
3263
33- const isSrcPlainObject = isPlainObject ( srcItem ) ;
64+ // Skip circular plain object references
65+ if ( isSrcPlainObject && ancestors . includes ( srcItem ) ) return ;
3466
35- // Skip objects in the current ancestor chain to prevent circular references
36- if ( isSrcPlainObject && ancestors . includes ( srcItem ) ) {
37- continue ;
38- }
67+ // Merge two plain objects recursively
68+ if ( isSrcPlainObject && isPlainObject ( tgtItem ) ) {
69+ ancestors . push ( srcItem ) ;
70+ mergeRecursive ( tgtItem , srcItem , ancestors ) ;
71+ ancestors . pop ( ) ;
72+
73+ return ;
74+ }
3975
40- // Merge nested plain objects recursively
41- if ( isSrcPlainObject && isPlainObject ( tgtItem ) ) {
76+ // Merge two arrays by index
77+ if ( Array . isArray ( srcItem ) && Array . isArray ( tgtItem ) ) {
78+ if ( ! ancestors . includes ( srcItem ) ) {
4279 ancestors . push ( srcItem ) ;
43- mergeRecursive ( tgtItem , srcItem , ancestors ) ;
80+ mergeArrayItemsByIndex ( tgtItem , srcItem , ancestors ) ;
4481 ancestors . pop ( ) ;
45- continue ;
4682 }
4783
48- // Otherwise, replace the target item with source item
49- if ( srcItem !== undefined || tgtItem === undefined ) {
50- targetArray [ i ] = srcItem ;
84+ return ;
85+ }
86+
87+ // Replace with a cloned copy of the source item
88+ if ( srcItem !== undefined || tgtItem === undefined ) {
89+ const result = cloneItem ( srcItem , ancestors ) ;
90+ if ( ! result . skip ) {
91+ targetArray [ index ] = result . value ;
5192 }
5293 }
5394} ;
5495
96+ /**
97+ * Merge source array items into target array by index.
98+ *
99+ * @internal
100+ */
101+ const mergeArrayItemsByIndex = (
102+ targetArray : unknown [ ] ,
103+ sourceArray : unknown [ ] ,
104+ ancestors : object [ ]
105+ ) : void => {
106+ for ( let i = 0 ; i < sourceArray . length ; i ++ ) {
107+ mergeArrayItem ( targetArray , i , sourceArray [ i ] , ancestors ) ;
108+ }
109+ } ;
110+
55111/**
56112 * Handle merging when source value is an array.
57113 *
@@ -65,7 +121,9 @@ const handleArrayMerge = (
65121 ancestors : object [ ]
66122) : void => {
67123 if ( ! Array . isArray ( targetValue ) ) {
68- target [ key ] = [ ...sourceArray ] ;
124+ const freshArray : unknown [ ] = [ ] ;
125+ mergeArrayItemsByIndex ( freshArray , sourceArray , ancestors ) ;
126+ target [ key ] = freshArray ;
69127 return ;
70128 }
71129
0 commit comments