@ditojs/utils 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/utils",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Utility Functions – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/utils",
@@ -32,7 +32,7 @@
32
32
  "typescript": "^5.0.4"
33
33
  },
34
34
  "types": "types",
35
- "gitHead": "ce98ce03662c08e040ad7f9cb93cc0f48e11edb9",
35
+ "gitHead": "874c68d5807ba97b67867f6b8d13f8fcb4990533",
36
36
  "scripts": {
37
37
  "types": "tsc --noEmit types/index.d.ts"
38
38
  },
@@ -5,6 +5,7 @@ exports[`index > exports all symbols 1`] = `
5
5
  "asArray": [Function],
6
6
  "asFunction": [Function],
7
7
  "asObject": [Function],
8
+ "assignDeeply": [Function],
8
9
  "camelize": [Function],
9
10
  "capitalize": [Function],
10
11
  "clone": [Function],
@@ -27,6 +28,7 @@ exports[`index > exports all symbols 1`] = `
27
28
  },
28
29
  },
29
30
  "deindent": [Function],
31
+ "deprecate": [Function],
30
32
  "equals": [Function],
31
33
  "escapeHtml": [Function],
32
34
  "escapeRegexp": [Function],
@@ -67,6 +69,7 @@ exports[`index > exports all symbols 1`] = `
67
69
  "mapSequentially": [Function],
68
70
  "mapValues": [Function],
69
71
  "merge": [Function],
72
+ "mergeDeeply": [Function],
70
73
  "mixin": [Function],
71
74
  "normalizeDataPath": [Function],
72
75
  "parseDataPath": [Function],
@@ -0,0 +1,9 @@
1
+ const loggedDeprecations = new Set()
2
+
3
+ export function deprecate(message) {
4
+ // Only log deprecation messages once.
5
+ if (!loggedDeprecations.has(message)) {
6
+ loggedDeprecations.add(message)
7
+ console.warn(message)
8
+ }
9
+ }
@@ -1,3 +1,4 @@
1
+ export * from './deprecate.js'
1
2
  export * from './toAsync.js'
2
3
  export * from './toCallback.js'
3
4
  export * from './toPromiseCallback.js'
@@ -3,6 +3,6 @@ export * from './equals.js'
3
3
  export * from './groupBy.js'
4
4
  export * from './mapKeys.js'
5
5
  export * from './mapValues.js'
6
- export * from './merge.js'
6
+ export * from './mergeDeeply.js'
7
7
  export * from './pick.js'
8
8
  export * from './pickBy.js'
@@ -0,0 +1,51 @@
1
+ import { isArray, isPlainObject } from '../base/index.js'
2
+ import { deprecate } from '../function/deprecate.js'
3
+ import { clone } from './clone.js'
4
+
5
+ export function mergeDeeply(target, ...sources) {
6
+ for (const source of sources) {
7
+ target = mergeValues(target, source, true, true)
8
+ }
9
+ return target
10
+ }
11
+
12
+ export function merge(...args) {
13
+ deprecate('merge() is deprecated, use mergeDeeply() instead.')
14
+ return mergeDeeply(...args)
15
+ }
16
+
17
+ export function assignDeeply(target, ...sources) {
18
+ for (const source of sources) {
19
+ target = mergeValues(target, source, true, false)
20
+ }
21
+ return target
22
+ }
23
+
24
+ function mergeValues(target, source, root, concat) {
25
+ if (target && source && target !== source) {
26
+ if (isArray(target) && isArray(source)) {
27
+ return mergeIteratively(target, source, root, concat, true)
28
+ } else if (isPlainObject(target) && isPlainObject(source)) {
29
+ return mergeIteratively(target, source, root, concat, false)
30
+ }
31
+ }
32
+ return root ? source ?? target : source
33
+ }
34
+
35
+ function mergeIteratively(target, source, root, concat, toArray) {
36
+ const result = root
37
+ ? target
38
+ : clone(target, { shallow: true })
39
+ for (const key of Object.keys(source)) {
40
+ const to = target[key]
41
+ const from = source[key]
42
+ const value = mergeValues(to, from, false, concat)
43
+ if (value === from && toArray && concat) {
44
+ // Append non-mergeable values to the end of results array.
45
+ result.push(value)
46
+ } else {
47
+ result[key] = value
48
+ }
49
+ }
50
+ return result
51
+ }
@@ -1,26 +1,26 @@
1
- import { merge } from './merge.js'
1
+ import { mergeDeeply, assignDeeply } from './mergeDeeply.js'
2
2
 
3
- describe('merge()', () => {
3
+ describe('mergeDeeply()', () => {
4
4
  it('should merge nested objects', () => {
5
5
  const source1 = { a: { b: [] } }
6
6
  const source2 = { a: { c: [] } }
7
7
  const expected = { a: { b: [], c: [] } }
8
- expect(merge({}, source1, source2)).toStrictEqual(expected)
9
- expect(merge({}, source2, source1)).toStrictEqual(expected)
8
+ expect(mergeDeeply({}, source1, source2)).toStrictEqual(expected)
9
+ expect(mergeDeeply({}, source2, source1)).toStrictEqual(expected)
10
10
  })
11
11
 
12
12
  it('should override keys in target with different types', () => {
13
13
  const source1 = { a: { b: 1 } }
14
14
  const source2 = { a: { b: [] } }
15
- expect(merge({}, source1, source2)).toStrictEqual(source2)
16
- expect(merge({}, source2, source1)).toStrictEqual(source1)
15
+ expect(mergeDeeply({}, source1, source2)).toStrictEqual(source2)
16
+ expect(mergeDeeply({}, source2, source1)).toStrictEqual(source1)
17
17
  })
18
18
 
19
19
  it('should merge objects at the same indices inside arrays', () => {
20
20
  const source1 = [{ a: 1 }, { b: 2 }]
21
21
  const source2 = [{ c: 3 }, { d: 4 }]
22
22
  const expected = [{ a: 1, c: 3 }, { b: 2, d: 4 }]
23
- expect(merge([], source1, source2)).toStrictEqual(expected)
23
+ expect(mergeDeeply([], source1, source2)).toStrictEqual(expected)
24
24
  })
25
25
 
26
26
  it('should merge `source` into `object`', () => {
@@ -52,26 +52,26 @@ describe('merge()', () => {
52
52
  ]
53
53
  }
54
54
 
55
- expect(merge(names, ages, heights)).toEqual(expected)
55
+ expect(mergeDeeply(names, ages, heights)).toEqual(expected)
56
56
  })
57
57
 
58
58
  it('should handle plain values', () => {
59
- expect(merge(true, false)).toEqual(false)
60
- expect(merge(10, 0)).toEqual(0)
61
- expect(merge(0, 10)).toEqual(10)
62
- expect(merge('foo', 'bar')).toEqual('bar')
59
+ expect(mergeDeeply(true, false)).toEqual(false)
60
+ expect(mergeDeeply(10, 0)).toEqual(0)
61
+ expect(mergeDeeply(0, 10)).toEqual(10)
62
+ expect(mergeDeeply('foo', 'bar')).toEqual('bar')
63
63
  })
64
64
 
65
65
  it('should not override values with nullish values at root level', () => {
66
- expect(merge({}, null)).toEqual({})
67
- expect(merge([], null)).toEqual([])
68
- expect(merge(10, null)).toEqual(10)
69
- expect(merge(false, null)).toEqual(false)
66
+ expect(mergeDeeply({}, null)).toEqual({})
67
+ expect(mergeDeeply([], null)).toEqual([])
68
+ expect(mergeDeeply(10, null)).toEqual(10)
69
+ expect(mergeDeeply(false, null)).toEqual(false)
70
70
  })
71
71
 
72
72
  it('should work with multiple arguments', () => {
73
73
  const expected = { a: 4 }
74
- const actual = merge({ a: 1 }, { a: 2 }, { a: 3 }, expected)
74
+ const actual = mergeDeeply({ a: 1 }, { a: 2 }, { a: 3 }, expected)
75
75
 
76
76
  expect(actual).toStrictEqual(expected)
77
77
  })
@@ -79,7 +79,7 @@ describe('merge()', () => {
79
79
  it('should not augment source objects', () => {
80
80
  const source1 = { a: [{ a: 1 }] }
81
81
  const source2 = { a: [{ b: 2 }] }
82
- const actual1 = merge({}, source1, source2)
82
+ const actual1 = mergeDeeply({}, source1, source2)
83
83
 
84
84
  expect(source1.a).toStrictEqual([{ a: 1 }])
85
85
  expect(source2.a).toStrictEqual([{ b: 2 }])
@@ -87,7 +87,7 @@ describe('merge()', () => {
87
87
 
88
88
  const source3 = { a: [[1, 2, 3]] }
89
89
  const source4 = { a: [[4, 5]] }
90
- const actual2 = merge({}, source3, source4)
90
+ const actual2 = mergeDeeply({}, source3, source4)
91
91
 
92
92
  expect(source3.a).toStrictEqual([[1, 2, 3]])
93
93
  expect(source4.a).toStrictEqual([[4, 5]])
@@ -95,33 +95,31 @@ describe('merge()', () => {
95
95
  })
96
96
 
97
97
  it('should overwrite existing values with `undefined` in objects', () => {
98
- const actual = merge({ a: 1 }, { a: undefined, b: undefined })
99
- expect(actual).toStrictEqual({ a: undefined, b: undefined })
98
+ const result = mergeDeeply({ a: 1 }, { a: undefined, b: undefined })
99
+ expect(result).toStrictEqual({ a: undefined, b: undefined })
100
100
  })
101
101
 
102
102
  it('should not overwrite existing values with `undefined` in arrays', () => {
103
- const array1 = [1, undefined, 3]
104
- const actual1 = merge([4, 5, 6], array1)
105
- expect(actual1).toStrictEqual([4, 5, 6, 1, undefined, 3])
103
+ const result1 = mergeDeeply([4, 5, 6], [1, undefined, 3])
104
+ expect(result1).toStrictEqual([4, 5, 6, 1, undefined, 3])
106
105
 
107
106
  // eslint-disable-next-line no-sparse-arrays
108
- const array2 = [1, , 3]
109
- const actual2 = merge([4, 5, 6], array2)
110
- expect(actual2).toStrictEqual([4, 5, 6, 1, 3])
107
+ const result2 = mergeDeeply([4, 5, 6], [1, , 3])
108
+ expect(result2).toStrictEqual([4, 5, 6, 1, 3])
111
109
  })
112
110
 
113
111
  it('should merge regexps', () => {
114
112
  const source1 = { a: /1/ }
115
113
  const source2 = { a: /2/ }
116
114
  const expected = { a: /2/ }
117
- expect(merge({}, source1, source2)).toStrictEqual(expected)
115
+ expect(mergeDeeply({}, source1, source2)).toStrictEqual(expected)
118
116
  })
119
117
 
120
118
  it('should merge dates', () => {
121
119
  const source1 = { a: new Date(2012, 5, 9) }
122
120
  const source2 = { a: new Date(2021, 5, 9) }
123
121
  const expected = { a: new Date(2021, 5, 9) }
124
- expect(merge({}, source1, source2)).toStrictEqual(expected)
122
+ expect(mergeDeeply({}, source1, source2)).toStrictEqual(expected)
125
123
  })
126
124
 
127
125
  it('should be fine with nested promises', async () => {
@@ -130,9 +128,25 @@ describe('merge()', () => {
130
128
  const source1 = { nested: { promise1 } }
131
129
  const source2 = { nested: { promise2 } }
132
130
  const expected = { nested: { promise1, promise2 } }
133
- const result = merge({}, source1, source2)
131
+ const result = mergeDeeply({}, source1, source2)
134
132
  expect(result).toStrictEqual(expected)
135
133
  expect(await result.nested.promise1).toStrictEqual(1)
136
134
  expect(await result.nested.promise2).toStrictEqual(2)
137
135
  })
138
136
  })
137
+
138
+ describe('assignDeeply()', () => {
139
+ it('should not concat-merge arrays', () => {
140
+ const result = assignDeeply({}, { a: [[1, 2, 3]] }, { a: [[4, 5]] })
141
+ expect(result).toStrictEqual({ a: [[4, 5, 3]] })
142
+ })
143
+
144
+ it('should overwrite existing values with `undefined` in arrays', () => {
145
+ const result1 = assignDeeply([4, 5, 6], [1, undefined, 3])
146
+ expect(result1).toStrictEqual([1, undefined, 3])
147
+
148
+ // eslint-disable-next-line no-sparse-arrays
149
+ const result2 = assignDeeply([4, 5, 6], [1, , 3])
150
+ expect(result2).toStrictEqual([1, 5, 3])
151
+ })
152
+ })
package/types/index.d.ts CHANGED
@@ -148,27 +148,49 @@ export function groupBy<T, K extends string | number | symbol>(
148
148
  callback: (item: T) => K
149
149
  ): Record<K, T[]>
150
150
 
151
- // TODO: document merge
152
- export function merge<ArgA, ArgB>(a: ArgA, b: ArgB): ArgA & ArgB
153
- export function merge<ArgA, ArgB, ArgC>(
151
+ // TODO: document mergeDeeply
152
+ export function mergeDeeply<ArgA, ArgB>(a: ArgA, b: ArgB): ArgA & ArgB
153
+ export function mergeDeeply<ArgA, ArgB, ArgC>(
154
154
  a: ArgA,
155
155
  b: ArgB,
156
156
  c: ArgC
157
157
  ): ArgA & ArgB & ArgC
158
- export function merge<ArgA, ArgB, ArgC, ArgD>(
158
+ export function mergeDeeply<ArgA, ArgB, ArgC, ArgD>(
159
159
  a: ArgA,
160
160
  b: ArgB,
161
161
  c: ArgC,
162
162
  d: ArgD
163
163
  ): ArgA & ArgB & ArgC & ArgD
164
- export function merge<ArgA, ArgB, ArgC, ArgD, ArgE>(
164
+ export function mergeDeeply<ArgA, ArgB, ArgC, ArgD, ArgE>(
165
165
  a: ArgA,
166
166
  b: ArgB,
167
167
  c: ArgC,
168
168
  d: ArgD,
169
169
  e: ArgE
170
170
  ): ArgA & ArgB & ArgC & ArgD & ArgE
171
- export function merge(...args: any[]): any
171
+ export function mergeDeeply(...args: any[]): any
172
+
173
+ // TODO: document assignDeeply
174
+ export function assignDeeply<ArgA, ArgB>(a: ArgA, b: ArgB): ArgA & ArgB
175
+ export function assignDeeply<ArgA, ArgB, ArgC>(
176
+ a: ArgA,
177
+ b: ArgB,
178
+ c: ArgC
179
+ ): ArgA & ArgB & ArgC
180
+ export function assignDeeply<ArgA, ArgB, ArgC, ArgD>(
181
+ a: ArgA,
182
+ b: ArgB,
183
+ c: ArgC,
184
+ d: ArgD
185
+ ): ArgA & ArgB & ArgC & ArgD
186
+ export function assignDeeply<ArgA, ArgB, ArgC, ArgD, ArgE>(
187
+ a: ArgA,
188
+ b: ArgB,
189
+ c: ArgC,
190
+ d: ArgD,
191
+ e: ArgE
192
+ ): ArgA & ArgB & ArgC & ArgD & ArgE
193
+ export function assignDeeply(...args: any[]): any
172
194
 
173
195
  /**
174
196
  * Returns the first argument which is not undefined.
@@ -1,41 +0,0 @@
1
- import { isArray, isPlainObject } from '../base/index.js'
2
- import { clone } from './clone.js'
3
-
4
- export function merge(target, ...sources) {
5
- const mergeIteratively = (target, source, root, concat) => {
6
- const result = root
7
- ? target
8
- : clone(target, { shallow: true })
9
- for (const key of Object.keys(source)) {
10
- const to = target[key]
11
- const from = source[key]
12
- if (
13
- !concat ||
14
- isArray(to) && isArray(from) ||
15
- isPlainObject(to) && isPlainObject(from)
16
- ) {
17
- result[key] = mergeValues(to, from, false)
18
- } else {
19
- // Append non-mergeable values to the end of results.
20
- result.push(from)
21
- }
22
- }
23
- return result
24
- }
25
-
26
- const mergeValues = (target, source, root) => {
27
- if (target && source && target !== source) {
28
- if (isArray(target) && isArray(source)) {
29
- return mergeIteratively(target, source, root, true)
30
- } else if (isPlainObject(target) && isPlainObject(source)) {
31
- return mergeIteratively(target, source, root, false)
32
- }
33
- }
34
- return root ? source ?? target : source
35
- }
36
-
37
- for (const source of sources) {
38
- target = mergeValues(target, source, true)
39
- }
40
- return target
41
- }