@splendidlabz/utils 1.7.0 → 1.8.2

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cjs/dom/font-size.cjs +1 -1
  3. package/dist/cjs/dom/index.cjs +91 -1
  4. package/dist/cjs/dom/observers/index.cjs +92 -2
  5. package/dist/cjs/dom/observers/scroll-observer.cjs +170 -0
  6. package/dist/cjs/lib/date/index.cjs +3 -1
  7. package/dist/cjs/lib/date/time.cjs +3 -1
  8. package/dist/cjs/lib/functions/functional.cjs +20 -0
  9. package/dist/cjs/lib/functions/index.cjs +20 -0
  10. package/dist/cjs/lib/index.cjs +27 -2
  11. package/dist/cjs/lib/numbers/index.cjs +1 -1
  12. package/dist/cjs/lib/objects/extend.cjs +2 -1
  13. package/dist/cjs/lib/objects/index.cjs +4 -1
  14. package/dist/cjs/lib/objects/mix/mix.cjs +4 -1
  15. package/dist/cjs/node/file-cache.cjs +3 -3
  16. package/dist/cjs/node/index.cjs +3 -3
  17. package/dist/esm/dom/font-size.js +1 -1
  18. package/dist/esm/dom/index.js +90 -1
  19. package/dist/esm/dom/observers/index.js +90 -1
  20. package/dist/esm/dom/observers/scroll-observer.js +144 -0
  21. package/dist/esm/lib/date/index.js +3 -1
  22. package/dist/esm/lib/date/time.js +3 -1
  23. package/dist/esm/lib/functions/functional.js +18 -0
  24. package/dist/esm/lib/functions/index.js +18 -0
  25. package/dist/esm/lib/index.js +24 -2
  26. package/dist/esm/lib/numbers/index.js +1 -1
  27. package/dist/esm/lib/objects/extend.js +2 -1
  28. package/dist/esm/lib/objects/index.js +3 -1
  29. package/dist/esm/lib/objects/mix/mix.js +3 -1
  30. package/dist/esm/node/file-cache.js +1 -1
  31. package/dist/esm/node/index.js +1 -1
  32. package/dist/types/dom/index.d.cts +1 -0
  33. package/dist/types/dom/observers/index.d.cts +1 -0
  34. package/dist/types/dom/observers/scroll-observer.d.cts +27 -0
  35. package/dist/types/lib/functions/functional.d.cts +71 -5
  36. package/dist/types/lib/functions/index.d.cts +1 -1
  37. package/dist/types/lib/index.d.cts +2 -2
  38. package/dist/types/lib/objects/extend.d.cts +1 -1
  39. package/dist/types/lib/objects/index.d.cts +1 -1
  40. package/dist/types/lib/objects/mix/mix.d.cts +43 -3
  41. package/package.json +2 -3
  42. package/src/dom/observers/index.js +1 -0
  43. package/src/dom/observers/resize-observer.js +5 -5
  44. package/src/dom/observers/scroll-observer.js +132 -0
  45. package/src/lib/date/time.js +2 -0
  46. package/src/lib/functions/functional.js +82 -0
  47. package/src/lib/functions/functional.test.js +196 -0
  48. package/src/lib/numbers/index.js +1 -1
  49. package/src/lib/numbers/split-unit.test.js +45 -0
  50. package/src/lib/objects/mix/mix.js +74 -4
  51. package/src/lib/objects/mix/mix.test.js +51 -2
  52. package/src/node/file-cache.js +1 -1
