@tldraw/utils 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f
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-cjs/index.d.ts +1350 -80
- package/dist-cjs/index.js +5 -5
- package/dist-cjs/lib/ExecutionQueue.js +79 -0
- package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
- package/dist-cjs/lib/PerformanceTracker.js +43 -0
- package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
- package/dist-cjs/lib/array.js +3 -1
- package/dist-cjs/lib/array.js.map +2 -2
- package/dist-cjs/lib/bind.js.map +2 -2
- package/dist-cjs/lib/cache.js +27 -5
- package/dist-cjs/lib/cache.js.map +2 -2
- package/dist-cjs/lib/control.js +12 -0
- package/dist-cjs/lib/control.js.map +2 -2
- package/dist-cjs/lib/debounce.js.map +2 -2
- package/dist-cjs/lib/error.js.map +2 -2
- package/dist-cjs/lib/file.js +76 -11
- package/dist-cjs/lib/file.js.map +2 -2
- package/dist-cjs/lib/function.js.map +2 -2
- package/dist-cjs/lib/hash.js.map +2 -2
- package/dist-cjs/lib/id.js.map +2 -2
- package/dist-cjs/lib/iterable.js.map +2 -2
- package/dist-cjs/lib/json-value.js.map +1 -1
- package/dist-cjs/lib/media/apng.js.map +2 -2
- package/dist-cjs/lib/media/avif.js.map +2 -2
- package/dist-cjs/lib/media/gif.js.map +2 -2
- package/dist-cjs/lib/media/media.js +130 -4
- package/dist-cjs/lib/media/media.js.map +2 -2
- package/dist-cjs/lib/media/png.js +141 -0
- package/dist-cjs/lib/media/png.js.map +2 -2
- package/dist-cjs/lib/media/webp.js +1 -0
- package/dist-cjs/lib/media/webp.js.map +2 -2
- package/dist-cjs/lib/network.js.map +2 -2
- package/dist-cjs/lib/number.js.map +2 -2
- package/dist-cjs/lib/object.js +1 -1
- package/dist-cjs/lib/object.js.map +2 -2
- package/dist-cjs/lib/perf.js.map +2 -2
- package/dist-cjs/lib/reordering.js.map +2 -2
- package/dist-cjs/lib/retry.js.map +2 -2
- package/dist-cjs/lib/sort.js.map +2 -2
- package/dist-cjs/lib/storage.js.map +2 -2
- package/dist-cjs/lib/stringEnum.js.map +2 -2
- package/dist-cjs/lib/throttle.js.map +2 -2
- package/dist-cjs/lib/timers.js +103 -4
- package/dist-cjs/lib/timers.js.map +2 -2
- package/dist-cjs/lib/types.js.map +1 -1
- package/dist-cjs/lib/url.js.map +2 -2
- package/dist-cjs/lib/value.js.map +2 -2
- package/dist-cjs/lib/version.js.map +2 -2
- package/dist-cjs/lib/warn.js.map +2 -2
- package/dist-esm/index.d.mts +1350 -80
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ExecutionQueue.mjs +79 -0
- package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
- package/dist-esm/lib/PerformanceTracker.mjs +43 -0
- package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
- package/dist-esm/lib/array.mjs +3 -1
- package/dist-esm/lib/array.mjs.map +2 -2
- package/dist-esm/lib/bind.mjs.map +2 -2
- package/dist-esm/lib/cache.mjs +27 -5
- package/dist-esm/lib/cache.mjs.map +2 -2
- package/dist-esm/lib/control.mjs +12 -0
- package/dist-esm/lib/control.mjs.map +2 -2
- package/dist-esm/lib/debounce.mjs.map +2 -2
- package/dist-esm/lib/error.mjs.map +2 -2
- package/dist-esm/lib/file.mjs +76 -11
- package/dist-esm/lib/file.mjs.map +2 -2
- package/dist-esm/lib/function.mjs.map +2 -2
- package/dist-esm/lib/hash.mjs.map +2 -2
- package/dist-esm/lib/id.mjs.map +2 -2
- package/dist-esm/lib/iterable.mjs.map +2 -2
- package/dist-esm/lib/media/apng.mjs.map +2 -2
- package/dist-esm/lib/media/avif.mjs.map +2 -2
- package/dist-esm/lib/media/gif.mjs.map +2 -2
- package/dist-esm/lib/media/media.mjs +130 -4
- package/dist-esm/lib/media/media.mjs.map +2 -2
- package/dist-esm/lib/media/png.mjs +141 -0
- package/dist-esm/lib/media/png.mjs.map +2 -2
- package/dist-esm/lib/media/webp.mjs +1 -0
- package/dist-esm/lib/media/webp.mjs.map +2 -2
- package/dist-esm/lib/network.mjs.map +2 -2
- package/dist-esm/lib/number.mjs.map +2 -2
- package/dist-esm/lib/object.mjs.map +2 -2
- package/dist-esm/lib/perf.mjs.map +2 -2
- package/dist-esm/lib/reordering.mjs.map +2 -2
- package/dist-esm/lib/retry.mjs.map +2 -2
- package/dist-esm/lib/sort.mjs.map +2 -2
- package/dist-esm/lib/storage.mjs.map +2 -2
- package/dist-esm/lib/stringEnum.mjs.map +2 -2
- package/dist-esm/lib/throttle.mjs.map +2 -2
- package/dist-esm/lib/timers.mjs +103 -4
- package/dist-esm/lib/timers.mjs.map +2 -2
- package/dist-esm/lib/url.mjs.map +2 -2
- package/dist-esm/lib/value.mjs.map +2 -2
- package/dist-esm/lib/version.mjs.map +2 -2
- package/dist-esm/lib/warn.mjs.map +2 -2
- package/package.json +1 -1
- package/src/lib/ExecutionQueue.test.ts +162 -20
- package/src/lib/ExecutionQueue.ts +110 -1
- package/src/lib/PerformanceTracker.test.ts +124 -0
- package/src/lib/PerformanceTracker.ts +63 -1
- package/src/lib/array.test.ts +263 -1
- package/src/lib/array.ts +183 -14
- package/src/lib/bind.test.ts +47 -0
- package/src/lib/bind.ts +69 -4
- package/src/lib/cache.test.ts +73 -0
- package/src/lib/cache.ts +47 -6
- package/src/lib/control.test.ts +50 -0
- package/src/lib/control.ts +198 -9
- package/src/lib/debounce.ts +28 -3
- package/src/lib/error.test.ts +60 -0
- package/src/lib/error.ts +27 -1
- package/src/lib/file.test.ts +49 -0
- package/src/lib/file.ts +117 -12
- package/src/lib/function.ts +11 -0
- package/src/lib/hash.test.ts +99 -0
- package/src/lib/hash.ts +69 -2
- package/src/lib/id.test.ts +32 -0
- package/src/lib/id.ts +53 -5
- package/src/lib/iterable.test.ts +25 -0
- package/src/lib/iterable.ts +4 -5
- package/src/lib/json-value.ts +71 -4
- package/src/lib/media/apng.test.ts +67 -0
- package/src/lib/media/apng.ts +38 -21
- package/src/lib/media/avif.test.ts +26 -0
- package/src/lib/media/avif.ts +34 -0
- package/src/lib/media/gif.test.ts +52 -0
- package/src/lib/media/gif.ts +25 -2
- package/src/lib/media/media.test.ts +58 -0
- package/src/lib/media/media.ts +220 -11
- package/src/lib/media/png.ts +162 -1
- package/src/lib/media/webp.test.ts +81 -0
- package/src/lib/media/webp.ts +33 -1
- package/src/lib/network.test.ts +38 -0
- package/src/lib/network.ts +6 -0
- package/src/lib/number.test.ts +74 -0
- package/src/lib/number.ts +29 -5
- package/src/lib/object.test.ts +236 -0
- package/src/lib/object.ts +194 -14
- package/src/lib/perf.ts +75 -3
- package/src/lib/reordering.test.ts +168 -0
- package/src/lib/reordering.ts +62 -4
- package/src/lib/retry.test.ts +77 -0
- package/src/lib/retry.ts +47 -1
- package/src/lib/sort.test.ts +36 -0
- package/src/lib/sort.ts +22 -1
- package/src/lib/storage.test.ts +130 -0
- package/src/lib/storage.tsx +54 -8
- package/src/lib/stringEnum.ts +20 -1
- package/src/lib/throttle.ts +46 -8
- package/src/lib/timers.test.ts +75 -0
- package/src/lib/timers.ts +124 -5
- package/src/lib/types.ts +126 -4
- package/src/lib/url.test.ts +44 -0
- package/src/lib/url.ts +40 -1
- package/src/lib/value.test.ts +102 -0
- package/src/lib/value.ts +67 -3
- package/src/lib/version.test.ts +494 -56
- package/src/lib/version.ts +36 -1
- package/src/lib/warn.test.ts +64 -0
- package/src/lib/warn.ts +43 -2
package/src/lib/timers.ts
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
/* eslint-disable no-restricted-properties */
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A utility class for managing timeouts, intervals, and animation frames with context-based organization and automatic cleanup.
|
|
5
|
+
* Helps prevent memory leaks by organizing timers into named contexts that can be cleared together.
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const timers = new Timers()
|
|
9
|
+
*
|
|
10
|
+
* // Set timers with context organization
|
|
11
|
+
* timers.setTimeout('ui', () => console.log('Auto save'), 5000)
|
|
12
|
+
* timers.setInterval('ui', () => console.log('Refresh'), 1000)
|
|
13
|
+
* timers.requestAnimationFrame('ui', () => console.log('Render'))
|
|
14
|
+
*
|
|
15
|
+
* // Clear all timers for a context
|
|
16
|
+
* timers.dispose('ui')
|
|
17
|
+
*
|
|
18
|
+
* // Or get context-bound functions
|
|
19
|
+
* const uiTimers = timers.forContext('ui')
|
|
20
|
+
* uiTimers.setTimeout(() => console.log('Contextual timeout'), 1000)
|
|
21
|
+
* ```
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
4
24
|
export class Timers {
|
|
5
25
|
private timeouts = new Map<string, number[]>()
|
|
6
26
|
private intervals = new Map<string, number[]>()
|
|
7
27
|
private rafs = new Map<string, number[]>()
|
|
8
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new Timers instance with bound methods for safe callback usage.
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const timers = new Timers()
|
|
34
|
+
* // Methods are pre-bound, safe to use as callbacks
|
|
35
|
+
* element.addEventListener('click', timers.dispose)
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
9
38
|
constructor() {
|
|
10
39
|
this.setTimeout = this.setTimeout.bind(this)
|
|
11
40
|
this.setInterval = this.setInterval.bind(this)
|
|
@@ -13,7 +42,21 @@ export class Timers {
|
|
|
13
42
|
this.dispose = this.dispose.bind(this)
|
|
14
43
|
}
|
|
15
44
|
|
|
16
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Creates a timeout that will be tracked under the specified context.
|
|
47
|
+
* @param contextId - The context identifier to group this timer under.
|
|
48
|
+
* @param handler - The function to execute when the timeout expires.
|
|
49
|
+
* @param timeout - The delay in milliseconds (default: 0).
|
|
50
|
+
* @param args - Additional arguments to pass to the handler.
|
|
51
|
+
* @returns The timer ID that can be used with clearTimeout.
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const timers = new Timers()
|
|
55
|
+
* const id = timers.setTimeout('autosave', () => save(), 5000)
|
|
56
|
+
* // Timer will be automatically cleared when 'autosave' context is disposed
|
|
57
|
+
* ```
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
17
60
|
setTimeout(contextId: string, handler: TimerHandler, timeout?: number, ...args: any[]): number {
|
|
18
61
|
const id = window.setTimeout(handler, timeout, args)
|
|
19
62
|
const current = this.timeouts.get(contextId) ?? []
|
|
@@ -21,7 +64,21 @@ export class Timers {
|
|
|
21
64
|
return id
|
|
22
65
|
}
|
|
23
66
|
|
|
24
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Creates an interval that will be tracked under the specified context.
|
|
69
|
+
* @param contextId - The context identifier to group this timer under.
|
|
70
|
+
* @param handler - The function to execute repeatedly.
|
|
71
|
+
* @param timeout - The delay in milliseconds between executions (default: 0).
|
|
72
|
+
* @param args - Additional arguments to pass to the handler.
|
|
73
|
+
* @returns The interval ID that can be used with clearInterval.
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const timers = new Timers()
|
|
77
|
+
* const id = timers.setInterval('refresh', () => updateData(), 1000)
|
|
78
|
+
* // Interval will be automatically cleared when 'refresh' context is disposed
|
|
79
|
+
* ```
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
25
82
|
setInterval(contextId: string, handler: TimerHandler, timeout?: number, ...args: any[]): number {
|
|
26
83
|
const id = window.setInterval(handler, timeout, args)
|
|
27
84
|
const current = this.intervals.get(contextId) ?? []
|
|
@@ -29,7 +86,19 @@ export class Timers {
|
|
|
29
86
|
return id
|
|
30
87
|
}
|
|
31
88
|
|
|
32
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Requests an animation frame that will be tracked under the specified context.
|
|
91
|
+
* @param contextId - The context identifier to group this animation frame under.
|
|
92
|
+
* @param callback - The function to execute on the next animation frame.
|
|
93
|
+
* @returns The request ID that can be used with cancelAnimationFrame.
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const timers = new Timers()
|
|
97
|
+
* const id = timers.requestAnimationFrame('render', () => draw())
|
|
98
|
+
* // Animation frame will be automatically cancelled when 'render' context is disposed
|
|
99
|
+
* ```
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
33
102
|
requestAnimationFrame(contextId: string, callback: FrameRequestCallback): number {
|
|
34
103
|
const id = window.requestAnimationFrame(callback)
|
|
35
104
|
const current = this.rafs.get(contextId) ?? []
|
|
@@ -37,7 +106,22 @@ export class Timers {
|
|
|
37
106
|
return id
|
|
38
107
|
}
|
|
39
108
|
|
|
40
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Disposes of all timers associated with the specified context.
|
|
111
|
+
* Clears all timeouts, intervals, and animation frames for the given context ID.
|
|
112
|
+
* @param contextId - The context identifier whose timers should be cleared.
|
|
113
|
+
* @returns void
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* const timers = new Timers()
|
|
117
|
+
* timers.setTimeout('ui', () => console.log('timeout'), 1000)
|
|
118
|
+
* timers.setInterval('ui', () => console.log('interval'), 500)
|
|
119
|
+
*
|
|
120
|
+
* // Clear all 'ui' context timers
|
|
121
|
+
* timers.dispose('ui')
|
|
122
|
+
* ```
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
41
125
|
dispose(contextId: string) {
|
|
42
126
|
this.timeouts.get(contextId)?.forEach((id) => clearTimeout(id))
|
|
43
127
|
this.intervals.get(contextId)?.forEach((id) => clearInterval(id))
|
|
@@ -48,12 +132,47 @@ export class Timers {
|
|
|
48
132
|
this.rafs.delete(contextId)
|
|
49
133
|
}
|
|
50
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Disposes of all timers across all contexts.
|
|
137
|
+
* Clears every timeout, interval, and animation frame managed by this instance.
|
|
138
|
+
* @returns void
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* const timers = new Timers()
|
|
142
|
+
* timers.setTimeout('ui', () => console.log('ui'), 1000)
|
|
143
|
+
* timers.setTimeout('background', () => console.log('bg'), 2000)
|
|
144
|
+
*
|
|
145
|
+
* // Clear everything
|
|
146
|
+
* timers.disposeAll()
|
|
147
|
+
* ```
|
|
148
|
+
* @public
|
|
149
|
+
*/
|
|
51
150
|
disposeAll() {
|
|
52
151
|
for (const contextId of this.timeouts.keys()) {
|
|
53
152
|
this.dispose(contextId)
|
|
54
153
|
}
|
|
55
154
|
}
|
|
56
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Returns an object with timer methods bound to a specific context.
|
|
158
|
+
* Convenient for getting context-specific timer functions without repeatedly passing the contextId.
|
|
159
|
+
* @param contextId - The context identifier to bind the returned methods to.
|
|
160
|
+
* @returns An object with setTimeout, setInterval, requestAnimationFrame, and dispose methods bound to the context.
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const timers = new Timers()
|
|
164
|
+
* const uiTimers = timers.forContext('ui')
|
|
165
|
+
*
|
|
166
|
+
* // These are equivalent to calling timers.setTimeout('ui', ...)
|
|
167
|
+
* uiTimers.setTimeout(() => console.log('timeout'), 1000)
|
|
168
|
+
* uiTimers.setInterval(() => console.log('interval'), 500)
|
|
169
|
+
* uiTimers.requestAnimationFrame(() => console.log('frame'))
|
|
170
|
+
*
|
|
171
|
+
* // Dispose only this context
|
|
172
|
+
* uiTimers.dispose()
|
|
173
|
+
* ```
|
|
174
|
+
* @public
|
|
175
|
+
*/
|
|
57
176
|
forContext(contextId: string) {
|
|
58
177
|
return {
|
|
59
178
|
setTimeout: (handler: TimerHandler, timeout?: number, ...args: any[]) =>
|
package/src/lib/types.ts
CHANGED
|
@@ -1,15 +1,137 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Makes all properties in a type and all nested properties optional recursively.
|
|
3
|
+
* This is useful for creating partial update objects where you only want to specify
|
|
4
|
+
* some deeply nested properties while leaving others unchanged.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* interface User {
|
|
9
|
+
* name: string
|
|
10
|
+
* settings: {
|
|
11
|
+
* theme: string
|
|
12
|
+
* notifications: {
|
|
13
|
+
* email: boolean
|
|
14
|
+
* push: boolean
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* type PartialUser = RecursivePartial<User>
|
|
20
|
+
* // Result: {
|
|
21
|
+
* // name?: string
|
|
22
|
+
* // settings?: {
|
|
23
|
+
* // theme?: string
|
|
24
|
+
* // notifications?: {
|
|
25
|
+
* // email?: boolean
|
|
26
|
+
* // push?: boolean
|
|
27
|
+
* // }
|
|
28
|
+
* // }
|
|
29
|
+
* // }
|
|
30
|
+
*
|
|
31
|
+
* const update: PartialUser = {
|
|
32
|
+
* settings: {
|
|
33
|
+
* notifications: {
|
|
34
|
+
* email: false
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
2
42
|
export type RecursivePartial<T> = {
|
|
3
43
|
[P in keyof T]?: RecursivePartial<T[P]>
|
|
4
44
|
}
|
|
5
45
|
|
|
6
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Expands a type definition to show its full structure in IDE tooltips and error messages.
|
|
48
|
+
* This utility type forces TypeScript to resolve and display the complete type structure
|
|
49
|
+
* instead of showing complex conditional types or intersections as-is.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* type User = { name: string }
|
|
54
|
+
* type WithId = { id: string }
|
|
55
|
+
* type UserWithId = User & WithId
|
|
56
|
+
*
|
|
57
|
+
* // Without Expand, IDE shows: User & WithId
|
|
58
|
+
* // With Expand, IDE shows: { name: string; id: string }
|
|
59
|
+
* type ExpandedUserWithId = Expand<UserWithId>
|
|
60
|
+
*
|
|
61
|
+
* // Useful for complex intersections
|
|
62
|
+
* type ComplexType = Expand<BaseType & Mixin1 & Mixin2>
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
7
67
|
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
|
|
8
68
|
|
|
9
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Makes specified keys in a type required while keeping all other properties as-is.
|
|
71
|
+
* This is useful when you need to ensure certain optional properties are provided
|
|
72
|
+
* in specific contexts without affecting the entire type structure.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* interface Shape {
|
|
77
|
+
* id: string
|
|
78
|
+
* x?: number
|
|
79
|
+
* y?: number
|
|
80
|
+
* visible?: boolean
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* // Make position properties required
|
|
84
|
+
* type PositionedShape = Required<Shape, 'x' | 'y'>
|
|
85
|
+
* // Result: {
|
|
86
|
+
* // id: string
|
|
87
|
+
* // x: number // now required
|
|
88
|
+
* // y: number // now required
|
|
89
|
+
* // visible?: boolean
|
|
90
|
+
* // }
|
|
91
|
+
*
|
|
92
|
+
* const shape: PositionedShape = {
|
|
93
|
+
* id: 'rect1',
|
|
94
|
+
* x: 10, // must provide
|
|
95
|
+
* y: 20, // must provide
|
|
96
|
+
* // visible is still optional
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
10
102
|
export type Required<T, K extends keyof T> = Expand<Omit<T, K> & { [P in K]-?: T[P] }>
|
|
11
103
|
|
|
12
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Automatically makes properties optional if their type includes `undefined`.
|
|
106
|
+
* This transforms properties like `prop: string | undefined` to `prop?: string | undefined`,
|
|
107
|
+
* making the API more ergonomic by not requiring explicit undefined assignments.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* interface RawConfig {
|
|
112
|
+
* name: string
|
|
113
|
+
* theme: string | undefined
|
|
114
|
+
* debug: boolean | undefined
|
|
115
|
+
* version: number
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* type Config = MakeUndefinedOptional<RawConfig>
|
|
119
|
+
* // Result: {
|
|
120
|
+
* // name: string
|
|
121
|
+
* // theme?: string | undefined // now optional
|
|
122
|
+
* // debug?: boolean | undefined // now optional
|
|
123
|
+
* // version: number
|
|
124
|
+
* // }
|
|
125
|
+
*
|
|
126
|
+
* const config: Config = {
|
|
127
|
+
* name: 'MyApp',
|
|
128
|
+
* version: 1
|
|
129
|
+
* // theme and debug can be omitted instead of explicitly set to undefined
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* @public
|
|
134
|
+
*/
|
|
13
135
|
export type MakeUndefinedOptional<T extends object> = Expand<
|
|
14
136
|
{
|
|
15
137
|
[P in { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]]: T[P]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { safeParseUrl } from './url'
|
|
3
|
+
|
|
4
|
+
describe('safeParseUrl', () => {
|
|
5
|
+
it('returns URL object for valid absolute URLs', () => {
|
|
6
|
+
const result = safeParseUrl('https://example.com/path')
|
|
7
|
+
|
|
8
|
+
expect(result).toBeInstanceOf(URL)
|
|
9
|
+
expect(result?.href).toBe('https://example.com/path')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('resolves relative URLs with base URL', () => {
|
|
13
|
+
const result = safeParseUrl('/path', 'https://example.com')
|
|
14
|
+
|
|
15
|
+
expect(result).toBeInstanceOf(URL)
|
|
16
|
+
expect(result?.href).toBe('https://example.com/path')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('accepts URL object as base URL', () => {
|
|
20
|
+
const baseUrl = new URL('https://example.com')
|
|
21
|
+
const result = safeParseUrl('/path', baseUrl)
|
|
22
|
+
|
|
23
|
+
expect(result).toBeInstanceOf(URL)
|
|
24
|
+
expect(result?.href).toBe('https://example.com/path')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns undefined for invalid URLs', () => {
|
|
28
|
+
expect(safeParseUrl('')).toBeUndefined()
|
|
29
|
+
expect(safeParseUrl('not-a-url')).toBeUndefined()
|
|
30
|
+
expect(safeParseUrl('https://exam ple.com')).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns undefined when relative URL has no base', () => {
|
|
34
|
+
const result = safeParseUrl('/relative/path')
|
|
35
|
+
|
|
36
|
+
expect(result).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns undefined when base URL is invalid', () => {
|
|
40
|
+
const result = safeParseUrl('/path', 'not-a-base-url')
|
|
41
|
+
|
|
42
|
+
expect(result).toBeUndefined()
|
|
43
|
+
})
|
|
44
|
+
})
|
package/src/lib/url.ts
CHANGED
|
@@ -1,4 +1,43 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Safely parses a URL string without throwing exceptions on invalid input.
|
|
3
|
+
* Returns a URL object for valid URLs or undefined for invalid ones.
|
|
4
|
+
*
|
|
5
|
+
* @param url - The URL string to parse
|
|
6
|
+
* @param baseUrl - Optional base URL to resolve relative URLs against
|
|
7
|
+
* @returns A URL object if parsing succeeds, undefined if it fails
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // Valid absolute URL
|
|
12
|
+
* const url1 = safeParseUrl('https://example.com')
|
|
13
|
+
* if (url1) {
|
|
14
|
+
* console.log(`Valid URL: ${url1.href}`) // "Valid URL: https://example.com/"
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* // Invalid URL
|
|
18
|
+
* const url2 = safeParseUrl('not-a-url')
|
|
19
|
+
* console.log(url2) // undefined
|
|
20
|
+
*
|
|
21
|
+
* // Relative URL with base
|
|
22
|
+
* const url3 = safeParseUrl('/path', 'https://example.com')
|
|
23
|
+
* if (url3) {
|
|
24
|
+
* console.log(url3.href) // "https://example.com/path"
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // Error handling
|
|
28
|
+
* function handleUserUrl(input: string) {
|
|
29
|
+
* const url = safeParseUrl(input)
|
|
30
|
+
* if (url) {
|
|
31
|
+
* return url
|
|
32
|
+
* } else {
|
|
33
|
+
* console.log('Invalid URL provided')
|
|
34
|
+
* return null
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
2
41
|
export const safeParseUrl = (url: string, baseUrl?: string | URL) => {
|
|
3
42
|
try {
|
|
4
43
|
return new URL(url, baseUrl)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
isNativeStructuredClone,
|
|
4
|
+
STRUCTURED_CLONE_OBJECT_PROTOTYPE,
|
|
5
|
+
structuredClone,
|
|
6
|
+
} from './value'
|
|
7
|
+
|
|
8
|
+
describe('value utilities', () => {
|
|
9
|
+
describe('structuredClone', () => {
|
|
10
|
+
it('should create deep copies of objects', () => {
|
|
11
|
+
const original = { a: 1, b: { c: 2 } }
|
|
12
|
+
const copy = structuredClone(original)
|
|
13
|
+
|
|
14
|
+
expect(copy).toEqual(original)
|
|
15
|
+
expect(copy).not.toBe(original)
|
|
16
|
+
expect(copy.b).not.toBe(original.b)
|
|
17
|
+
|
|
18
|
+
// Modifying copy should not affect original
|
|
19
|
+
copy.b.c = 3
|
|
20
|
+
expect(original.b.c).toBe(2)
|
|
21
|
+
expect(copy.b.c).toBe(3)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should handle primitive values', () => {
|
|
25
|
+
expect(structuredClone(42)).toBe(42)
|
|
26
|
+
expect(structuredClone('hello')).toBe('hello')
|
|
27
|
+
expect(structuredClone(true)).toBe(true)
|
|
28
|
+
expect(structuredClone(null)).toBe(null)
|
|
29
|
+
expect(structuredClone(undefined)).toBe(undefined)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should handle arrays', () => {
|
|
33
|
+
const original = [1, [2, 3], { a: 4 }]
|
|
34
|
+
const copy = structuredClone(original)
|
|
35
|
+
|
|
36
|
+
expect(copy).toEqual(original)
|
|
37
|
+
expect(copy).not.toBe(original)
|
|
38
|
+
expect(copy[1]).not.toBe(original[1])
|
|
39
|
+
expect(copy[2]).not.toBe(original[2])
|
|
40
|
+
|
|
41
|
+
// Modifying copy should not affect original
|
|
42
|
+
;(copy[1] as number[]).push(5)
|
|
43
|
+
;(copy[2] as any).a = 99
|
|
44
|
+
|
|
45
|
+
expect(original[1]).toEqual([2, 3])
|
|
46
|
+
expect((original[2] as any).a).toBe(4)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should handle nested structures', () => {
|
|
50
|
+
const original = {
|
|
51
|
+
level1: {
|
|
52
|
+
level2: {
|
|
53
|
+
level3: {
|
|
54
|
+
value: 'deep',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const copy = structuredClone(original)
|
|
61
|
+
|
|
62
|
+
expect(copy).toEqual(original)
|
|
63
|
+
expect(copy.level1).not.toBe(original.level1)
|
|
64
|
+
expect(copy.level1.level2).not.toBe(original.level1.level2)
|
|
65
|
+
expect(copy.level1.level2.level3).not.toBe(original.level1.level2.level3)
|
|
66
|
+
|
|
67
|
+
copy.level1.level2.level3.value = 'modified'
|
|
68
|
+
expect(original.level1.level2.level3.value).toBe('deep')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should handle dates', () => {
|
|
72
|
+
const date = new Date('2023-01-01')
|
|
73
|
+
const copy = structuredClone(date)
|
|
74
|
+
|
|
75
|
+
expect(copy).toEqual(date)
|
|
76
|
+
expect(copy).not.toBe(date)
|
|
77
|
+
expect(copy instanceof Date).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should handle circular references if native structuredClone is available', () => {
|
|
81
|
+
if (isNativeStructuredClone) {
|
|
82
|
+
const obj: any = { a: 1 }
|
|
83
|
+
obj.self = obj
|
|
84
|
+
|
|
85
|
+
const copy = structuredClone(obj)
|
|
86
|
+
|
|
87
|
+
expect(copy.a).toBe(1)
|
|
88
|
+
expect(copy.self).toBe(copy)
|
|
89
|
+
expect(copy).not.toBe(obj)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('STRUCTURED_CLONE_OBJECT_PROTOTYPE', () => {
|
|
95
|
+
it('should be the prototype of objects created by structuredClone', () => {
|
|
96
|
+
const obj = {}
|
|
97
|
+
const cloned = structuredClone(obj)
|
|
98
|
+
|
|
99
|
+
expect(Object.getPrototypeOf(cloned)).toBe(STRUCTURED_CLONE_OBJECT_PROTOTYPE)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
package/src/lib/value.ts
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
* Get whether a value is not undefined.
|
|
3
3
|
*
|
|
4
4
|
* @param value - The value to check.
|
|
5
|
+
* @returns True if the value is not undefined, with proper type narrowing.
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const maybeString: string | undefined = getValue()
|
|
9
|
+
*
|
|
10
|
+
* if (isDefined(maybeString)) {
|
|
11
|
+
* // TypeScript knows maybeString is string, not undefined
|
|
12
|
+
* console.log(maybeString.toUpperCase())
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* // Filter undefined values from arrays
|
|
16
|
+
* const values = [1, undefined, 2, undefined, 3]
|
|
17
|
+
* const definedValues = values.filter(isDefined) // [1, 2, 3]
|
|
18
|
+
* ```
|
|
5
19
|
* @public
|
|
6
20
|
*/
|
|
7
21
|
export function isDefined<T>(value: T): value is typeof value extends undefined ? never : T {
|
|
@@ -9,9 +23,23 @@ export function isDefined<T>(value: T): value is typeof value extends undefined
|
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
/**
|
|
12
|
-
* Get whether a value is null
|
|
26
|
+
* Get whether a value is not null.
|
|
13
27
|
*
|
|
14
28
|
* @param value - The value to check.
|
|
29
|
+
* @returns True if the value is not null, with proper type narrowing.
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const maybeString: string | null = getValue()
|
|
33
|
+
*
|
|
34
|
+
* if (isNonNull(maybeString)) {
|
|
35
|
+
* // TypeScript knows maybeString is string, not null
|
|
36
|
+
* console.log(maybeString.length)
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // Filter null values from arrays
|
|
40
|
+
* const values = ["a", null, "b", null, "c"]
|
|
41
|
+
* const nonNullValues = values.filter(isNonNull) // ["a", "b", "c"]
|
|
42
|
+
* ```
|
|
15
43
|
* @public
|
|
16
44
|
*/
|
|
17
45
|
export function isNonNull<T>(value: T): value is typeof value extends null ? never : T {
|
|
@@ -19,9 +47,23 @@ export function isNonNull<T>(value: T): value is typeof value extends null ? nev
|
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
/**
|
|
22
|
-
* Get whether a value is nullish (null
|
|
50
|
+
* Get whether a value is not nullish (not null and not undefined).
|
|
23
51
|
*
|
|
24
52
|
* @param value - The value to check.
|
|
53
|
+
* @returns True if the value is neither null nor undefined, with proper type narrowing.
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const maybeString: string | null | undefined = getValue()
|
|
57
|
+
*
|
|
58
|
+
* if (isNonNullish(maybeString)) {
|
|
59
|
+
* // TypeScript knows maybeString is string, not null or undefined
|
|
60
|
+
* console.log(maybeString.charAt(0))
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // Filter nullish values from arrays
|
|
64
|
+
* const values = ["hello", null, "world", undefined, "!"]
|
|
65
|
+
* const cleanValues = values.filter(isNonNullish) // ["hello", "world", "!"]
|
|
66
|
+
* ```
|
|
25
67
|
* @public
|
|
26
68
|
*/
|
|
27
69
|
export function isNonNullish<T>(
|
|
@@ -52,15 +94,37 @@ const _structuredClone = getStructuredClone()
|
|
|
52
94
|
* Create a deep copy of a value. Uses the structuredClone API if available, otherwise uses JSON.parse(JSON.stringify()).
|
|
53
95
|
*
|
|
54
96
|
* @param i - The value to clone.
|
|
55
|
-
* @
|
|
97
|
+
* @returns A deep copy of the input value.
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const original = { a: 1, b: { c: 2 } }
|
|
101
|
+
* const copy = structuredClone(original)
|
|
102
|
+
*
|
|
103
|
+
* copy.b.c = 3
|
|
104
|
+
* console.log(original.b.c) // 2 (unchanged)
|
|
105
|
+
* console.log(copy.b.c) // 3
|
|
106
|
+
*
|
|
107
|
+
* // Works with complex objects
|
|
108
|
+
* const complexObject = {
|
|
109
|
+
* date: new Date(),
|
|
110
|
+
* array: [1, 2, 3],
|
|
111
|
+
* nested: { deep: { value: "test" } }
|
|
112
|
+
* }
|
|
113
|
+
* const cloned = structuredClone(complexObject)
|
|
114
|
+
* ```
|
|
115
|
+
* @public
|
|
116
|
+
*/
|
|
56
117
|
export const structuredClone = _structuredClone[0]
|
|
57
118
|
|
|
58
119
|
/**
|
|
120
|
+
* Whether the current environment has native structuredClone support.
|
|
121
|
+
* @returns True if using native structuredClone, false if using JSON fallback.
|
|
59
122
|
* @internal
|
|
60
123
|
*/
|
|
61
124
|
export const isNativeStructuredClone = _structuredClone[1]
|
|
62
125
|
|
|
63
126
|
/**
|
|
127
|
+
* The prototype object used by structuredClone for cloned objects.
|
|
64
128
|
* When we patch structuredClone in jsdom for testing (see https://github.com/jsdom/jsdom/issues/3363),
|
|
65
129
|
* the Object that is used as a prototype for the cloned object is not the same as the Object in
|
|
66
130
|
* the code under test (that comes from jsdom's fake global context). This constant is used in
|