@ditojs/utils 2.5.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.5.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": "1339a92f8adbfabee89d516ccfca84e13b48833b",
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,12 +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
+ })
57
+
58
+ it('should handle plain values', () => {
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
+ })
64
+
65
+ it('should not override values with nullish values at root level', () => {
66
+ expect(mergeDeeply({}, null)).toEqual({})
67
+ expect(mergeDeeply([], null)).toEqual([])
68
+ expect(mergeDeeply(10, null)).toEqual(10)
69
+ expect(mergeDeeply(false, null)).toEqual(false)
56
70
  })
57
71
 
58
72
  it('should work with multiple arguments', () => {
59
73
  const expected = { a: 4 }
60
- const actual = merge({ a: 1 }, { a: 2 }, { a: 3 }, expected)
74
+ const actual = mergeDeeply({ a: 1 }, { a: 2 }, { a: 3 }, expected)
61
75
 
62
76
  expect(actual).toStrictEqual(expected)
63
77
  })
@@ -65,7 +79,7 @@ describe('merge()', () => {
65
79
  it('should not augment source objects', () => {
66
80
  const source1 = { a: [{ a: 1 }] }
67
81
  const source2 = { a: [{ b: 2 }] }
68
- const actual1 = merge({}, source1, source2)
82
+ const actual1 = mergeDeeply({}, source1, source2)
69
83
 
70
84
  expect(source1.a).toStrictEqual([{ a: 1 }])
71
85
  expect(source2.a).toStrictEqual([{ b: 2 }])
@@ -73,7 +87,7 @@ describe('merge()', () => {
73
87
 
74
88
  const source3 = { a: [[1, 2, 3]] }
75
89
  const source4 = { a: [[4, 5]] }
76
- const actual2 = merge({}, source3, source4)
90
+ const actual2 = mergeDeeply({}, source3, source4)
77
91
 
78
92
  expect(source3.a).toStrictEqual([[1, 2, 3]])
79
93
  expect(source4.a).toStrictEqual([[4, 5]])
@@ -81,33 +95,31 @@ describe('merge()', () => {
81
95
  })
82
96
 
83
97
  it('should overwrite existing values with `undefined` in objects', () => {
84
- const actual = merge({ a: 1 }, { a: undefined, b: undefined })
85
- 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 })
86
100
  })
87
101
 
88
102
  it('should not overwrite existing values with `undefined` in arrays', () => {
89
- const array1 = [1, undefined, 3]
90
- const actual1 = merge([4, 5, 6], array1)
91
- 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])
92
105
 
93
106
  // eslint-disable-next-line no-sparse-arrays
94
- const array2 = [1, , 3]
95
- const actual2 = merge([4, 5, 6], array2)
96
- 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])
97
109
  })
98
110
 
99
111
  it('should merge regexps', () => {
100
112
  const source1 = { a: /1/ }
101
113
  const source2 = { a: /2/ }
102
114
  const expected = { a: /2/ }
103
- expect(merge({}, source1, source2)).toStrictEqual(expected)
115
+ expect(mergeDeeply({}, source1, source2)).toStrictEqual(expected)
104
116
  })
105
117
 
106
118
  it('should merge dates', () => {
107
119
  const source1 = { a: new Date(2012, 5, 9) }
108
120
  const source2 = { a: new Date(2021, 5, 9) }
109
121
  const expected = { a: new Date(2021, 5, 9) }
110
- expect(merge({}, source1, source2)).toStrictEqual(expected)
122
+ expect(mergeDeeply({}, source1, source2)).toStrictEqual(expected)
111
123
  })
112
124
 
113
125
  it('should be fine with nested promises', async () => {
@@ -116,9 +128,25 @@ describe('merge()', () => {
116
128
  const source1 = { nested: { promise1 } }
117
129
  const source2 = { nested: { promise2 } }
118
130
  const expected = { nested: { promise1, promise2 } }
119
- const result = merge({}, source1, source2)
131
+ const result = mergeDeeply({}, source1, source2)
120
132
  expect(result).toStrictEqual(expected)
121
133
  expect(await result.nested.promise1).toStrictEqual(1)
122
134
  expect(await result.nested.promise2).toStrictEqual(2)
123
135
  })
124
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, cloneTarget, concat) => {
6
- const result = cloneTarget
7
- ? clone(target, { shallow: true })
8
- : target
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, true)
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, cloneTarget) => {
27
- if (target && source && target !== source) {
28
- if (isArray(target) && isArray(source)) {
29
- return mergeIteratively(target, source, cloneTarget, true)
30
- } else if (isPlainObject(target) && isPlainObject(source)) {
31
- return mergeIteratively(target, source, cloneTarget, false)
32
- }
33
- }
34
- return source
35
- }
36
-
37
- for (const source of sources) {
38
- mergeValues(target, source, false)
39
- }
40
- return target
41
- }