@@ -0,0 +1,196 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ compose,
4
+ composeAsync,
5
+ curry,
6
+ pipe,
7
+ pipeAsync,
8
+ times,
9
+ } from './functional.js'
10
+
11
+ describe('functional utilities', () => {
12
+ describe('curry', () => {
13
+ it('should curry a function with multiple arguments', () => {
14
+ const add = (a, b, c) => a + b + c
15
+ const curriedAdd = curry(add)
16
+
17
+ expect(curriedAdd(1)(2)(3)).toBe(6)
18
+ expect(curriedAdd(1, 2)(3)).toBe(6)
19
+ expect(curriedAdd(1)(2, 3)).toBe(6)
20
+ expect(curriedAdd(1, 2, 3)).toBe(6)
21
+ })
22
+ })
23
+
24
+ describe('compose (sync)', () => {
25
+ it('should compose functions from right to left', () => {
26
+ const add1 = x => x + 1
27
+ const multiply2 = x => x * 2
28
+ const composed = compose(add1, multiply2)
29
+
30
+ expect(composed(3)).toBe(7) // (3 * 2) + 1
31
+ })
32
+
33
+ it('should work with single function', () => {
34
+ const add1 = x => x + 1
35
+ const composed = compose(add1)
36
+
37
+ expect(composed(5)).toBe(6)
38
+ })
39
+
40
+ it('should work with multiple functions', () => {
41
+ const add1 = x => x + 1
42
+ const multiply2 = x => x * 2
43
+ const subtract3 = x => x - 3
44
+ const composed = compose(subtract3, add1, multiply2)
45
+
46
+ expect(composed(5)).toBe(8) // ((5 * 2) + 1) - 3 = 8
47
+ })
48
+ })
49
+
50
+ describe('composeAsync', () => {
51
+ it('should compose async functions from right to left', async () => {
52
+ const add1 = x => x + 1
53
+ const multiplyAsync = async x => x * 2
54
+
55
+ const result = await composeAsync(add1, multiplyAsync)(3)
56
+ expect(result).toBe(7) // (3 * 2) + 1
57
+ })
58
+
59
+ it('should work with all sync functions', async () => {
60
+ const add1 = x => x + 1
61
+ const multiply2 = x => x * 2
62
+
63
+ const result = await composeAsync(add1, multiply2)(3)
64
+ expect(result).toBe(7) // (3 * 2) + 1
65
+ })
66
+
67
+ it('should work with all async functions', async () => {
68
+ const add1Async = async x => x + 1
69
+ const multiply2Async = async x => x * 2
70
+
71
+ const result = await composeAsync(add1Async, multiply2Async)(3)
72
+ expect(result).toBe(7) // (3 * 2) + 1
73
+ })
74
+
75
+ it('should work with mixed sync and async functions', async () => {
76
+ const add1 = x => x + 1
77
+ const multiply2Async = async x => x * 2
78
+ const subtract3 = x => x - 3
79
+
80
+ const result = await composeAsync(subtract3, add1, multiply2Async)(5)
81
+ expect(result).toBe(8) // ((5 * 2) + 1) - 3 = 8
82
+ })
83
+ })
84
+
85
+ describe('pipe (sync)', () => {
86
+ it('should pipe functions from left to right', () => {
87
+ const add1 = x => x + 1
88
+ const multiply2 = x => x * 2
89
+ const piped = pipe(add1, multiply2)
90
+
91
+ expect(piped(3)).toBe(8) // (3 + 1) * 2
92
+ })
93
+
94
+ it('should work with single function', () => {
95
+ const add1 = x => x + 1
96
+ const piped = pipe(add1)
97
+
98
+ expect(piped(5)).toBe(6)
99
+ })
100
+
101
+ it('should work with multiple functions', () => {
102
+ const add1 = x => x + 1
103
+ const multiply2 = x => x * 2
104
+ const subtract3 = x => x - 3
105
+ const piped = pipe(add1, multiply2, subtract3)
106
+
107
+ expect(piped(5)).toBe(9) // ((5 + 1) * 2) - 3 = 9
108
+ })
109
+ })
110
+
111
+ describe('pipeAsync', () => {
112
+ it('should pipe async functions from left to right', async () => {
113
+ const add1 = x => x + 1
114
+ const multiplyAsync = async x => x * 2
115
+
116
+ const result = await pipeAsync(add1, multiplyAsync)(3)
117
+ expect(result).toBe(8) // (3 + 1) * 2
118
+ })
119
+
120
+ it('should work with all sync functions', async () => {
121
+ const add1 = x => x + 1
122
+ const multiply2 = x => x * 2
123
+
124
+ const result = await pipeAsync(add1, multiply2)(3)
125
+ expect(result).toBe(8) // (3 + 1) * 2
126
+ })
127
+
128
+ it('should work with all async functions', async () => {
129
+ const add1Async = async x => x + 1
130
+ const multiply2Async = async x => x * 2
131
+
132
+ const result = await pipeAsync(add1Async, multiply2Async)(3)
133
+ expect(result).toBe(8) // (3 + 1) * 2
134
+ })
135
+
136
+ it('should work with mixed sync and async functions', async () => {
137
+ const add1 = x => x + 1
138
+ const multiply2Async = async x => x * 2
139
+ const subtract3 = x => x - 3
140
+
141
+ const result = await pipeAsync(add1, multiply2Async, subtract3)(5)
142
+ expect(result).toBe(9) // ((5 + 1) * 2) - 3 = 9
143
+ })
144
+
145
+ it('should handle promises in the pipeline', async () => {
146
+ const asyncAdd = async x => {
147
+ await new Promise(resolve => setTimeout(resolve, 1))
148
+ return x + 1
149
+ }
150
+ const multiply2 = x => x * 2
151
+
152
+ const result = await pipeAsync(asyncAdd, multiply2)(3)
153
+ expect(result).toBe(8) // (3 + 1) * 2
154
+ })
155
+ })
156
+
157
+ describe('times', () => {
158
+ it('should call function n times with index', () => {
159
+ const result = times(i => i * 2, 3)
160
+ expect(result).toEqual([0, 2, 4])
161
+ })
162
+
163
+ it('should work with zero iterations', () => {
164
+ const result = times(i => i, 0)
165
+ expect(result).toEqual([])
166
+ })
167
+
168
+ it('should work with functions that ignore index', () => {
169
+ const result = times(() => 'hello', 2)
170
+ expect(result).toEqual(['hello', 'hello'])
171
+ })
172
+ })
173
+
174
+ // Edge cases and error handling
175
+ describe('edge cases', () => {
176
+ it('should handle empty function arrays in compose', () => {
177
+ const composed = compose()
178
+ expect(composed(5)).toBe(5)
179
+ })
180
+
181
+ it('should handle empty function arrays in pipe', () => {
182
+ const piped = pipe()
183
+ expect(piped(5)).toBe(5)
184
+ })
185
+
186
+ it('should handle empty function arrays in composeAsync', async () => {
187
+ const result = await composeAsync()(5)
188
+ expect(result).toBe(5)
189
+ })
190
+
191
+ it('should handle empty function arrays in pipeAsync', async () => {
192
+ const result = await pipeAsync()(5)
193
+ expect(result).toBe(5)
194
+ })
195
+ })
196
+ })
@@ -4,7 +4,7 @@ export function splitUnit(string) {
4
4
  // If there are no units, return the number straight.
5
5
  if (typeof string === 'number') return [string, null]
6
6
 
7
- const regex = /(-?\d*\.?\d+)([a-z]+)/i
7
+ const regex = /(-?\d*\.?\d+)\s*([a-z]+)/i
8
8
  const match = string.match(regex)
9
9
 
10
10
  if (match) return [parseFloat(match[1]), match[2]]
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { splitUnit } from './index.js'
3
+
4
+ describe('splitUnit', () => {
5
+ test('handles number input', () => {
6
+ const result = splitUnit(42)
7
+ expect(result).toEqual([42, null])
8
+ })
9
+
10
+ test('handles unitless string numbers', () => {
11
+ const result = splitUnit('100')
12
+ expect(result).toEqual([100, null])
13
+ })
14
+
15
+ test('handles "3hours" without space', () => {
16
+ const result = splitUnit('3hours')
17
+ expect(result).toEqual([3, 'hours'])
18
+ })
19
+
20
+ test('handles "2rem" CSS unit', () => {
21
+ const result = splitUnit('2rem')
22
+ expect(result).toEqual([2, 'rem'])
23
+ })
24
+
25
+ test('handles decimal values', () => {
26
+ const result = splitUnit('1.5em')
27
+ expect(result).toEqual([1.5, 'em'])
28
+ })
29
+
30
+ test('handles negative values', () => {
31
+ const result = splitUnit('-10px')
32
+ expect(result).toEqual([-10, 'px'])
33
+ })
34
+
35
+ // Tests with spaces
36
+ test('handles values with spaces', () => {
37
+ const result = splitUnit('30 days')
38
+ expect(result).toEqual([30, 'days'])
39
+ })
40
+
41
+ test('handles values with many spaces between number and unit', () => {
42
+ const result = splitUnit('10 em')
43
+ expect(result).toEqual([10, 'em'])
44
+ })
45
+ })
@@ -1,7 +1,45 @@
1
- const DEFAULT_OPTIONS = { array: 'concat' }
2
-
1
+ const DEFAULT_OPTIONS = { array: 'replace' } // Can be concat or replace
2
+
3
+ /**
4
+ * Deep merges multiple objects into a new object with intelligent handling of arrays and nested objects.
5
+ *
6
+ * @param {...Object} sources - Objects to merge. Null and undefined values are skipped.
7
+ * @return {Object} A new object containing the merged properties from all sources.
8
+ *
9
+ * @example
10
+ * const obj1 = { a: 1, b: { x: 10 } }
11
+ * const obj2 = { b: { y: 20 }, c: 3 }
12
+ * const result = mix(obj1, obj2)
13
+ * // Returns: { a: 1, b: { x: 10, y: 20 }, c: 3 }
14
+ */
3
15
  export const mix = createMix()
4
-
16
+ export const concatMix = createMix({ array: 'concat' })
17
+
18
+ /**
19
+ * Creates a customized mix function with specific options for array handling.
20
+ *
21
+ * @param {Object} [userOptions={}] - Configuration options for the mix function.
22
+ * @param {('replace'|'concat')} [userOptions.array='replace'] - How to handle array merging:
23
+ * - 'replace': Arrays from later sources completely replace arrays from earlier sources
24
+ * - 'concat': Arrays are concatenated together
25
+ * @return {Function} A mix function configured with the provided options.
26
+ *
27
+ * @example
28
+ * // Create mix function that concatenates arrays
29
+ * const concatMix = createMix({ array: 'concat' })
30
+ * const obj1 = { items: [1, 2] }
31
+ * const obj2 = { items: [3, 4] }
32
+ * const result = concatMix(obj1, obj2)
33
+ * // Returns: { items: [1, 2, 3, 4] }
34
+ *
35
+ * @example
36
+ * // Create mix function that replaces arrays (default behavior)
37
+ * const replaceMix = createMix({ array: 'replace' })
38
+ * const obj1 = { items: [1, 2] }
39
+ * const obj2 = { items: [3, 4] }
40
+ * const result = replaceMix(obj1, obj2)
41
+ * // Returns: { items: [3, 4] }
42
+ */
5
43
  export function createMix(userOptions = {}) {
6
44
  const options = { ...DEFAULT_OPTIONS, ...userOptions }
7
45
 
@@ -18,6 +56,15 @@ export function createMix(userOptions = {}) {
18
56
  return _mix
19
57
  }
20
58
 
59
+ /**
60
+ * Recursively merges properties from input object into output object.
61
+ * Handles property descriptors, prevents prototype pollution, and deep clones values.
62
+ *
63
+ * @param {Object} output - The target object to merge properties into.
64
+ * @param {Object} input - The source object to merge properties from.
65
+ * @param {Object} options - Merge options including array handling strategy.
66
+ * @property {('replace'|'concat')} options.array - How to handle array merging.
67
+ */
21
68
  function merge(output, input, options) {
22
69
  const props = Object.keys(input)
23
70
 
@@ -63,7 +110,18 @@ function merge(output, input, options) {
63
110
  }
64
111
  }
65
112
 
66
- // Creates a deep clone for each value
113
+ /**
114
+ * Creates a deep clone of a property descriptor value.
115
+ * Handles arrays, objects, dates, maps, sets, and other types appropriately.
116
+ *
117
+ * @param {*} value - The value to clone.
118
+ * @return {*} A deep clone of the input value.
119
+ * @property {Array} value - Cloned recursively with all elements deep cloned.
120
+ * @property {Object} value - Cloned with all properties and descriptors preserved.
121
+ * @property {Date} value - Cloned as new Date with same time value.
122
+ * @property {Map} value - Cloned with all entries deep cloned.
123
+ * @property {Set} value - Cloned with all entries deep cloned.
124
+ */
67
125
  function cloneDescriptorValue(value) {
68
126
  // Arrays
69
127
  if (objectType(value) === '[object Array]') {
@@ -116,6 +174,18 @@ function cloneDescriptorValue(value) {
116
174
  return value
117
175
  }
118
176
 
177
+ /**
178
+ * Returns the precise object type of a value using Object.prototype.toString.
179
+ *
180
+ * @param {*} value - The value to get the type of.
181
+ * @return {string} The object type string (e.g., '[object Array]', '[object Object]', '[object Date]').
182
+ *
183
+ * @example
184
+ * objectType([]) // '[object Array]'
185
+ * objectType({}) // '[object Object]'
186
+ * objectType(new Date()) // '[object Date]'
187
+ * objectType(null) // '[object Null]'
188
+ */
119
189
  function objectType(value) {
120
190
  return Object.prototype.toString.call(value)
121
191
  }
@@ -1,6 +1,6 @@
1
1
  import { expect, it } from 'vitest'
2
2
 
3
- import { createMix, mix } from './mix.js'
3
+ import { concatMix, createMix, mix } from './mix.js'
4
4
 
5
5
  it('Merge objects', () => {
6
6
  const one = { one: 'one', overwrite: 'nope' }
@@ -176,7 +176,8 @@ it('Double objects with nested same array', () => {
176
176
 
177
177
  const three = mix(one, two)
178
178
  expect(typeof three.array.push).toBe('function')
179
- expect(three.array).toHaveLength(6)
179
+ expect(three.array).toHaveLength(3)
180
+ expect(three.array).toEqual([4, 5, 6])
180
181
  })
181
182
 
182
183
  it('Objects in Arrays', () => {
@@ -217,6 +218,54 @@ it('Deep nested arrays', () => {
217
218
  expect(two.array[1]).not.toBe(three.array[1])
218
219
  })
219
220
 
221
+ it('concatMix: concatenates arrays', () => {
222
+ const one = {
223
+ array: [1, 2, 3],
224
+ }
225
+ const two = {
226
+ array: [4, 5, 6],
227
+ }
228
+
229
+ const three = concatMix(one, two)
230
+ expect(typeof three.array.push).toBe('function')
231
+ expect(three.array).toHaveLength(6)
232
+ expect(three.array).toEqual([1, 2, 3, 4, 5, 6])
233
+ })
234
+
235
+ it('concatMix: empty array concat', () => {
236
+ const one = {}
237
+ const two = { array: [1, 2, 3] }
238
+ const three = concatMix(one, two)
239
+
240
+ expect(three).toEqual(two)
241
+ expect(typeof three.array.push).toBe('function')
242
+ expect(three.array).toHaveLength(3)
243
+
244
+ // Prevent mutation
245
+ three.array.push(4)
246
+ expect(two.array).toHaveLength(3)
247
+ expect(three.array).toHaveLength(4)
248
+ })
249
+
250
+ it('concatMix: nested objects with concatenated arrays', () => {
251
+ const one = {
252
+ nested: {
253
+ items: [1, 2],
254
+ other: 'value1',
255
+ },
256
+ }
257
+ const two = {
258
+ nested: {
259
+ items: [3, 4],
260
+ other: 'value2',
261
+ },
262
+ }
263
+
264
+ const three = concatMix(one, two)
265
+ expect(three.nested.items).toEqual([1, 2, 3, 4])
266
+ expect(three.nested.other).toBe('value2')
267
+ })
268
+
220
269
  it('Functions (cannot be cloned)', () => {
221
270
  const one = {}
222
271
  const two = {
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs/promises'
2
- import glob from 'glob-promise'
2
+ import { glob } from 'glob'
3
3
  import path from 'node:path'
4
4
  import { sort } from '../lib/arrays/index.js'
5
5