@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.
@@ -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
+ }
@@ -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
+ })
@@ -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 = (id % Number.MAX_SAFE_INTEGER) + 1
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'
@@ -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[]> = !!Map.groupBy ? mapGroupByNative : mapGroupByPolyfill
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
@@ -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[]>> = !!Object.groupBy
43
+ ) => Partial<Record<K, T[]>> = hasObjectGroupBy
42
44
  ? objectGroupByNative
43
45
  : objectGroupByPolyfill
@@ -0,0 +1,13 @@
1
+ import { once } from './once'
2
+
3
+ function fn1() {
4
+ console.log('fn1')
5
+ }
6
+
7
+ const once1 = once(fn1)
8
+
9
+ function fn2() {
10
+ console.log('fn2')
11
+ }
12
+
13
+ export { once1, fn2 }
@@ -0,0 +1,3 @@
1
+ import { fn2 } from './once-stub-1'
2
+
3
+ fn2()
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
  })