@ditojs/utils 2.0.0 → 2.1.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 +3 -3
- package/src/array/shuffle.js +1 -1
- package/src/array/shuffle.test.js +3 -1
- package/src/base/base.js +25 -16
- package/src/base/base.test.js +22 -7
- package/src/class/mixin.js +1 -1
- package/src/class/mixin.test.js +13 -8
- package/src/dataPath/getEntriesAtDataPath.js +3 -1
- package/src/dataPath/getValueAtDataPath.js +13 -8
- package/src/dataPath/setDataPathEntries.test.js +6 -4
- package/src/function/toAsync.test.js +2 -2
- package/src/html/escapeHtml.js +7 -2
- package/src/object/clone.js +16 -10
- package/src/object/equals.test.js +71 -28
- package/src/object/groupBy.test.js +8 -6
- package/src/object/mapKeys.js +0 -1
- package/src/object/mapValues.js +0 -1
- package/src/object/merge.js +8 -4
- package/src/string/camelize.js +9 -8
- package/src/string/camelize.test.js +4 -1
- package/src/string/capitalize.js +3 -2
- package/src/string/decamelize.js +12 -10
- package/src/string/deindent.js +10 -6
- package/src/string/deindent.test.js +11 -11
- package/src/string/format.js +25 -21
- package/src/string/format.test.js +7 -7
- package/src/string/isCreditCard.js +2 -1
- package/src/string/isDomain.js +2 -1
- package/src/string/isEmail.js +2 -1
- package/src/string/isHostname.js +2 -1
- package/src/string/isUrl.js +9 -9
- package/src/string/isUrl.test.js +2 -2
- package/src/string/labelize.js +10 -8
- package/src/timer/debounce.js +2 -2
- package/src/timer/debounceAsync.js +2 -2
- package/src/timer/debounceAsync.test.js +51 -58
- package/types/index.d.ts +9 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"punycode": "^2.3.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"typescript": "^5.0.
|
|
32
|
+
"typescript": "^5.0.4"
|
|
33
33
|
},
|
|
34
34
|
"types": "types",
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "5c5048b661395268e720b9ec402d4301bb4a34aa",
|
|
36
36
|
"scripts": {
|
|
37
37
|
"types": "tsc --noEmit types/index.d.ts"
|
|
38
38
|
},
|
package/src/array/shuffle.js
CHANGED
|
@@ -17,7 +17,9 @@ describe('shuffle()', () => {
|
|
|
17
17
|
const res = shuffle([1, 2])
|
|
18
18
|
results[res] = res
|
|
19
19
|
}
|
|
20
|
-
const sorted = Object.entries(results)
|
|
20
|
+
const sorted = Object.entries(results)
|
|
21
|
+
.sort()
|
|
22
|
+
.map(([, value]) => value)
|
|
21
23
|
expect(sorted).toStrictEqual([[1, 2], [2, 1]])
|
|
22
24
|
})
|
|
23
25
|
})
|
package/src/base/base.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export const { isArray } = Array
|
|
2
2
|
const { toString } = Object.prototype
|
|
3
3
|
|
|
4
|
-
export const is =
|
|
4
|
+
export const is = (
|
|
5
|
+
Object.is ||
|
|
5
6
|
// SameValue algorithm:
|
|
6
7
|
// istanbul ignore next
|
|
7
8
|
// eslint-disable-next-line no-self-compare
|
|
8
|
-
(x, y) => x === y ? x !== 0 || 1 / x === 1 / y : x !== x && y !== y
|
|
9
|
+
((x, y) => (x === y ? x !== 0 || 1 / x === 1 / y : x !== x && y !== y))
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
export function isPlainObject(arg) {
|
|
@@ -13,11 +14,14 @@ export function isPlainObject(arg) {
|
|
|
13
14
|
// We also need to check for ctor.name === 'Object', in case this is an object
|
|
14
15
|
// from another global scope (e.g. another vm context in Node.js).
|
|
15
16
|
// When an value has no constructor, it was created with `Object.create(null)`
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
ctor
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
return (
|
|
18
|
+
!!arg && (
|
|
19
|
+
ctor && (
|
|
20
|
+
ctor === Object ||
|
|
21
|
+
ctor.name === 'Object'
|
|
22
|
+
) ||
|
|
23
|
+
!ctor && !isModule(arg)
|
|
24
|
+
)
|
|
21
25
|
)
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -35,7 +39,7 @@ function getPrimitiveCheck(name) {
|
|
|
35
39
|
// here because `new Date().valueOf()` also returns a number.
|
|
36
40
|
const typeName = name.toLowerCase()
|
|
37
41
|
const toStringName = `[object ${name}]`
|
|
38
|
-
return function(arg) {
|
|
42
|
+
return function (arg) {
|
|
39
43
|
const type = typeof arg
|
|
40
44
|
return (
|
|
41
45
|
type === typeName ||
|
|
@@ -64,13 +68,16 @@ export function isPromise(arg) {
|
|
|
64
68
|
return !!arg && isFunction(arg.then) && isFunction(arg.catch)
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
export const isInteger =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
export const isInteger = (
|
|
72
|
+
Number.isInteger ||
|
|
73
|
+
function isInteger(arg) {
|
|
74
|
+
return (
|
|
75
|
+
isNumber(arg) &&
|
|
76
|
+
isFinite(arg) &&
|
|
77
|
+
Math.floor(arg) === arg
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
74
81
|
|
|
75
82
|
export function isAsync(arg) {
|
|
76
83
|
return arg?.[Symbol.toStringTag] === 'AsyncFunction'
|
|
@@ -88,9 +95,11 @@ export function isArrayLike(arg) {
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
export function isEmpty(arg) {
|
|
91
|
-
return
|
|
98
|
+
return (
|
|
99
|
+
arg == null ||
|
|
92
100
|
isArrayLike(arg) && arg.length === 0 ||
|
|
93
101
|
isObject(arg) && Object.keys(arg).length === 0
|
|
102
|
+
)
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
export function asObject(arg) {
|
package/src/base/base.test.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
-
isPlainObject,
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
isPlainObject,
|
|
3
|
+
isObject,
|
|
4
|
+
isArray,
|
|
5
|
+
isModule,
|
|
6
|
+
isFunction,
|
|
7
|
+
isString,
|
|
8
|
+
isBoolean,
|
|
9
|
+
isNumber,
|
|
10
|
+
isDate,
|
|
11
|
+
isRegExp,
|
|
12
|
+
isPromise,
|
|
13
|
+
isAsync,
|
|
14
|
+
isInteger,
|
|
15
|
+
isArrayLike,
|
|
16
|
+
isEmpty,
|
|
17
|
+
asObject,
|
|
18
|
+
asArray,
|
|
19
|
+
asFunction
|
|
5
20
|
} from './base.js'
|
|
6
21
|
|
|
7
22
|
import * as module from './index.js'
|
|
@@ -14,12 +29,12 @@ const boolean = true
|
|
|
14
29
|
const date = new Date()
|
|
15
30
|
const regexp = /x/
|
|
16
31
|
const symbol = Symbol('a')
|
|
17
|
-
const instance = new class {}()
|
|
32
|
+
const instance = new (class {})()
|
|
18
33
|
const promise = new Promise(() => {})
|
|
19
|
-
const func = function() {}
|
|
34
|
+
const func = function () {}
|
|
20
35
|
const lambda = () => {}
|
|
21
|
-
const asyncFunc = async function() {}
|
|
22
|
-
const asyncLambda = async function() {}
|
|
36
|
+
const asyncFunc = async function () {}
|
|
37
|
+
const asyncLambda = async function () {}
|
|
23
38
|
|
|
24
39
|
describe('isPlainObject()', () => {
|
|
25
40
|
describe.each([
|
package/src/class/mixin.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const mixinsMap = new WeakMap()
|
|
4
4
|
|
|
5
5
|
export function mixin(mixinFunction) {
|
|
6
|
-
return function(targetClass) {
|
|
6
|
+
return function (targetClass) {
|
|
7
7
|
// Prevent multiple application of the same mixins in nested inheritance
|
|
8
8
|
// through keeping track of what was already applied in weak-maps:
|
|
9
9
|
if (targetClass && !hasMixin(targetClass, mixinFunction)) {
|
package/src/class/mixin.test.js
CHANGED
|
@@ -5,13 +5,19 @@ describe('mixin()', () => {
|
|
|
5
5
|
function() {}
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const SomeMixin1 = mixin(
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const SomeMixin1 = mixin(
|
|
9
|
+
Class =>
|
|
10
|
+
class extends Class {
|
|
11
|
+
function1() {}
|
|
12
|
+
}
|
|
13
|
+
)
|
|
11
14
|
|
|
12
|
-
const SomeMixin2 = mixin(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const SomeMixin2 = mixin(
|
|
16
|
+
Class =>
|
|
17
|
+
class extends Class {
|
|
18
|
+
function2() {}
|
|
19
|
+
}
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
it('should apply mixin to class', () => {
|
|
17
23
|
const NewClass = SomeMixin1(SomeClass)
|
|
@@ -57,8 +63,7 @@ describe('mixin()', () => {
|
|
|
57
63
|
|
|
58
64
|
it(`should not apply mixin more than once, even if it was applied higher up`, () => {
|
|
59
65
|
const NewClass1 = SomeMixin1(SomeClass)
|
|
60
|
-
class NewClass2 extends NewClass1 {
|
|
61
|
-
}
|
|
66
|
+
class NewClass2 extends NewClass1 {}
|
|
62
67
|
const NewClass3 = SomeMixin1(NewClass2)
|
|
63
68
|
expect(NewClass3).toBe(NewClass2)
|
|
64
69
|
})
|
|
@@ -6,7 +6,9 @@ const NOT_FOUND = Symbol('NOT_FOUND')
|
|
|
6
6
|
export function getEntriesAtDataPath(
|
|
7
7
|
obj,
|
|
8
8
|
path,
|
|
9
|
-
handleError = () => {
|
|
9
|
+
handleError = () => {
|
|
10
|
+
throw new Error(`Invalid path: ${path}`)
|
|
11
|
+
}
|
|
10
12
|
) {
|
|
11
13
|
const parsedPath = parseDataPath(path)
|
|
12
14
|
|
|
@@ -6,7 +6,9 @@ const NOT_FOUND = Symbol('NOT_FOUND')
|
|
|
6
6
|
export function getValueAtDataPath(
|
|
7
7
|
obj,
|
|
8
8
|
path,
|
|
9
|
-
handleError = () => {
|
|
9
|
+
handleError = () => {
|
|
10
|
+
throw new Error(`Invalid path: ${path}`)
|
|
11
|
+
}
|
|
10
12
|
) {
|
|
11
13
|
const parsedPath = parseDataPath(path)
|
|
12
14
|
let index = 0
|
|
@@ -20,18 +22,21 @@ export function getValueAtDataPath(
|
|
|
20
22
|
// Support wildcards on arrays and objects
|
|
21
23
|
const pathNested = parsedPath.slice(index + 1)
|
|
22
24
|
const values = isArray(obj) ? obj : Object.values(obj)
|
|
23
|
-
return values
|
|
24
|
-
value => getValueAtDataPath(value, pathNested, handleError)
|
|
25
|
-
|
|
25
|
+
return values
|
|
26
|
+
.map(value => getValueAtDataPath(value, pathNested, handleError))
|
|
27
|
+
.flat(1)
|
|
26
28
|
} else if (part === '**') {
|
|
27
29
|
// Support deep wildcards on arrays and objects
|
|
28
30
|
const pathNested = parsedPath.slice(index + 1)
|
|
29
31
|
const pathSelf = parsedPath.slice(index)
|
|
30
32
|
const values = isArray(obj) ? obj : Object.values(obj)
|
|
31
|
-
return values
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
return values
|
|
34
|
+
.map(value => [
|
|
35
|
+
...asArray(getValueAtDataPath(value, pathNested, () => NOT_FOUND)),
|
|
36
|
+
...asArray(getValueAtDataPath(value, pathSelf, () => NOT_FOUND))
|
|
37
|
+
])
|
|
38
|
+
.flat(1)
|
|
39
|
+
.filter(value => value !== NOT_FOUND)
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
return handleError?.(obj, part, index)
|
|
@@ -13,10 +13,12 @@ describe('setDataPathEntries()', () => {
|
|
|
13
13
|
const add = { prop: 'new' }
|
|
14
14
|
|
|
15
15
|
it('should add data at a path to a given object and array', () => {
|
|
16
|
-
expect(() =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
expect(() =>
|
|
17
|
+
setDataPathEntries(data, {
|
|
18
|
+
'object.array[0].added': add,
|
|
19
|
+
'object.array[1]': add
|
|
20
|
+
})
|
|
21
|
+
)
|
|
20
22
|
.not.toThrow()
|
|
21
23
|
expect(data.object.array[0].added).toStrictEqual(add)
|
|
22
24
|
expect(data.object.array[1]).toStrictEqual(add)
|
|
@@ -3,7 +3,7 @@ import { toAsync } from './toAsync.js'
|
|
|
3
3
|
describe('toAsync()', () => {
|
|
4
4
|
it('should convert callback functions to async', async () => {
|
|
5
5
|
expect.assertions(1)
|
|
6
|
-
const asyncFunc = toAsync(function(toResolve, callback) {
|
|
6
|
+
const asyncFunc = toAsync(function (toResolve, callback) {
|
|
7
7
|
process.nextTick(() => {
|
|
8
8
|
callback(null, toResolve)
|
|
9
9
|
})
|
|
@@ -16,7 +16,7 @@ describe('toAsync()', () => {
|
|
|
16
16
|
it('should convert callback errors to exceptions', async () => {
|
|
17
17
|
expect.assertions(1)
|
|
18
18
|
const error = new Error('This error is intentional')
|
|
19
|
-
const throwError = toAsync(function(toReject, callback) {
|
|
19
|
+
const throwError = toAsync(function (toReject, callback) {
|
|
20
20
|
process.nextTick(() => {
|
|
21
21
|
callback(toReject)
|
|
22
22
|
})
|
package/src/html/escapeHtml.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export function escapeHtml(html) {
|
|
2
2
|
return html != null
|
|
3
|
-
? html
|
|
4
|
-
|
|
3
|
+
? html
|
|
4
|
+
.toString()
|
|
5
|
+
.replace(
|
|
6
|
+
/["&<>]/g,
|
|
7
|
+
chr =>
|
|
8
|
+
({ '"': '"', '&': '&', '<': '<', '>': '>' }[chr])
|
|
9
|
+
)
|
|
5
10
|
: ''
|
|
6
11
|
}
|
package/src/object/clone.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
isArray,
|
|
2
|
+
isArray,
|
|
3
|
+
isObject,
|
|
4
|
+
isDate,
|
|
5
|
+
isRegExp,
|
|
6
|
+
isFunction,
|
|
7
|
+
isPromise
|
|
3
8
|
} from '../base/index.js'
|
|
4
9
|
import { pick } from './pick.js'
|
|
5
10
|
|
|
@@ -21,11 +26,12 @@ export function clone(value, options) {
|
|
|
21
26
|
return copy
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
const handleValue = value =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
:
|
|
28
|
-
|
|
29
|
+
const handleValue = value =>
|
|
30
|
+
shallow || transferables?.includes(value)
|
|
31
|
+
? value
|
|
32
|
+
: clones.has(value)
|
|
33
|
+
? clones.get(value)
|
|
34
|
+
: cloneValue(value)
|
|
29
35
|
|
|
30
36
|
const cloneValue = value => {
|
|
31
37
|
let copy = value
|
|
@@ -39,15 +45,15 @@ export function clone(value, options) {
|
|
|
39
45
|
copy[i] = handleValue(value[i])
|
|
40
46
|
}
|
|
41
47
|
} else if (isObject(value)) {
|
|
42
|
-
|
|
48
|
+
// Rely on arg.clone() if it exists and assume it creates an actual clone.
|
|
43
49
|
if (isFunction(value.clone)) {
|
|
44
50
|
copy = storeClone(value, value.clone(options))
|
|
45
51
|
} else if (isPromise(value)) {
|
|
46
|
-
|
|
52
|
+
// https://stackoverflow.com/questions/37063293/can-i-clone-a-promise
|
|
47
53
|
copy = storeClone(value, value.then())
|
|
48
54
|
} else {
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
// Prevent calling the actual constructor since it is not guaranteed to
|
|
56
|
+
// work as intended here, and only clone non-inherited own properties.
|
|
51
57
|
copy = storeClone(value, Object.create(Object.getPrototypeOf(value)))
|
|
52
58
|
clones.set(value, copy)
|
|
53
59
|
const keys = enumerable ? Object.keys(value) : Reflect.ownKeys(value)
|
|
@@ -4,27 +4,52 @@ describe('equals()', () => {
|
|
|
4
4
|
const symbol1 = Symbol('a')
|
|
5
5
|
const symbol2 = Symbol('b')
|
|
6
6
|
describe.each([
|
|
7
|
-
[1, 1, true],
|
|
8
|
-
[
|
|
9
|
-
[
|
|
7
|
+
[1, 1, true],
|
|
8
|
+
[1, Object(1), true],
|
|
9
|
+
[1, '1', false],
|
|
10
|
+
[1, 2, false],
|
|
11
|
+
[-0, -0, true],
|
|
12
|
+
[0, 0, true],
|
|
13
|
+
[0, Object(0), true],
|
|
14
|
+
[Object(0), Object(0), true],
|
|
15
|
+
[-0, 0, true],
|
|
16
|
+
[0, '0', false],
|
|
10
17
|
[0, null, false],
|
|
11
18
|
|
|
12
|
-
[NaN, NaN, true],
|
|
13
|
-
[
|
|
14
|
-
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
-
[
|
|
21
|
-
[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
[
|
|
25
|
-
|
|
26
|
-
[
|
|
27
|
-
[
|
|
19
|
+
[NaN, NaN, true],
|
|
20
|
+
[NaN, Object(NaN), true],
|
|
21
|
+
[Object(NaN), Object(NaN), true],
|
|
22
|
+
[NaN, 'a', false],
|
|
23
|
+
[NaN, Infinity, false],
|
|
24
|
+
|
|
25
|
+
['a', 'a', true],
|
|
26
|
+
['a', Object('a'), true],
|
|
27
|
+
[Object('a'), Object('a'), true],
|
|
28
|
+
['a', 'b', false],
|
|
29
|
+
['a', ['a'], false],
|
|
30
|
+
|
|
31
|
+
[true, true, true],
|
|
32
|
+
[true, Object(true), true],
|
|
33
|
+
[Object(true), Object(true), true],
|
|
34
|
+
[true, 1, false],
|
|
35
|
+
[true, 'a', false],
|
|
36
|
+
[false, false, true],
|
|
37
|
+
[false, Object(false), true],
|
|
38
|
+
[Object(false), Object(false), true],
|
|
39
|
+
[false, 0, false],
|
|
40
|
+
[false, '', false],
|
|
41
|
+
|
|
42
|
+
[symbol1, symbol1, true],
|
|
43
|
+
[symbol1, Object(symbol1), true],
|
|
44
|
+
[Object(symbol1), Object(symbol1), true],
|
|
45
|
+
[symbol1, symbol2, false],
|
|
46
|
+
|
|
47
|
+
[null, null, true],
|
|
48
|
+
[null, undefined, false],
|
|
49
|
+
[null, {}, false],
|
|
50
|
+
[null, '', false],
|
|
51
|
+
[undefined, undefined, true],
|
|
52
|
+
[undefined, null, false],
|
|
28
53
|
[undefined, '', false]
|
|
29
54
|
])(
|
|
30
55
|
'equals(%o, %o) (should compare primitives)',
|
|
@@ -91,13 +116,27 @@ describe('equals()', () => {
|
|
|
91
116
|
let array1 = [1, 2, 3]
|
|
92
117
|
let array2 = [1, 2, 3]
|
|
93
118
|
|
|
94
|
-
array1.every =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
array1.every =
|
|
120
|
+
array1.filter =
|
|
121
|
+
array1.forEach =
|
|
122
|
+
array1.indexOf =
|
|
123
|
+
array1.lastIndexOf =
|
|
124
|
+
array1.map =
|
|
125
|
+
array1.some =
|
|
126
|
+
array1.reduce =
|
|
127
|
+
array1.reduceRight =
|
|
128
|
+
null
|
|
129
|
+
|
|
130
|
+
array2.concat =
|
|
131
|
+
array2.join =
|
|
132
|
+
array2.pop =
|
|
133
|
+
array2.reverse =
|
|
134
|
+
array2.shift =
|
|
135
|
+
array2.slice =
|
|
136
|
+
array2.sort =
|
|
137
|
+
array2.splice =
|
|
138
|
+
array2.unshift =
|
|
139
|
+
null
|
|
101
140
|
|
|
102
141
|
expect(equals(array1, array2)).toBe(true)
|
|
103
142
|
|
|
@@ -217,8 +256,12 @@ describe('equals()', () => {
|
|
|
217
256
|
})
|
|
218
257
|
|
|
219
258
|
it('should compare functions', () => {
|
|
220
|
-
function a() {
|
|
221
|
-
|
|
259
|
+
function a() {
|
|
260
|
+
return 1 + 2
|
|
261
|
+
}
|
|
262
|
+
function b() {
|
|
263
|
+
return 1 + 2
|
|
264
|
+
}
|
|
222
265
|
|
|
223
266
|
expect(equals(a, a)).toBe(true)
|
|
224
267
|
expect(equals(a, b)).toBe(false)
|
|
@@ -17,12 +17,14 @@ describe('groupBy()', () => {
|
|
|
17
17
|
[2, 'a'],
|
|
18
18
|
[2, 'b']
|
|
19
19
|
]
|
|
20
|
-
expect(groupBy(array, value => value[0])).toStrictEqual(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
expect(groupBy(array, value => value[0])).toStrictEqual({
|
|
21
|
+
1: [[1, 'a']],
|
|
22
|
+
2: [[2, 'a'], [2, 'b']]
|
|
23
|
+
})
|
|
24
|
+
expect(groupBy(array, value => value[1])).toStrictEqual({
|
|
25
|
+
a: [[1, 'a'], [2, 'a']],
|
|
26
|
+
b: [[2, 'b']]
|
|
27
|
+
})
|
|
26
28
|
})
|
|
27
29
|
|
|
28
30
|
it('should work with an object for `collection`', () => {
|
package/src/object/mapKeys.js
CHANGED
package/src/object/mapValues.js
CHANGED
package/src/object/merge.js
CHANGED
|
@@ -3,10 +3,14 @@ import { clone } from './clone.js'
|
|
|
3
3
|
|
|
4
4
|
export function merge(target, ...sources) {
|
|
5
5
|
const _merge = (target, source, cloneTarget) => {
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
if (
|
|
7
|
+
target &&
|
|
8
|
+
source &&
|
|
9
|
+
target !== source && (
|
|
10
|
+
isArray(target) && isArray(source) ||
|
|
11
|
+
isPlainObject(target) && isPlainObject(source)
|
|
12
|
+
)
|
|
13
|
+
) {
|
|
10
14
|
const result = cloneTarget
|
|
11
15
|
? clone(target)
|
|
12
16
|
: target
|
package/src/string/camelize.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
export function camelize(str, pascalCase = false) {
|
|
2
2
|
return str
|
|
3
3
|
? str
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
// Trim beginnings and ends
|
|
5
|
+
.replace(/^[-_\s]+|[-_\s]+$/g, '')
|
|
6
|
+
.replace(
|
|
7
|
+
/(^|[-_\s]+)(\w)([A-Z]*)([a-z]*)/g,
|
|
8
|
+
(all, sep, first, upperRest, lowerRest) =>
|
|
9
|
+
`${
|
|
10
|
+
pascalCase || sep ? first.toUpperCase() : first.toLowerCase()
|
|
11
|
+
}${upperRest.toLowerCase()}${lowerRest}`
|
|
12
|
+
)
|
|
12
13
|
: ''
|
|
13
14
|
}
|
|
@@ -14,7 +14,10 @@ describe('camelize()', () => {
|
|
|
14
14
|
(pascalCase, expected) => {
|
|
15
15
|
describe.each([
|
|
16
16
|
...strings,
|
|
17
|
-
'foo-bar',
|
|
17
|
+
'foo-bar',
|
|
18
|
+
'foo_bar',
|
|
19
|
+
'--foo-bar--',
|
|
20
|
+
'__foo_bar__'
|
|
18
21
|
])(
|
|
19
22
|
`camelize(%o, ${pascalCase})`,
|
|
20
23
|
value => {
|
package/src/string/capitalize.js
CHANGED
package/src/string/decamelize.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
export function decamelize(str, sep = ' ') {
|
|
2
2
|
return str
|
|
3
3
|
? str
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
.replace(
|
|
5
|
+
// TODO: Once JavaScript supports Unicode property escapes in regexps,
|
|
6
|
+
// switch to better parsing that matches non-ASCII uppercase letters:
|
|
7
|
+
// /([\p{Ll}\d])(\p{Lu})/gu
|
|
8
|
+
// See: https://caniuse.com/?search=Unicode%20property%20escape
|
|
9
|
+
/([a-z\d])([A-Z])|(\S)(?:\s+)(\S)/g,
|
|
10
|
+
(all, lower, upper, beforeSpace, afterSpace) =>
|
|
11
|
+
upper
|
|
12
|
+
? `${lower}${sep}${upper}`
|
|
13
|
+
: `${beforeSpace}${sep}${afterSpace}`
|
|
14
|
+
)
|
|
15
|
+
.toLowerCase()
|
|
14
16
|
: ''
|
|
15
17
|
}
|
|
16
18
|
|
package/src/string/deindent.js
CHANGED
|
@@ -11,11 +11,13 @@ export function deindent(strings, ...values) {
|
|
|
11
11
|
// First, perform interpolation
|
|
12
12
|
let parts = []
|
|
13
13
|
for (let i = 0; i < strings.length; i++) {
|
|
14
|
-
parts.push(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
parts.push(
|
|
15
|
+
strings[i]
|
|
16
|
+
// Join lines when there is a line-break suppressed by a preceding '\\'
|
|
17
|
+
.replace(/\\\n\s*/g, '')
|
|
18
|
+
// Handle escaped back-ticks
|
|
19
|
+
.replace(/\\`/g, '`')
|
|
20
|
+
)
|
|
19
21
|
if (i < values.length) {
|
|
20
22
|
// See if the value itself contains multiple lines, and if so, indent
|
|
21
23
|
// each of them by the whitespace that precedes it except the first.
|
|
@@ -32,7 +34,9 @@ export function deindent(strings, ...values) {
|
|
|
32
34
|
// # line, by making sure no line breaks follow
|
|
33
35
|
// # until the end.
|
|
34
36
|
const str = parts.join('')
|
|
35
|
-
const match = str.match(
|
|
37
|
+
const match = str.match(
|
|
38
|
+
/((?:^[\n\r]*|[\n\r])(?:[\n\r]*))(\s+)(?:[^\n\r]*)$/
|
|
39
|
+
)
|
|
36
40
|
const [, lineBreaks = '', indent = ''] = match || []
|
|
37
41
|
parts = [str]
|
|
38
42
|
parts.push(lines.join(`${getLineBreak(lineBreaks)}${indent}`))
|
|
@@ -8,7 +8,7 @@ describe('deindent()', () => {
|
|
|
8
8
|
indented
|
|
9
9
|
text`
|
|
10
10
|
).toBe(
|
|
11
|
-
`some
|
|
11
|
+
`some
|
|
12
12
|
indented
|
|
13
13
|
text`
|
|
14
14
|
)
|
|
@@ -22,7 +22,7 @@ describe('deindent()', () => {
|
|
|
22
22
|
text
|
|
23
23
|
`
|
|
24
24
|
).toBe(
|
|
25
|
-
`some
|
|
25
|
+
`some
|
|
26
26
|
indented
|
|
27
27
|
text
|
|
28
28
|
`
|
|
@@ -39,7 +39,7 @@ describe('deindent()', () => {
|
|
|
39
39
|
text
|
|
40
40
|
`
|
|
41
41
|
).toBe(
|
|
42
|
-
`
|
|
42
|
+
`
|
|
43
43
|
|
|
44
44
|
some
|
|
45
45
|
indented
|
|
@@ -58,7 +58,7 @@ some
|
|
|
58
58
|
|
|
59
59
|
`
|
|
60
60
|
).toBe(
|
|
61
|
-
`some
|
|
61
|
+
`some
|
|
62
62
|
indented
|
|
63
63
|
text
|
|
64
64
|
|
|
@@ -76,7 +76,7 @@ some
|
|
|
76
76
|
indented
|
|
77
77
|
text`
|
|
78
78
|
).toBe(
|
|
79
|
-
`some
|
|
79
|
+
`some
|
|
80
80
|
indented
|
|
81
81
|
text
|
|
82
82
|
some
|
|
@@ -91,7 +91,7 @@ some
|
|
|
91
91
|
indented
|
|
92
92
|
text`
|
|
93
93
|
).toBe(
|
|
94
|
-
`some
|
|
94
|
+
`some
|
|
95
95
|
indented
|
|
96
96
|
text`
|
|
97
97
|
)
|
|
@@ -106,7 +106,7 @@ some
|
|
|
106
106
|
${singleLineText}
|
|
107
107
|
`
|
|
108
108
|
).toBe(
|
|
109
|
-
`some
|
|
109
|
+
`some
|
|
110
110
|
indented
|
|
111
111
|
single-line text
|
|
112
112
|
`
|
|
@@ -122,7 +122,7 @@ indented
|
|
|
122
122
|
${multiLineText}
|
|
123
123
|
`
|
|
124
124
|
).toBe(
|
|
125
|
-
`some
|
|
125
|
+
`some
|
|
126
126
|
indented
|
|
127
127
|
multi-
|
|
128
128
|
line
|
|
@@ -140,7 +140,7 @@ text
|
|
|
140
140
|
${multiLineText}
|
|
141
141
|
`
|
|
142
142
|
).toBe(
|
|
143
|
-
`some
|
|
143
|
+
`some
|
|
144
144
|
indented
|
|
145
145
|
multi-
|
|
146
146
|
line
|
|
@@ -158,7 +158,7 @@ text
|
|
|
158
158
|
content: ${multiLineText}
|
|
159
159
|
`
|
|
160
160
|
).toBe(
|
|
161
|
-
`some
|
|
161
|
+
`some
|
|
162
162
|
indented
|
|
163
163
|
content: multi-
|
|
164
164
|
line
|
|
@@ -173,7 +173,7 @@ text
|
|
|
173
173
|
indented
|
|
174
174
|
text`
|
|
175
175
|
).toBe(
|
|
176
|
-
`some
|
|
176
|
+
`some
|
|
177
177
|
indented
|
|
178
178
|
text`
|
|
179
179
|
)
|
package/src/string/format.js
CHANGED
|
@@ -32,14 +32,14 @@ export function format(value, {
|
|
|
32
32
|
? defaultOption
|
|
33
33
|
: isObject(option)
|
|
34
34
|
? Object.entries({
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
...defaultOption,
|
|
36
|
+
...option
|
|
37
|
+
}).reduce((opt, [key, value]) => {
|
|
38
|
+
if (value !== false) {
|
|
39
|
+
opt[key] = value
|
|
40
|
+
}
|
|
41
|
+
return opt
|
|
42
|
+
}, {})
|
|
43
43
|
: {}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -62,9 +62,11 @@ export function format(value, {
|
|
|
62
62
|
// e.g. to replace decimals and such:
|
|
63
63
|
const parts = new Intl.NumberFormat(locale, options)
|
|
64
64
|
.formatToParts(value)
|
|
65
|
-
return parts
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
return parts
|
|
66
|
+
.map(
|
|
67
|
+
({ type, value }) => options.format(value, type, options) ?? value
|
|
68
|
+
)
|
|
69
|
+
.join('')
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
} else if (isDate(value)) {
|
|
@@ -86,16 +88,18 @@ export function format(value, {
|
|
|
86
88
|
const parts = new Intl.DateTimeFormat(locale, options)
|
|
87
89
|
.formatToParts(value)
|
|
88
90
|
let modeOpts = null
|
|
89
|
-
return parts
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
return parts
|
|
92
|
+
.map(({ type, value }) => {
|
|
93
|
+
if (type !== 'literal') {
|
|
94
|
+
// Switch mode between date/time to pick the right format method:
|
|
95
|
+
const mode = ['weekday', 'day', 'month', 'year'].includes(type)
|
|
96
|
+
? 'date'
|
|
97
|
+
: 'time'
|
|
98
|
+
modeOpts = opts[mode]
|
|
99
|
+
}
|
|
100
|
+
return modeOpts?.format?.(value, type, modeOpts) ?? value
|
|
101
|
+
})
|
|
102
|
+
.join('')
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
}
|
|
@@ -148,11 +148,12 @@ describe('format()', () => {
|
|
|
148
148
|
number: {
|
|
149
149
|
style: 'currency',
|
|
150
150
|
currency: 'GBP',
|
|
151
|
-
format: (value, type) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
format: (value, type) =>
|
|
152
|
+
type === 'group'
|
|
153
|
+
? `’`
|
|
154
|
+
: type === 'decimal'
|
|
155
|
+
? ','
|
|
156
|
+
: undefined
|
|
156
157
|
}
|
|
157
158
|
})
|
|
158
159
|
).toBe(`£123’456’789,00`)
|
|
@@ -166,11 +167,10 @@ describe('format()', () => {
|
|
|
166
167
|
day: 'numeric',
|
|
167
168
|
month: 'numeric',
|
|
168
169
|
year: 'numeric',
|
|
169
|
-
format: (value, type, options) =>
|
|
170
|
+
format: (value, type, options) =>
|
|
170
171
|
type === 'literal' && options.month === 'numeric'
|
|
171
172
|
? value.replace(/\//, '.').replace(/,/, '')
|
|
172
173
|
: undefined
|
|
173
|
-
)
|
|
174
174
|
},
|
|
175
175
|
time: {
|
|
176
176
|
hour: '2-digit',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const creditCardRegExp =
|
|
1
|
+
const creditCardRegExp =
|
|
2
|
+
/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})|62[0-9]{14}$/
|
|
2
3
|
|
|
3
4
|
export function isCreditCard(str) {
|
|
4
5
|
str = str && str.replace(/[\s-]+/g, '')
|
package/src/string/isDomain.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import punycode from 'punycode'
|
|
3
3
|
|
|
4
4
|
// Best effort approach, allowing Internationalized domain name (with punycode)
|
|
5
|
-
const domainRegExp =
|
|
5
|
+
const domainRegExp =
|
|
6
|
+
/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i
|
|
6
7
|
|
|
7
8
|
export function isDomain(str) {
|
|
8
9
|
return !!(str && domainRegExp.test(punycode.toASCII(str)))
|
package/src/string/isEmail.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const emailRegExp =
|
|
1
|
+
const emailRegExp =
|
|
2
|
+
/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
|
|
2
3
|
|
|
3
4
|
export function isEmail(str) {
|
|
4
5
|
return !!(str && emailRegExp.test(str))
|
package/src/string/isHostname.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// https://tools.ietf.org/html/rfc1034#section-3.5
|
|
2
2
|
// https://tools.ietf.org/html/rfc1123#section-2
|
|
3
|
-
const hostnameRegExp =
|
|
3
|
+
const hostnameRegExp =
|
|
4
|
+
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*$/i
|
|
4
5
|
|
|
5
6
|
export function isHostname(str) {
|
|
6
7
|
return !!(str && hostnameRegExp.test(str))
|
package/src/string/isUrl.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
// isUrl() returns `true` for 'google.com', and isAbsoluteUrl() returns `false`!
|
|
3
3
|
|
|
4
4
|
const isUrlRegExp = new RegExp(
|
|
5
|
-
'^((https?|ftps?|mailto|rtsp|mms)?://)?' +
|
|
6
|
-
|
|
5
|
+
'^((https?|ftps?|mailto|rtsp|mms)?://)?' + // user:pass@
|
|
6
|
+
"(([0-9a-z_!~*'().&=+$%-]+:)?[0-9a-z_!~*'().&=+$%-]*@)?" +
|
|
7
7
|
'(' +
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
'(\\d{1,3}\\.){3}\\d{1,3}' + // ip
|
|
9
|
+
'|' +
|
|
10
|
+
'([0-9a-z_-]+\\.)*' + // sub-domain: www.
|
|
11
|
+
'[0-9a-z][0-9a-z_-]{0,61}[0-9a-z]\\.' + // domain-name
|
|
12
|
+
// tld - https://tools.ietf.org/id/draft-liman-tld-names-00.html#rfc.section.2
|
|
13
|
+
'([a-z]{2,61}|xn--[0-9a-z][0-9a-z-]{0,61}[0-9a-z])' +
|
|
14
14
|
')' + // top level domain.
|
|
15
15
|
'(:[0-9]{1,5})?' + // port
|
|
16
16
|
'((/?)|' + // allow ending in a slash
|
|
17
|
-
|
|
17
|
+
"([/?#][0-9a-z_!~*'().;:@&=+$,%/?#-]+)+/?)$", // path
|
|
18
18
|
'i'
|
|
19
19
|
)
|
|
20
20
|
|
package/src/string/isUrl.test.js
CHANGED
|
@@ -24,7 +24,7 @@ describe('isUrl()', () => {
|
|
|
24
24
|
'http://10.0.0.0/',
|
|
25
25
|
'http://189.123.14.13/',
|
|
26
26
|
'http://duckduckgo.com/?q=%2F',
|
|
27
|
-
|
|
27
|
+
"http://foobar.com/t$-_.+!*'(),",
|
|
28
28
|
'http://foobar.com/?foo=bar#baz=qux',
|
|
29
29
|
'http://foobar.com?foo=bar',
|
|
30
30
|
'http://foobar.com#baz=qux',
|
|
@@ -76,7 +76,7 @@ describe('isUrl()', () => {
|
|
|
76
76
|
'http://localhost:61500this is an invalid url!!!!',
|
|
77
77
|
'////foobar.com',
|
|
78
78
|
'http:////foobar.com',
|
|
79
|
-
|
|
79
|
+
"https://example.com/foo/<script>alert('XSS')</script>/"
|
|
80
80
|
// TODO: block this: 'mailto:foo@bar.com'
|
|
81
81
|
])(
|
|
82
82
|
'isUrl(%o)',
|
package/src/string/labelize.js
CHANGED
|
@@ -2,13 +2,15 @@ export function labelize(str) {
|
|
|
2
2
|
// Handle hyphenated, underscored and camel-cased property names and
|
|
3
3
|
// expand them to title cased labels.
|
|
4
4
|
return str
|
|
5
|
-
? str.replace(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
? str.replace(
|
|
6
|
+
/([-_ ]|^)(\w)|([a-z])(?=[A-Z0-9])|(\d)([a-zA-Z])/g,
|
|
7
|
+
function (all, hyphen, hyphenated, camel, decimal, decimalNext) {
|
|
8
|
+
return hyphenated
|
|
9
|
+
? `${hyphen ? ' ' : ''}${hyphenated.toUpperCase()}`
|
|
10
|
+
: camel
|
|
11
|
+
? `${camel} `
|
|
12
|
+
: `${decimal} ${decimalNext.toUpperCase()}`
|
|
13
|
+
}
|
|
14
|
+
)
|
|
13
15
|
: ''
|
|
14
16
|
}
|
package/src/timer/debounce.js
CHANGED
|
@@ -6,7 +6,7 @@ export function debounce(func, options) {
|
|
|
6
6
|
let timer = null
|
|
7
7
|
let result
|
|
8
8
|
|
|
9
|
-
const debounced = function(...args) {
|
|
9
|
+
const debounced = function (...args) {
|
|
10
10
|
const callNow = immediate && !timer
|
|
11
11
|
clearTimeout(timer)
|
|
12
12
|
timer = setTimeout(async () => {
|
|
@@ -21,7 +21,7 @@ export function debounce(func, options) {
|
|
|
21
21
|
return result
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
debounced.cancel = function() {
|
|
24
|
+
debounced.cancel = function () {
|
|
25
25
|
const pending = timer !== null
|
|
26
26
|
if (pending) {
|
|
27
27
|
clearTimeout(timer)
|
|
@@ -26,7 +26,7 @@ export function debounceAsync(func, options) {
|
|
|
26
26
|
callbacks = []
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const debounced = function(...args) {
|
|
29
|
+
const debounced = function (...args) {
|
|
30
30
|
return new Promise((resolve, reject) => {
|
|
31
31
|
const callNow = immediate && !timer
|
|
32
32
|
clearTimeout(timer)
|
|
@@ -45,7 +45,7 @@ export function debounceAsync(func, options) {
|
|
|
45
45
|
})
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
debounced.cancel = function() {
|
|
48
|
+
debounced.cancel = function () {
|
|
49
49
|
const pending = timer !== null
|
|
50
50
|
if (pending) {
|
|
51
51
|
resolve(undefined)
|
|
@@ -53,7 +53,9 @@ describe('debounceAsync()', () => {
|
|
|
53
53
|
|
|
54
54
|
it('should pass through `this`', async () => {
|
|
55
55
|
expect.assertions(2)
|
|
56
|
-
const func = vi.fn(async function() {
|
|
56
|
+
const func = vi.fn(async function () {
|
|
57
|
+
return this
|
|
58
|
+
})
|
|
57
59
|
const debounced = debounceAsync(func, 1000)
|
|
58
60
|
const obj = {}
|
|
59
61
|
const promises = []
|
|
@@ -82,69 +84,60 @@ describe('debounceAsync()', () => {
|
|
|
82
84
|
expect(func).toBeCalledTimes(1)
|
|
83
85
|
})
|
|
84
86
|
|
|
85
|
-
it(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
it('should execute once immediately if intervals are less than wait', async () => {
|
|
88
|
+
expect.assertions(2)
|
|
89
|
+
const func = vi.fn()
|
|
90
|
+
const debounced = debounceAsync(func, { delay: 1000, immediate: true })
|
|
91
|
+
const promises = []
|
|
92
|
+
promises.push(debounced())
|
|
93
|
+
expect(func).toBeCalledTimes(1)
|
|
94
|
+
for (let i = 0; i < 10; i++) {
|
|
95
|
+
vi.advanceTimersByTime(500)
|
|
92
96
|
promises.push(debounced())
|
|
93
|
-
expect(func).toBeCalledTimes(1)
|
|
94
|
-
for (let i = 0; i < 10; i++) {
|
|
95
|
-
vi.advanceTimersByTime(500)
|
|
96
|
-
promises.push(debounced())
|
|
97
|
-
}
|
|
98
|
-
await Promise.all(promises)
|
|
99
|
-
expect(func).toBeCalledTimes(1)
|
|
100
97
|
}
|
|
101
|
-
|
|
98
|
+
await Promise.all(promises)
|
|
99
|
+
expect(func).toBeCalledTimes(1)
|
|
100
|
+
})
|
|
102
101
|
|
|
103
|
-
it(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < 10; i++) {
|
|
113
|
-
vi.advanceTimersByTime(500)
|
|
114
|
-
promises.push(debounced())
|
|
115
|
-
}
|
|
116
|
-
expect(func).toBeCalledTimes(1)
|
|
117
|
-
vi.advanceTimersByTime(1000)
|
|
102
|
+
it('should execute twice immediately with long enough intervals', async () => {
|
|
103
|
+
expect.assertions(3)
|
|
104
|
+
const func = vi.fn()
|
|
105
|
+
const debounced = debounceAsync(func, { delay: 1000, immediate: true })
|
|
106
|
+
const promises = []
|
|
107
|
+
promises.push(debounced())
|
|
108
|
+
expect(func).toBeCalledTimes(1)
|
|
109
|
+
for (let i = 0; i < 10; i++) {
|
|
110
|
+
vi.advanceTimersByTime(500)
|
|
118
111
|
promises.push(debounced())
|
|
119
|
-
await Promise.all(promises)
|
|
120
|
-
expect(func).toBeCalledTimes(2)
|
|
121
112
|
}
|
|
122
|
-
|
|
113
|
+
expect(func).toBeCalledTimes(1)
|
|
114
|
+
vi.advanceTimersByTime(1000)
|
|
115
|
+
promises.push(debounced())
|
|
116
|
+
await Promise.all(promises)
|
|
117
|
+
expect(func).toBeCalledTimes(2)
|
|
118
|
+
})
|
|
123
119
|
|
|
124
|
-
it(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (++count > 1) {
|
|
131
|
-
throw new Error('boom')
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
const debounced = debounceAsync(func, { delay: 1000, immediate: true })
|
|
135
|
-
const promises = []
|
|
136
|
-
promises.push(debounced())
|
|
137
|
-
expect(func).toBeCalledTimes(1)
|
|
138
|
-
for (let i = 0; i < 10; i++) {
|
|
139
|
-
vi.advanceTimersByTime(500)
|
|
140
|
-
promises.push(debounced())
|
|
120
|
+
it('should only reject the last waiting promise', async () => {
|
|
121
|
+
expect.assertions(5)
|
|
122
|
+
let count = 0
|
|
123
|
+
const func = vi.fn(() => {
|
|
124
|
+
if (++count > 1) {
|
|
125
|
+
throw new Error('boom')
|
|
141
126
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
127
|
+
})
|
|
128
|
+
const debounced = debounceAsync(func, { delay: 1000, immediate: true })
|
|
129
|
+
const promises = []
|
|
130
|
+
promises.push(debounced())
|
|
131
|
+
expect(func).toBeCalledTimes(1)
|
|
132
|
+
for (let i = 0; i < 10; i++) {
|
|
133
|
+
vi.advanceTimersByTime(500)
|
|
134
|
+
promises.push(debounced())
|
|
148
135
|
}
|
|
149
|
-
|
|
136
|
+
expect(func).toBeCalledTimes(1)
|
|
137
|
+
vi.advanceTimersByTime(1000)
|
|
138
|
+
await expect(debounced()).rejects.toThrow('boom')
|
|
139
|
+
const results = await Promise.all(promises)
|
|
140
|
+
expect(results).toStrictEqual(new Array(11).fill(undefined))
|
|
141
|
+
expect(func).toBeCalledTimes(2)
|
|
142
|
+
})
|
|
150
143
|
})
|
package/types/index.d.ts
CHANGED
|
@@ -116,23 +116,23 @@ export function clone<T>(value: T, options?: {
|
|
|
116
116
|
*
|
|
117
117
|
* @default false
|
|
118
118
|
*/
|
|
119
|
-
shallow?: boolean
|
|
119
|
+
shallow?: boolean
|
|
120
120
|
/**
|
|
121
121
|
* Whether to clone all object properties, or only those that are defined
|
|
122
122
|
* directly on that object, and are not inherited from the object's prototype.
|
|
123
123
|
*
|
|
124
124
|
* @default true
|
|
125
125
|
*/
|
|
126
|
-
enumerable?: boolean
|
|
126
|
+
enumerable?: boolean
|
|
127
127
|
/**
|
|
128
128
|
* @default `true` if `options.enumerable` is true, `false` otherwise
|
|
129
129
|
*/
|
|
130
|
-
descriptors?: boolean
|
|
131
|
-
transferables?: any[]
|
|
130
|
+
descriptors?: boolean
|
|
131
|
+
transferables?: any[]
|
|
132
132
|
/**
|
|
133
133
|
* Optional callback to process the cloned value.
|
|
134
134
|
*/
|
|
135
|
-
processValue?: <S>(value: T) => S
|
|
135
|
+
processValue?: <S>(value: T) => S
|
|
136
136
|
}): T
|
|
137
137
|
|
|
138
138
|
/**
|
|
@@ -220,14 +220,14 @@ export function mapConcurrently<T, R>(
|
|
|
220
220
|
input: Promise<T[]> | T[],
|
|
221
221
|
callback: (value: T, index: number, array: T[]) => Promise<R> | R,
|
|
222
222
|
options?: {
|
|
223
|
-
concurrency?: number
|
|
223
|
+
concurrency?: number
|
|
224
224
|
}
|
|
225
|
-
): Promise<R[]
|
|
225
|
+
): Promise<R[]>
|
|
226
226
|
|
|
227
227
|
export function mapSequentially<T, R>(
|
|
228
228
|
input: Promise<T[]> | T[],
|
|
229
229
|
callback: (value: T, index: number) => Promise<R> | R
|
|
230
|
-
): Promise<R[]
|
|
230
|
+
): Promise<R[]>
|
|
231
231
|
|
|
232
232
|
/* --------------------------------- string --------------------------------- */
|
|
233
233
|
|
|
@@ -458,8 +458,7 @@ export function debounceAsync<T extends (...args: any[]) => Promise<any>>(
|
|
|
458
458
|
// overloaded node function types), excluding the custom promisified function
|
|
459
459
|
// functionality, which toAsync does not support:
|
|
460
460
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/fa723dee727e433add7e700420523f0f90e87a01/types/node/util.d.ts#L86
|
|
461
|
-
interface CustomPromisifyLegacy<TCustom extends Function>
|
|
462
|
-
extends Function {
|
|
461
|
+
interface CustomPromisifyLegacy<TCustom extends Function> extends Function {
|
|
463
462
|
__promisify__: TCustom
|
|
464
463
|
}
|
|
465
464
|
type CustomToAsync<TCustom extends Function> = CustomPromisifyLegacy<TCustom>
|