@ocavue/utils 1.1.0 → 1.3.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/dist/index.d.ts +139 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -229
- package/dist/index.js.map +1 -1
- package/package.json +16 -13
- package/src/__snapshots__/e2e.test.ts.snap +247 -0
- package/src/default-map.test.ts +290 -1
- package/src/default-map.ts +76 -0
- package/src/e2e.test.ts +36 -0
- package/src/get-id.test.ts +20 -1
- package/src/get-id.ts +15 -1
- package/src/index.ts +3 -1
- package/src/map-group-by.ts +3 -1
- package/src/map-values.test.ts +41 -0
- package/src/map-values.ts +43 -0
- package/src/object-entries.test.ts +33 -0
- package/src/object-entries.ts +58 -0
- package/src/object-group-by.ts +3 -1
- package/src/once-stub-1.ts +13 -0
- package/src/once-stub-2.ts +3 -0
- package/src/once.test.ts +29 -0
package/src/default-map.ts
CHANGED
|
@@ -20,3 +20,79 @@ export class DefaultMap<K, V> extends Map<K, V> {
|
|
|
20
20
|
return value
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A weak map that automatically creates values for missing keys using a factory function.
|
|
26
|
+
*
|
|
27
|
+
* Similar to DefaultMap but uses WeakMap as the base, allowing garbage collection of keys.
|
|
28
|
+
*/
|
|
29
|
+
export class DefaultWeakMap<K extends WeakKey, V> extends WeakMap<K, V> {
|
|
30
|
+
private readonly defaultFactory: () => V
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
defaultFactory: () => V,
|
|
34
|
+
entries?: readonly (readonly [K, V])[] | null,
|
|
35
|
+
) {
|
|
36
|
+
super(entries)
|
|
37
|
+
this.defaultFactory = defaultFactory
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override get(key: K): V {
|
|
41
|
+
if (this.has(key)) {
|
|
42
|
+
return super.get(key)!
|
|
43
|
+
}
|
|
44
|
+
const value = this.defaultFactory()
|
|
45
|
+
this.set(key, value)
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A map that counts occurrences of keys.
|
|
52
|
+
*
|
|
53
|
+
* Similar to Python's [Counter](https://docs.python.org/3.13/library/collections.html#collections.Counter).
|
|
54
|
+
*/
|
|
55
|
+
export class Counter<K> extends DefaultMap<K, number> {
|
|
56
|
+
constructor(iterable?: Iterable<readonly [K, number]>) {
|
|
57
|
+
super(() => 0, iterable)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Increments the count for a key by a given amount (default 1).
|
|
62
|
+
*/
|
|
63
|
+
increment(key: K, amount = 1): void {
|
|
64
|
+
this.set(key, this.get(key) + amount)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decrements the count for a key by a given amount (default 1).
|
|
69
|
+
*/
|
|
70
|
+
decrement(key: K, amount = 1): void {
|
|
71
|
+
this.set(key, this.get(key) - amount)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A weak map that counts occurrences of object keys.
|
|
77
|
+
*
|
|
78
|
+
* Similar to Counter but uses WeakMap as the base, allowing garbage collection of keys.
|
|
79
|
+
*/
|
|
80
|
+
export class WeakCounter<K extends WeakKey> extends DefaultWeakMap<K, number> {
|
|
81
|
+
constructor(entries?: readonly (readonly [K, number])[] | null) {
|
|
82
|
+
super(() => 0, entries)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Increments the count for a key by a given amount (default 1).
|
|
87
|
+
*/
|
|
88
|
+
increment(key: K, amount = 1): void {
|
|
89
|
+
this.set(key, this.get(key) + amount)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Decrements the count for a key by a given amount (default 1).
|
|
94
|
+
*/
|
|
95
|
+
decrement(key: K, amount = 1): void {
|
|
96
|
+
this.set(key, this.get(key) - amount)
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/e2e.test.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { x } from 'tinyexec'
|
|
7
|
+
import { glob } from 'tinyglobby'
|
|
8
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
9
|
+
|
|
10
|
+
const ROOT_DIR = path.join(import.meta.dirname, '..')
|
|
11
|
+
const E2E_OUT_DIR = path.join(ROOT_DIR, 'e2e', 'dist')
|
|
12
|
+
|
|
13
|
+
describe('e2e', () => {
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
await x('pnpm', ['-w', 'build'], {
|
|
16
|
+
nodeOptions: { cwd: ROOT_DIR, stdio: 'inherit' },
|
|
17
|
+
throwOnError: true,
|
|
18
|
+
})
|
|
19
|
+
await x('pnpm', ['--filter', 'e2e', 'run', 'build'], {
|
|
20
|
+
nodeOptions: { cwd: ROOT_DIR, stdio: 'inherit' },
|
|
21
|
+
throwOnError: true,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('bundler outputs match snapshot', async () => {
|
|
26
|
+
const files = await glob('**/*', { cwd: E2E_OUT_DIR, onlyFiles: true })
|
|
27
|
+
const output = ['']
|
|
28
|
+
|
|
29
|
+
for (const file of files.sort()) {
|
|
30
|
+
const content = fs.readFileSync(path.join(E2E_OUT_DIR, file), 'utf-8')
|
|
31
|
+
output.push('#'.repeat(80), file, '-'.repeat(80), content, '')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
expect(output.join('\n')).toMatchSnapshot()
|
|
35
|
+
})
|
|
36
|
+
})
|
package/src/get-id.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { getId } from './get-id'
|
|
3
|
+
import { getId, setMaxSafeInteger } from './get-id'
|
|
4
4
|
|
|
5
5
|
describe('getId', () => {
|
|
6
6
|
it('returns a positive number', () => {
|
|
@@ -11,4 +11,23 @@ describe('getId', () => {
|
|
|
11
11
|
it('returns different values on consecutive calls', () => {
|
|
12
12
|
expect(getId()).not.toBe(getId())
|
|
13
13
|
})
|
|
14
|
+
|
|
15
|
+
it('never exceeds the configured maximum', () => {
|
|
16
|
+
const max = 5
|
|
17
|
+
setMaxSafeInteger(max)
|
|
18
|
+
let prevId = -1
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
for (let i = 0; i < max * 3; i += 1) {
|
|
22
|
+
const id = getId()
|
|
23
|
+
expect(id).toBeLessThan(max)
|
|
24
|
+
expect(id).toBeGreaterThanOrEqual(1)
|
|
25
|
+
expect(id).not.toBe(prevId)
|
|
26
|
+
|
|
27
|
+
prevId = id
|
|
28
|
+
}
|
|
29
|
+
} finally {
|
|
30
|
+
setMaxSafeInteger(Number.MAX_SAFE_INTEGER)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
14
33
|
})
|
package/src/get-id.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
let id = 0
|
|
2
2
|
|
|
3
|
+
let maxSafeInteger = Number.MAX_SAFE_INTEGER
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sets the maximum safe integer for the id generator. Only for testing purposes.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export function setMaxSafeInteger(max: number) {
|
|
11
|
+
maxSafeInteger = max
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* Generates a unique positive integer.
|
|
5
16
|
*/
|
|
6
17
|
export function getId(): number {
|
|
7
|
-
id
|
|
18
|
+
id++
|
|
19
|
+
if (id >= maxSafeInteger) {
|
|
20
|
+
id = 1
|
|
21
|
+
}
|
|
8
22
|
return id
|
|
9
23
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export * from './checker'
|
|
2
|
-
export { DefaultMap } from './default-map'
|
|
2
|
+
export { Counter, DefaultMap, DefaultWeakMap, WeakCounter } from './default-map'
|
|
3
3
|
export * from './dom'
|
|
4
4
|
export { formatBytes } from './format-bytes'
|
|
5
5
|
export { getId } from './get-id'
|
|
6
6
|
export { isDeepEqual } from './is-deep-equal'
|
|
7
7
|
export { mapGroupBy } from './map-group-by'
|
|
8
|
+
export { mapValues } from './map-values'
|
|
9
|
+
export { objectEntries, type ObjectEntries } from './object-entries'
|
|
8
10
|
export { objectGroupBy } from './object-group-by'
|
|
9
11
|
export { once } from './once'
|
|
10
12
|
export { sleep } from './sleep'
|
package/src/map-group-by.ts
CHANGED
|
@@ -30,6 +30,8 @@ export function mapGroupByNative<K, T>(
|
|
|
30
30
|
return Map.groupBy(items, keySelector)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const hasMapGroupBy: boolean = /* @__PURE__ */ (() => !!Map.groupBy)()
|
|
34
|
+
|
|
33
35
|
/**
|
|
34
36
|
* A polyfill for the `Map.groupBy` static method.
|
|
35
37
|
*
|
|
@@ -38,4 +40,4 @@ export function mapGroupByNative<K, T>(
|
|
|
38
40
|
export const mapGroupBy: <K, T>(
|
|
39
41
|
items: Iterable<T>,
|
|
40
42
|
keySelector: (item: T, index: number) => K,
|
|
41
|
-
) => Map<K, T[]> =
|
|
43
|
+
) => Map<K, T[]> = hasMapGroupBy ? mapGroupByNative : mapGroupByPolyfill
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { mapValues } from './map-values'
|
|
6
|
+
|
|
7
|
+
describe('mapValues', () => {
|
|
8
|
+
it('transforms values with callback function', () => {
|
|
9
|
+
const prices = { apple: 1, banana: 2, orange: 3 }
|
|
10
|
+
const doubled = mapValues(prices, (price) => price * 2)
|
|
11
|
+
|
|
12
|
+
expect(doubled).toEqual({ apple: 2, banana: 4, orange: 6 })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('provides key to callback function', () => {
|
|
16
|
+
const users = { john: 25, jane: 30 }
|
|
17
|
+
const greetings = mapValues(
|
|
18
|
+
users,
|
|
19
|
+
(age, name) => `${name} is ${age} years old`,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(greetings).toEqual({
|
|
23
|
+
john: 'john is 25 years old',
|
|
24
|
+
jane: 'jane is 30 years old',
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('handles empty objects', () => {
|
|
29
|
+
const empty = {}
|
|
30
|
+
const result = mapValues(empty, (value) => value)
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual({})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('transforms value types', () => {
|
|
36
|
+
const data = { a: '1', b: '2', c: '3' }
|
|
37
|
+
const numbers = mapValues(data, (str) => Number.parseInt(str, 10))
|
|
38
|
+
|
|
39
|
+
expect(numbers).toEqual({ a: 1, b: 2, c: 3 })
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a new object with the same keys as the input object, but with values
|
|
3
|
+
* transformed by the provided callback function. Similar to `Array.prototype.map()`
|
|
4
|
+
* but for object values.
|
|
5
|
+
|
|
6
|
+
* @param object - The object whose values will be transformed.
|
|
7
|
+
* @param callback - A function that transforms each value. Receives the value and
|
|
8
|
+
* its corresponding key as arguments.
|
|
9
|
+
* @returns A new object with the same keys but transformed values.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const prices = { apple: 1, banana: 2, orange: 3 }
|
|
14
|
+
* const doubled = mapValues(prices, (price) => price * 2)
|
|
15
|
+
* // Result: { apple: 2, banana: 4, orange: 6 }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const users = { john: 25, jane: 30, bob: 35 }
|
|
21
|
+
* const greetings = mapValues(users, (age, name) => `${name} is ${age} years old`)
|
|
22
|
+
* // Result: { john: 'john is 25 years old', jane: 'jane is 30 years old', bob: 'bob is 35 years old' }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const data = { a: '1', b: '2', c: '3' }
|
|
28
|
+
* const numbers = mapValues(data, (str) => parseInt(str, 10))
|
|
29
|
+
* // Result: { a: 1, b: 2, c: 3 }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
export function mapValues<ValueIn, ValueOut>(
|
|
35
|
+
object: Record<string, ValueIn>,
|
|
36
|
+
callback: (value: ValueIn, key: string) => ValueOut,
|
|
37
|
+
): Record<string, ValueOut> {
|
|
38
|
+
let result = {} as Record<string, ValueOut>
|
|
39
|
+
for (const [key, value] of Object.entries(object)) {
|
|
40
|
+
result[key] = callback(value, key)
|
|
41
|
+
}
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { objectEntries } from './object-entries'
|
|
6
|
+
|
|
7
|
+
describe('objectEntries', () => {
|
|
8
|
+
it('returns entries for an empty object', () => {
|
|
9
|
+
const obj = {}
|
|
10
|
+
const entries = objectEntries(obj)
|
|
11
|
+
expect(entries).toEqual([])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('returns entries for objects with mixed types', () => {
|
|
15
|
+
const obj = { name: 'Alice', age: 30, active: true }
|
|
16
|
+
const entries = objectEntries(obj)
|
|
17
|
+
expect(entries).toEqual([
|
|
18
|
+
['name', 'Alice'],
|
|
19
|
+
['age', 30],
|
|
20
|
+
['active', true],
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('preserves type safety with const objects', () => {
|
|
25
|
+
const obj = { a: 1, b: 'hello', c: true } as const
|
|
26
|
+
const entries = objectEntries(obj)
|
|
27
|
+
|
|
28
|
+
expect(entries).toHaveLength(3)
|
|
29
|
+
expect(entries).toContainEqual(['a', 1])
|
|
30
|
+
expect(entries).toContainEqual(['b', 'hello'])
|
|
31
|
+
expect(entries).toContainEqual(['c', true])
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TypeScript utility type that represents the entries of an object as a union of tuple types.
|
|
3
|
+
* Each tuple contains a key-value pair where the key and value types are precisely typed
|
|
4
|
+
* according to the input object type.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* type MyObject = { a: 1; b: 'B' }
|
|
9
|
+
* type MyEntries = ObjectEntries<MyObject>
|
|
10
|
+
* // ^ ["a", 1] | ["b", "B"]
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export type ObjectEntries<T extends Record<string, any>> = {
|
|
16
|
+
[K in keyof T]: [K, T[K]]
|
|
17
|
+
}[keyof T]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A type-safe wrapper around `Object.entries()` that preserves the exact types of object keys
|
|
21
|
+
* and values. Unlike the standard `Object.entries()` which returns `[string, any][]`, this
|
|
22
|
+
* function returns an array of tuples where each tuple is precisely typed according to the
|
|
23
|
+
* input object's structure.
|
|
24
|
+
*
|
|
25
|
+
* This is particularly useful when working with objects that have known, fixed property types
|
|
26
|
+
* and you want to maintain type safety when iterating over entries.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const myObject = { a: 1, b: 'hello', c: true } as const
|
|
31
|
+
* const entries = objectEntries(myObject)
|
|
32
|
+
* // Type: (["a", 1] | ["b", "hello"] | ["c", true])[]
|
|
33
|
+
*
|
|
34
|
+
* for (const [key, value] of entries) {
|
|
35
|
+
* // key is typed as "a" | "b" | "c"
|
|
36
|
+
* // value is typed as 1 | "hello" | true
|
|
37
|
+
* console.log(`${key}: ${value}`)
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* interface User {
|
|
44
|
+
* name: string
|
|
45
|
+
* age: number
|
|
46
|
+
* active: boolean
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* const user: User = { name: 'Alice', age: 30, active: true }
|
|
50
|
+
* const entries = objectEntries(user)
|
|
51
|
+
* // Type: (["name", string] | ["age", number] | ["active", boolean])[]
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export const objectEntries: <T extends Record<string, any>>(
|
|
57
|
+
obj: T,
|
|
58
|
+
) => ObjectEntries<T>[] = Object.entries
|
package/src/object-group-by.ts
CHANGED
|
@@ -30,6 +30,8 @@ export function objectGroupByNative<K extends PropertyKey, T>(
|
|
|
30
30
|
return Object.groupBy(items, keySelector)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const hasObjectGroupBy: boolean = /* @__PURE__ */ (() => !!Object.groupBy)()
|
|
34
|
+
|
|
33
35
|
/**
|
|
34
36
|
* A polyfill for the `Object.groupBy` static method.
|
|
35
37
|
*
|
|
@@ -38,6 +40,6 @@ export function objectGroupByNative<K extends PropertyKey, T>(
|
|
|
38
40
|
export const objectGroupBy: <K extends PropertyKey, T>(
|
|
39
41
|
items: Iterable<T>,
|
|
40
42
|
keySelector: (item: T, index: number) => K,
|
|
41
|
-
) => Partial<Record<K, T[]>> =
|
|
43
|
+
) => Partial<Record<K, T[]>> = hasObjectGroupBy
|
|
42
44
|
? objectGroupByNative
|
|
43
45
|
: objectGroupByPolyfill
|
package/src/once.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
3
5
|
import { describe, it, expect, vi } from 'vitest'
|
|
4
6
|
|
|
5
7
|
import { once } from './once'
|
|
@@ -23,4 +25,31 @@ describe('once', () => {
|
|
|
23
25
|
const getValue = once(() => value)
|
|
24
26
|
expect(getValue()).toBe(value)
|
|
25
27
|
})
|
|
28
|
+
|
|
29
|
+
it('can tree-shake', async () => {
|
|
30
|
+
const esbuild = await import('esbuild')
|
|
31
|
+
const cwd = import.meta.dirname
|
|
32
|
+
const input = path.join(cwd, 'once-stub-2.ts')
|
|
33
|
+
|
|
34
|
+
const result = await esbuild.build({
|
|
35
|
+
entryPoints: [input],
|
|
36
|
+
format: 'esm',
|
|
37
|
+
platform: 'neutral',
|
|
38
|
+
minify: true,
|
|
39
|
+
bundle: true,
|
|
40
|
+
write: false,
|
|
41
|
+
absWorkingDir: cwd,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const files = result.outputFiles.map((file) => {
|
|
45
|
+
return file.text
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(files).toMatchInlineSnapshot(`
|
|
49
|
+
[
|
|
50
|
+
"function t(n){let o=!1,e;return()=>(o||(e=n(),o=!0,n=void 0),e)}function r(){console.log("fn1")}var u=t(r);function f(){console.log("fn2")}f();
|
|
51
|
+
",
|
|
52
|
+
]
|
|
53
|
+
`)
|
|
54
|
+
})
|
|
26
55
|
})
|