@sohanemon/utils 5.3.0 → 6.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +329 -112
- package/dist/functions/deepmerge.d.ts +59 -0
- package/dist/functions/deepmerge.js +116 -0
- package/dist/functions/hydrate.d.ts +7 -37
- package/dist/functions/hydrate.js +19 -143
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.js +1 -0
- package/dist/functions/utils.js +1 -1
- package/dist/types/guards.d.ts +1 -0
- package/dist/types/guards.js +11 -0
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -3,22 +3,23 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|

|
|
6
|
+

|
|
6
7
|
|
|
7
8
|
## Description
|
|
8
9
|
|
|
9
|
-
`sohanemon/utils` is a collection of utility functions and
|
|
10
|
+
`sohanemon/utils` is a comprehensive collection of utility functions, hooks, components, and types designed to simplify common tasks in modern web development. It includes utilities for object manipulation, async operations, scheduling, data transformation, React hooks for state management and effects, UI components, and advanced TypeScript types. The library is built with TypeScript and is fully typed, ensuring a smooth and error-free development experience.
|
|
10
11
|
|
|
11
12
|
## Features
|
|
12
13
|
|
|
13
|
-
- **Object Utilities**: Functions to
|
|
14
|
+
- **Object Utilities**: Functions to access and manipulate nested object properties, extend objects, and more.
|
|
15
|
+
- **Data Transformation**: Deep merging, null-to-undefined conversion, slug generation, text normalization, and more.
|
|
16
|
+
- **Async Operations**: Polling, scheduling, debouncing, throttling, and safe async execution.
|
|
14
17
|
- **Cookie Management**: Functions to set, get, delete, and check for cookies.
|
|
15
18
|
- **Class Name Merging**: A utility to merge class names with Tailwind CSS and custom logic.
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **URL Parameter Management**: A hook to manage URL parameters as state.
|
|
21
|
-
- **DOM Calculation**: Hooks to calculate dimensions of elements based on viewport and other block dimensions.
|
|
19
|
+
- **React Hooks**: Hooks for media queries, effects, state management (local/session storage, URL params), DOM calculations, async operations, scheduling, and more.
|
|
20
|
+
- **UI Components**: React components for HTML injection, media wrapping, responsive indicators, scrollable markers, and Iconify icons.
|
|
21
|
+
- **TypeScript Types**: Advanced utility types for deep partials, requireds, readonly, guards, and type-level logic gates.
|
|
22
|
+
- **Browser Utilities**: Clipboard operations, scroll management, SSR detection, and more.
|
|
22
23
|
|
|
23
24
|
## Installation
|
|
24
25
|
|
|
@@ -38,17 +39,20 @@ yarn add @sohanemon/utils
|
|
|
38
39
|
|
|
39
40
|
### Importing Utilities
|
|
40
41
|
|
|
41
|
-
You can import individual utilities or
|
|
42
|
+
You can import individual utilities, hooks, components, or types as needed:
|
|
42
43
|
|
|
43
|
-
```
|
|
44
|
-
import { cn, getObjectValue, setClientSideCookie } from '@sohanemon/utils';
|
|
44
|
+
```typescript
|
|
45
|
+
import { cn, getObjectValue, setClientSideCookie, hydrate, poll } from '@sohanemon/utils';
|
|
46
|
+
import { useAsync, useLocalStorage } from '@sohanemon/utils';
|
|
47
|
+
import { HtmlInjector, ResponsiveIndicator } from '@sohanemon/utils';
|
|
48
|
+
import type { DeepPartial, Primitive } from '@sohanemon/utils';
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
### Examples
|
|
48
52
|
|
|
49
53
|
#### Class Name Merging
|
|
50
54
|
|
|
51
|
-
```
|
|
55
|
+
```typescript
|
|
52
56
|
import { cn } from '@sohanemon/utils';
|
|
53
57
|
|
|
54
58
|
const className = cn('bg-blue-500', 'text-white', 'p-4', 'rounded-lg');
|
|
@@ -56,118 +60,102 @@ const className = cn('bg-blue-500', 'text-white', 'p-4', 'rounded-lg');
|
|
|
56
60
|
|
|
57
61
|
#### Object Utilities
|
|
58
62
|
|
|
59
|
-
```
|
|
60
|
-
import { getObjectValue } from '@sohanemon/utils';
|
|
63
|
+
```typescript
|
|
64
|
+
import { getObjectValue, extendProps } from '@sohanemon/utils';
|
|
61
65
|
|
|
62
66
|
const obj = { a: { b: { c: 1 } } };
|
|
63
67
|
const value = getObjectValue(obj, 'a.b.c'); // 1
|
|
68
|
+
|
|
69
|
+
const extended = extendProps({ a: 1 }, { b: 'hello' }); // { a: 1, b: 'hello' }
|
|
64
70
|
```
|
|
65
71
|
|
|
66
|
-
####
|
|
72
|
+
#### Data Transformation
|
|
67
73
|
|
|
68
|
-
```
|
|
69
|
-
import {
|
|
74
|
+
```typescript
|
|
75
|
+
import { hydrate, convertToSlug, normalizeText } from '@sohanemon/utils';
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
const
|
|
77
|
+
const cleaned = hydrate({ a: null, b: { c: null } }); // { a: undefined, b: { c: undefined } }
|
|
78
|
+
const slug = convertToSlug('Hello World!'); // 'hello-world'
|
|
79
|
+
const normalized = normalizeText('Café', { removeAccents: true }); // 'cafe'
|
|
73
80
|
```
|
|
74
81
|
|
|
75
|
-
####
|
|
82
|
+
#### Async Operations
|
|
76
83
|
|
|
77
|
-
```
|
|
78
|
-
import {
|
|
84
|
+
```typescript
|
|
85
|
+
import { poll, shield, sleep } from '@sohanemon/utils';
|
|
79
86
|
|
|
80
|
-
const
|
|
87
|
+
const result = await poll(async () => {
|
|
88
|
+
const status = await checkStatus();
|
|
89
|
+
return status === 'ready' ? status : null;
|
|
90
|
+
}, { interval: 2000, timeout: 30000 });
|
|
91
|
+
|
|
92
|
+
const [error, data] = await shield(fetchData());
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Cookie Management
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { setClientSideCookie, getClientSideCookie } from '@sohanemon/utils';
|
|
99
|
+
|
|
100
|
+
setClientSideCookie('username', 'sohanemon', 7);
|
|
101
|
+
const { value } = getClientSideCookie('username'); // 'sohanemon'
|
|
81
102
|
```
|
|
82
103
|
|
|
83
104
|
#### Debounce and Throttle
|
|
84
105
|
|
|
85
|
-
```
|
|
106
|
+
```typescript
|
|
86
107
|
import { debounce, throttle } from '@sohanemon/utils';
|
|
87
108
|
|
|
88
109
|
const debouncedFunction = debounce(() => console.log('Debounced!'), 300);
|
|
89
110
|
const throttledFunction = throttle(() => console.log('Throttled!'), 300);
|
|
90
111
|
```
|
|
91
112
|
|
|
92
|
-
####
|
|
93
|
-
|
|
94
|
-
```javascript
|
|
95
|
-
import { useCopyToClipboard } from '@sohanemon/utils/hooks';
|
|
96
|
-
|
|
97
|
-
const { isCopied, copy } = useCopyToClipboard();
|
|
113
|
+
#### React Hooks
|
|
98
114
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<button onClick={() => copy('Hello, World!')}>Copy</button>
|
|
102
|
-
{isCopied && <span>Copied!</span>}
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
```
|
|
115
|
+
```typescript
|
|
116
|
+
import { useAsync, useLocalStorage, useMediaQuery, useCopyToClipboard } from '@sohanemon/utils';
|
|
106
117
|
|
|
107
|
-
|
|
118
|
+
const { data, isLoading } = useAsync(async (signal) => {
|
|
119
|
+
return await fetchData(signal);
|
|
120
|
+
}, { mode: 'auto' });
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
import { useLocalStorage } from '@sohanemon/utils/hooks';
|
|
122
|
+
const [value, setValue] = useLocalStorage('key', { count: 0 });
|
|
111
123
|
|
|
112
|
-
const
|
|
124
|
+
const isMobile = useMediaQuery('sm');
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
<div>
|
|
116
|
-
<input
|
|
117
|
-
type="text"
|
|
118
|
-
value={value}
|
|
119
|
-
onChange={(e) => setValue(e.target.value)}
|
|
120
|
-
/>
|
|
121
|
-
</div>
|
|
122
|
-
);
|
|
126
|
+
const { isCopied, copy } = useCopyToClipboard();
|
|
123
127
|
```
|
|
124
128
|
|
|
125
|
-
####
|
|
126
|
-
|
|
127
|
-
```javascript
|
|
128
|
-
import { useUrlParams } from '@sohanemon/utils/hooks';
|
|
129
|
+
#### UI Components
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
```tsx
|
|
132
|
+
import { HtmlInjector, ResponsiveIndicator, Iconify } from '@sohanemon/utils';
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
type="text"
|
|
136
|
-
value={param}
|
|
137
|
-
onChange={(e) => setParam(e.target.value)}
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
134
|
+
<HtmlInjector html="<p>Injected HTML</p>" />
|
|
135
|
+
<ResponsiveIndicator />
|
|
136
|
+
<Iconify icon="mdi:home" />
|
|
141
137
|
```
|
|
142
138
|
|
|
143
|
-
####
|
|
139
|
+
#### TypeScript Types
|
|
144
140
|
|
|
145
|
-
```
|
|
146
|
-
import {
|
|
147
|
-
|
|
148
|
-
const { height, width } = useDomCalculation({
|
|
149
|
-
blockIds: ['header', 'footer'],
|
|
150
|
-
margin: 20,
|
|
151
|
-
substract: true,
|
|
152
|
-
});
|
|
141
|
+
```typescript
|
|
142
|
+
import type { DeepPartial, Nullable, KeysOfType } from '@sohanemon/utils';
|
|
153
143
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
</div>
|
|
158
|
-
);
|
|
144
|
+
type PartialUser = DeepPartial<User>;
|
|
145
|
+
type NullableUser = Nullable<User>;
|
|
146
|
+
type StringKeys = KeysOfType<User, string>;
|
|
159
147
|
```
|
|
160
148
|
|
|
161
149
|
## API Documentation
|
|
162
150
|
|
|
163
|
-
###
|
|
151
|
+
### Functions
|
|
164
152
|
|
|
153
|
+
#### Class Name Merging
|
|
165
154
|
```typescript
|
|
166
155
|
cn(...inputs: ClassValue[]): string
|
|
167
156
|
```
|
|
168
157
|
|
|
169
|
-
|
|
170
|
-
|
|
158
|
+
#### Object Utilities
|
|
171
159
|
```typescript
|
|
172
160
|
getObjectValue<T, K extends Array<string | number>, D>(
|
|
173
161
|
obj: T,
|
|
@@ -190,26 +178,63 @@ getObjectValue<T, S extends string>(
|
|
|
190
178
|
obj: T,
|
|
191
179
|
path: S
|
|
192
180
|
): GetValue<T, SplitPath<S>> | undefined;
|
|
193
|
-
```
|
|
194
181
|
|
|
195
|
-
|
|
182
|
+
extendProps<T extends object, P extends object>(
|
|
183
|
+
base: T,
|
|
184
|
+
props: P
|
|
185
|
+
): T & P;
|
|
186
|
+
```
|
|
196
187
|
|
|
188
|
+
#### Data Transformation
|
|
197
189
|
```typescript
|
|
198
|
-
|
|
199
|
-
deleteClientSideCookie(name: string, path?: string): void
|
|
200
|
-
hasClientSideCookie(name: string): boolean
|
|
201
|
-
getClientSideCookie(name: string): { value: string | undefined }
|
|
202
|
-
```
|
|
190
|
+
hydrate<T>(data: T): Hydrate<T>
|
|
203
191
|
|
|
204
|
-
|
|
192
|
+
deepmerge<T, U>(
|
|
193
|
+
target: T,
|
|
194
|
+
source: U,
|
|
195
|
+
options?: {
|
|
196
|
+
arrayMerge?: (target: any[], source: any[]) => any[];
|
|
197
|
+
maxDepth?: number;
|
|
198
|
+
}
|
|
199
|
+
): T & U
|
|
205
200
|
|
|
206
|
-
|
|
207
|
-
useMediaQuery(tailwindBreakpoint: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | `(${string})`): boolean
|
|
208
|
-
```
|
|
201
|
+
convertToSlug(str: string): string
|
|
209
202
|
|
|
210
|
-
|
|
203
|
+
normalizeText(
|
|
204
|
+
str?: string | null,
|
|
205
|
+
options?: {
|
|
206
|
+
lowercase?: boolean;
|
|
207
|
+
removeAccents?: boolean;
|
|
208
|
+
removeNonAlphanumeric?: boolean;
|
|
209
|
+
}
|
|
210
|
+
): string
|
|
211
211
|
|
|
212
|
+
convertToNormalCase(inputString: string): string
|
|
213
|
+
|
|
214
|
+
escapeRegExp(str: string): string
|
|
215
|
+
|
|
216
|
+
printf(format: string, ...args: unknown[]): string
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### Async & Scheduling
|
|
212
220
|
```typescript
|
|
221
|
+
poll<T>(
|
|
222
|
+
cond: () => Promise<T | null | false | undefined>,
|
|
223
|
+
options?: {
|
|
224
|
+
interval?: number;
|
|
225
|
+
timeout?: number;
|
|
226
|
+
signal?: AbortSignal;
|
|
227
|
+
jitter?: boolean;
|
|
228
|
+
}
|
|
229
|
+
): Promise<T>
|
|
230
|
+
|
|
231
|
+
schedule(task: Task, options?: ScheduleOpts): void
|
|
232
|
+
|
|
233
|
+
shield<T, E = Error>(operation: Promise<T>): Promise<[E | null, T | null]>
|
|
234
|
+
shield<T, E = Error>(operation: () => T): [E | null, T | null]
|
|
235
|
+
|
|
236
|
+
sleep(time?: number, signal?: AbortSignal): Promise<void>
|
|
237
|
+
|
|
213
238
|
debounce<F extends (...args: any[]) => any>(
|
|
214
239
|
function_: F,
|
|
215
240
|
wait?: number,
|
|
@@ -223,39 +248,231 @@ throttle<F extends (...args: any[]) => any>(
|
|
|
223
248
|
): ThrottledFunction<F>
|
|
224
249
|
```
|
|
225
250
|
|
|
226
|
-
|
|
251
|
+
#### Cookie Management
|
|
252
|
+
```typescript
|
|
253
|
+
setClientSideCookie(name: string, value: string, days?: number, path?: string): void
|
|
254
|
+
deleteClientSideCookie(name: string, path?: string): void
|
|
255
|
+
hasClientSideCookie(name: string): boolean
|
|
256
|
+
getClientSideCookie(name: string): { value: string | undefined }
|
|
257
|
+
```
|
|
227
258
|
|
|
259
|
+
#### Browser Utilities
|
|
228
260
|
```typescript
|
|
229
|
-
|
|
261
|
+
copyToClipboard(value: string, onSuccess?: () => void): void
|
|
262
|
+
|
|
263
|
+
scrollTo(
|
|
264
|
+
containerSelector: string | React.RefObject<HTMLDivElement>,
|
|
265
|
+
to: 'top' | 'bottom'
|
|
266
|
+
): void
|
|
267
|
+
|
|
268
|
+
goToClientSideHash(id: string, opts?: ScrollIntoViewOptions): void
|
|
269
|
+
|
|
270
|
+
isSSR: boolean
|
|
271
|
+
|
|
272
|
+
svgToBase64(str: string): string
|
|
273
|
+
|
|
274
|
+
isLinkActive(options: {
|
|
275
|
+
path: string;
|
|
276
|
+
currentPath: string;
|
|
277
|
+
locales?: string[];
|
|
278
|
+
exact?: boolean;
|
|
279
|
+
}): boolean
|
|
280
|
+
|
|
281
|
+
isNavActive(href: string, path: string): boolean // deprecated
|
|
282
|
+
|
|
283
|
+
cleanSrc(src: string): string
|
|
230
284
|
```
|
|
231
285
|
|
|
232
|
-
###
|
|
286
|
+
### React Hooks
|
|
233
287
|
|
|
288
|
+
#### State & Effects
|
|
234
289
|
```typescript
|
|
235
|
-
|
|
290
|
+
useAction<Input, Result>(
|
|
291
|
+
action: ActionType<Input, Result>,
|
|
292
|
+
options?: UseActionOptions<Input, Result>
|
|
293
|
+
): {
|
|
294
|
+
execute: (input: Input) => void;
|
|
295
|
+
executeAsync: (input: Input) => Promise<Result>;
|
|
296
|
+
reset: () => void;
|
|
297
|
+
useExecute: (input: Input) => void;
|
|
298
|
+
data: Result | null;
|
|
299
|
+
error: Error | null;
|
|
300
|
+
input: Input | undefined;
|
|
301
|
+
isIdle: boolean;
|
|
302
|
+
isLoading: boolean;
|
|
303
|
+
isSuccess: boolean;
|
|
304
|
+
isError: boolean;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
useAsync<TData, TError extends Error = Error>(
|
|
308
|
+
asyncFn: (signal: AbortSignal) => Promise<TData>,
|
|
309
|
+
options?: UseAsyncOptions<TData, TError>
|
|
310
|
+
): UseAsyncReturn<TData, TError>
|
|
311
|
+
|
|
312
|
+
useLocalStorage<T extends Record<string, any>>(
|
|
313
|
+
key: string,
|
|
314
|
+
defaultValue: T
|
|
315
|
+
): [T, React.Dispatch<React.SetStateAction<T>>]
|
|
316
|
+
|
|
317
|
+
useSessionStorage<T extends Record<string, any>>(
|
|
318
|
+
key: string,
|
|
319
|
+
defaultValue: T
|
|
320
|
+
): [T, React.Dispatch<React.SetStateAction<T>>]
|
|
321
|
+
|
|
322
|
+
useUrlParams<T extends string | number | boolean>(
|
|
323
|
+
key: string,
|
|
324
|
+
defaultValue: T
|
|
325
|
+
): [T, (value: T) => void]
|
|
326
|
+
|
|
327
|
+
useDebounce<T>(state: T, delay?: number): T
|
|
328
|
+
|
|
329
|
+
useTimeout(callback: () => void, delay?: number | null): void
|
|
330
|
+
|
|
331
|
+
useEffectOnce(effect: React.EffectCallback): void
|
|
332
|
+
|
|
333
|
+
useUpdateEffect(effect: React.EffectCallback, deps: React.DependencyList): void
|
|
334
|
+
|
|
335
|
+
useIsomorphicEffect: typeof React.useLayoutEffect | typeof React.useEffect
|
|
236
336
|
```
|
|
237
337
|
|
|
238
|
-
|
|
338
|
+
#### UI & Interaction
|
|
339
|
+
```typescript
|
|
340
|
+
useMediaQuery(tailwindBreakpoint: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | `(${string})`): boolean
|
|
341
|
+
|
|
342
|
+
useClickOutside(callback: () => void): React.RefObject<HTMLDivElement>
|
|
343
|
+
|
|
344
|
+
useCopyToClipboard(options?: { timeout?: number }): {
|
|
345
|
+
isCopied: boolean;
|
|
346
|
+
copy: (value: string) => void;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
useWindowEvent<K extends keyof WindowEventMap>(
|
|
350
|
+
type: K,
|
|
351
|
+
listener: (this: Window, ev: WindowEventMap[K]) => void,
|
|
352
|
+
options?: boolean | AddEventListenerOptions
|
|
353
|
+
): void
|
|
239
354
|
|
|
355
|
+
useQuerySelector<T extends Element>(selector: string): T | null
|
|
356
|
+
|
|
357
|
+
useIsClient(): boolean
|
|
358
|
+
|
|
359
|
+
useLockScroll(): void
|
|
360
|
+
|
|
361
|
+
useIntersection(options?: UseIntersectionOptions): {
|
|
362
|
+
ref: React.RefObject<Element>;
|
|
363
|
+
isIntersecting: boolean;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
useIsScrolling(): {
|
|
367
|
+
isScrolling: boolean;
|
|
368
|
+
scrollableContainerRef: React.RefObject<HTMLElement>;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
useIsAtTop(options?: { offset?: number }): {
|
|
372
|
+
scrollableContainerRef: React.RefObject<HTMLElement>;
|
|
373
|
+
isAtTop: boolean;
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### DOM & Layout
|
|
378
|
+
```typescript
|
|
379
|
+
useDomCalculation(options: CalculationProps): { height: number; width: number }
|
|
380
|
+
|
|
381
|
+
useHeightCalculation(options: CalculationProps2): number
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### Scheduling
|
|
240
385
|
```typescript
|
|
241
|
-
|
|
386
|
+
useSchedule(options?: ScheduleOpts): (task: Task) => void
|
|
387
|
+
|
|
388
|
+
useScheduledEffect(
|
|
389
|
+
effect: () => void | (() => void),
|
|
390
|
+
deps?: React.DependencyList,
|
|
391
|
+
options?: ScheduleOpts
|
|
392
|
+
): void
|
|
242
393
|
```
|
|
243
394
|
|
|
244
|
-
###
|
|
395
|
+
### Components
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
Iconify: React.Component (from @iconify/react)
|
|
399
|
+
|
|
400
|
+
HtmlInjector: React.Component<{ html: string; className?: string }>
|
|
401
|
+
|
|
402
|
+
MediaWrapper: React.Component<{
|
|
403
|
+
src: string;
|
|
404
|
+
alt?: string;
|
|
405
|
+
className?: string;
|
|
406
|
+
lazy?: boolean;
|
|
407
|
+
}>
|
|
408
|
+
|
|
409
|
+
ResponsiveIndicator: React.Component<{
|
|
410
|
+
className?: string;
|
|
411
|
+
showText?: boolean;
|
|
412
|
+
}>
|
|
413
|
+
|
|
414
|
+
ScrollableMarker: React.Component<{
|
|
415
|
+
className?: string;
|
|
416
|
+
children?: React.ReactNode;
|
|
417
|
+
}>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Types
|
|
421
|
+
|
|
422
|
+
#### Utility Types
|
|
423
|
+
```typescript
|
|
424
|
+
Keys<T extends object>: keyof T
|
|
425
|
+
Values<T extends object>: T[keyof T]
|
|
426
|
+
DeepPartial<T>: T with all properties optional recursively
|
|
427
|
+
SelectivePartial<T, K>: T with selected keys optional
|
|
428
|
+
DeepRequired<T>: T with all properties required recursively
|
|
429
|
+
SelectiveRequired<T, K>: T with selected keys required
|
|
430
|
+
Never<T>: Object with never values
|
|
431
|
+
Nullable<T>: T with null added to primitives
|
|
432
|
+
Optional<T>: T with undefined added to primitives
|
|
433
|
+
Nullish<T>: T with null|undefined added to primitives
|
|
434
|
+
Maybe<T>: T with all properties optional and nullish
|
|
435
|
+
DeepReadonly<T>: T with all properties readonly recursively
|
|
436
|
+
Mutable<T>: T with readonly removed
|
|
437
|
+
KeysOfType<T, U>: Keys of T where value is U
|
|
438
|
+
OmitByType<T, U>: T without properties of type U
|
|
439
|
+
RequiredKeys<T, K>: T with selected keys required
|
|
440
|
+
Diff<T, U>: Properties in T or U but not both
|
|
441
|
+
Intersection<T, U>: Common properties
|
|
442
|
+
Merge<T, U>: Merged object
|
|
443
|
+
Substract<T, U>: T without U properties
|
|
444
|
+
AllOrNone<T>: T or empty object
|
|
445
|
+
OneOf<T>: Union of single property objects
|
|
446
|
+
TwoOf<T>: Union of two property objects
|
|
447
|
+
Prettify<T>: Clean type representation
|
|
448
|
+
NestedKeyOf<T>: All nested keys as strings
|
|
449
|
+
Without<T, U>: T without U keys
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### Type Guards & Primitives
|
|
453
|
+
```typescript
|
|
454
|
+
Primitive: string | number | bigint | boolean | symbol | null | undefined
|
|
455
|
+
Falsy: false | '' | 0 | null | undefined
|
|
456
|
+
|
|
457
|
+
isFalsy(val: unknown): val is Falsy
|
|
458
|
+
isNullish(val: unknown): val is null | undefined
|
|
459
|
+
isPrimitive(val: unknown): val is Primitive
|
|
460
|
+
isPlainObject(value: unknown): value is Record<string, any>
|
|
461
|
+
```
|
|
245
462
|
|
|
463
|
+
#### Logic Gates
|
|
246
464
|
```typescript
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}): { height: number; width: number }
|
|
465
|
+
BUFFER<T>: T
|
|
466
|
+
IMPLIES<T, U>: true if T extends U
|
|
467
|
+
XOR_Binary<T, U>: T | U with exclusions
|
|
468
|
+
XNOR_Binary<T, U>: T & U | neither
|
|
469
|
+
AND<T extends any[]>: All true
|
|
470
|
+
OR<T extends any[]>: At least one true
|
|
471
|
+
XOR<T extends any[]>: Odd number true
|
|
472
|
+
XNOR<T extends any[]>: Even number true
|
|
473
|
+
NOT<T>: Never properties
|
|
474
|
+
NAND<T extends any[]>: NOT AND
|
|
475
|
+
NOR<T extends any[]>: NOT OR
|
|
259
476
|
```
|
|
260
477
|
|
|
261
478
|
## Contributing
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
type TAllKeys<T> = T extends any ? keyof T : never;
|
|
2
|
+
type TIndexValue<T, K extends PropertyKey, D = never> = T extends any ? K extends keyof T ? T[K] : D : never;
|
|
3
|
+
type TPartialKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>> extends infer O ? {
|
|
4
|
+
[P in keyof O]: O[P];
|
|
5
|
+
} : never;
|
|
6
|
+
type TFunction = (...a: any[]) => any;
|
|
7
|
+
type TPrimitives = string | number | boolean | bigint | symbol | Date | TFunction;
|
|
8
|
+
type TMerged<T> = [T] extends [Array<any>] ? {
|
|
9
|
+
[K in keyof T]: TMerged<T[K]>;
|
|
10
|
+
} : [T] extends [TPrimitives] ? T : [T] extends [object] ? TPartialKeys<{
|
|
11
|
+
[K in TAllKeys<T>]: TMerged<TIndexValue<T, K>>;
|
|
12
|
+
}, never> : T;
|
|
13
|
+
/**
|
|
14
|
+
* Deeply merges multiple objects, with later sources taking precedence.
|
|
15
|
+
* Handles nested objects, arrays, and special object types with circular reference detection.
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - Deep merging of nested objects
|
|
19
|
+
* - Configurable array merging strategies
|
|
20
|
+
* - Circular reference detection and handling
|
|
21
|
+
* - Support for symbols and special objects (Date, RegExp, etc.)
|
|
22
|
+
* - Type-safe with improved generics
|
|
23
|
+
* - Optional cloning to avoid mutation
|
|
24
|
+
* - Custom merge functions for specific keys
|
|
25
|
+
*
|
|
26
|
+
* @template T - The target object type
|
|
27
|
+
* @param target - The target object to merge into
|
|
28
|
+
* @param sources - Source objects to merge from (can have additional properties)
|
|
29
|
+
* @param options - Configuration options
|
|
30
|
+
* @param options.arrayMerge - How to merge arrays: 'replace' (default), 'concat', or 'merge'
|
|
31
|
+
* @param options.clone - Whether to clone the target (default: true)
|
|
32
|
+
* @param options.customMerge - Custom merge function for specific keys
|
|
33
|
+
* @param options.maxDepth - Maximum recursion depth (default: 100)
|
|
34
|
+
* @returns The merged object with proper typing
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Basic merge
|
|
38
|
+
* deepmerge({ a: 1 }, { b: 2 }) // { a: 1, b: 2 }
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Nested merge
|
|
42
|
+
* deepmerge({ a: { x: 1 } }, { a: { y: 2 } }) // { a: { x: 1, y: 2 } }
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Array concat
|
|
46
|
+
* deepmerge({ arr: [1] }, { arr: [2] }, { arrayMerge: 'concat' }) // { arr: [1, 2] }
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Sources with extra properties
|
|
50
|
+
* deepmerge({ a: 1 }, { b: 2, c: 3 }) // { a: 1, b: 2, c: 3 }
|
|
51
|
+
*/
|
|
52
|
+
export declare function deepmerge<T extends Record<string, any>, S extends Record<string, any>[]>(target: T, ...sources: S): TMerged<T | S[number]>;
|
|
53
|
+
export declare function deepmerge<T extends Record<string, any>, S extends Record<string, any>[]>(target: T, sources: S, options?: {
|
|
54
|
+
arrayMerge?: 'replace' | 'concat' | 'merge' | ((target: any[], source: any[]) => any[]);
|
|
55
|
+
clone?: boolean;
|
|
56
|
+
customMerge?: (key: string | symbol, targetValue: any, sourceValue: any) => any;
|
|
57
|
+
maxDepth?: number;
|
|
58
|
+
}): TMerged<T | S[number]>;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { isPlainObject } from '../types';
|
|
2
|
+
export function deepmerge(target, ...args) {
|
|
3
|
+
let sources;
|
|
4
|
+
let options = {};
|
|
5
|
+
// Check if last arg is options object
|
|
6
|
+
const lastArg = args[args.length - 1];
|
|
7
|
+
if (lastArg &&
|
|
8
|
+
typeof lastArg === 'object' &&
|
|
9
|
+
!Array.isArray(lastArg) &&
|
|
10
|
+
(lastArg.arrayMerge !== undefined ||
|
|
11
|
+
lastArg.clone !== undefined ||
|
|
12
|
+
lastArg.customMerge !== undefined ||
|
|
13
|
+
lastArg.maxDepth !== undefined)) {
|
|
14
|
+
options = { ...options, ...lastArg };
|
|
15
|
+
sources = args.slice(0, -1);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
sources = args;
|
|
19
|
+
}
|
|
20
|
+
const { arrayMerge = 'replace', clone = true, customMerge, maxDepth = 100, } = options;
|
|
21
|
+
const visited = new WeakMap();
|
|
22
|
+
return mergeObjects(target, sources, 0);
|
|
23
|
+
function mergeObjects(target, sources, depth) {
|
|
24
|
+
if (depth >= maxDepth) {
|
|
25
|
+
console.warn(`[deepmerge] Maximum depth ${maxDepth} exceeded. Returning target as-is.`);
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
if (!isPlainObject(target) && !Array.isArray(target)) {
|
|
29
|
+
// For primitives or special objects, return the last source or target
|
|
30
|
+
for (const source of sources) {
|
|
31
|
+
if (source !== undefined)
|
|
32
|
+
return source;
|
|
33
|
+
}
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
let result = clone
|
|
37
|
+
? Array.isArray(target)
|
|
38
|
+
? [...target]
|
|
39
|
+
: { ...target }
|
|
40
|
+
: target;
|
|
41
|
+
for (const source of sources) {
|
|
42
|
+
if (source == null)
|
|
43
|
+
continue;
|
|
44
|
+
if (visited.has(source)) {
|
|
45
|
+
// Circular reference, skip
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
visited.set(source, result);
|
|
49
|
+
if (Array.isArray(result) && Array.isArray(source)) {
|
|
50
|
+
result = mergeArrays(result, source, arrayMerge);
|
|
51
|
+
}
|
|
52
|
+
else if (isPlainObject(result) && isPlainObject(source)) {
|
|
53
|
+
const keys = new Set([
|
|
54
|
+
...Object.keys(result),
|
|
55
|
+
...Object.keys(source),
|
|
56
|
+
...Object.getOwnPropertySymbols(result),
|
|
57
|
+
...Object.getOwnPropertySymbols(source),
|
|
58
|
+
]);
|
|
59
|
+
for (const key of keys) {
|
|
60
|
+
const targetValue = result[key];
|
|
61
|
+
const sourceValue = source[key];
|
|
62
|
+
if (customMerge &&
|
|
63
|
+
customMerge(key, targetValue, sourceValue) !== undefined) {
|
|
64
|
+
result[key] = customMerge(key, targetValue, sourceValue);
|
|
65
|
+
}
|
|
66
|
+
else if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {
|
|
67
|
+
result[key] = mergeObjects(targetValue, [sourceValue], depth + 1);
|
|
68
|
+
}
|
|
69
|
+
else if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
|
|
70
|
+
result[key] = mergeArrays(targetValue, sourceValue, arrayMerge);
|
|
71
|
+
}
|
|
72
|
+
else if (sourceValue !== undefined) {
|
|
73
|
+
result[key] = sourceValue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// If types don't match, source takes precedence
|
|
79
|
+
result = source;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function mergeArrays(target, source, strategy) {
|
|
85
|
+
if (typeof strategy === 'function') {
|
|
86
|
+
return strategy(target, source);
|
|
87
|
+
}
|
|
88
|
+
switch (strategy) {
|
|
89
|
+
case 'concat':
|
|
90
|
+
return [...target, ...source];
|
|
91
|
+
case 'merge':
|
|
92
|
+
const maxLength = Math.max(target.length, source.length);
|
|
93
|
+
const merged = [];
|
|
94
|
+
for (let i = 0; i < maxLength; i++) {
|
|
95
|
+
if (i < target.length && i < source.length) {
|
|
96
|
+
if (isPlainObject(target[i]) && isPlainObject(source[i])) {
|
|
97
|
+
merged[i] = mergeObjects(target[i], [source[i]], 0);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
merged[i] = source[i];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (i < target.length) {
|
|
104
|
+
merged[i] = target[i];
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
merged[i] = source[i];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return merged;
|
|
111
|
+
case 'replace':
|
|
112
|
+
default:
|
|
113
|
+
return [...source];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,45 +1,15 @@
|
|
|
1
|
+
type Hydrate<T> = T extends null ? undefined : T extends (infer U)[] ? Hydrate<U>[] : T extends object ? {
|
|
2
|
+
[K in keyof T]: Hydrate<T[K]>;
|
|
3
|
+
} : T;
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
* and merges in a fallback object for default values.
|
|
4
|
-
*
|
|
5
|
-
* Features:
|
|
6
|
-
* - Circular reference detection to prevent infinite loops
|
|
7
|
-
* - Proper handling of special objects (Date, Map, Set, RegExp, etc.)
|
|
8
|
-
* - Strict type safety with improved generics
|
|
9
|
-
* - Better error messages and validation
|
|
10
|
-
* - Support for nested structures with Symbol properties
|
|
11
|
-
* - Optional maximum recursion depth to prevent stack overflow
|
|
12
|
-
* - Configurable null-to-undefined conversion
|
|
5
|
+
* Converts all `null` values to `undefined` in the data structure recursively.
|
|
13
6
|
*
|
|
14
7
|
* @param data - Any input data (object, array, primitive)
|
|
15
|
-
* @param fallback - Optional fallback values to merge with
|
|
16
|
-
* @param options - Configuration options
|
|
17
|
-
* @param options.maxDepth - Maximum recursion depth (default: 100)
|
|
18
|
-
* @param options.throwOnCircular - Whether to throw on circular refs (default: false)
|
|
19
|
-
* @param options.convertNullToUndefined - Convert null to undefined (default: true)
|
|
20
8
|
* @returns Same type as input, but with all nulls replaced by undefined
|
|
21
9
|
*
|
|
22
|
-
* @throws {TypeError} If data or fallback are invalid types when strict validation is enabled
|
|
23
|
-
* @throws {RangeError} If circular reference detected and throwOnCircular is true
|
|
24
|
-
*
|
|
25
10
|
* @example
|
|
26
|
-
* // Basic usage
|
|
27
11
|
* hydrate({ a: null, b: 'test' }) // { a: undefined, b: 'test' }
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* // With fallback values
|
|
31
|
-
* hydrate({ a: null }, { a: 'default' }) // { a: 'default' }
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* // Keep nulls as-is
|
|
35
|
-
* hydrate({ a: null }, undefined, { convertNullToUndefined: false }) // { a: null }
|
|
36
|
-
*/
|
|
37
|
-
export declare function hydrate<T>(data: T, fallback?: Partial<T>, options?: {
|
|
38
|
-
maxDepth?: number;
|
|
39
|
-
throwOnCircular?: boolean;
|
|
40
|
-
convertNullToUndefined?: boolean;
|
|
41
|
-
}): T;
|
|
42
|
-
/**
|
|
43
|
-
* Type guard utility for checking if a value is an object with a specific shape
|
|
12
|
+
* hydrate([null, 1, { c: null }]) // [undefined, 1, { c: undefined }]
|
|
44
13
|
*/
|
|
45
|
-
export declare function
|
|
14
|
+
export declare function hydrate<T>(data: T): Hydrate<T>;
|
|
15
|
+
export {};
|
|
@@ -1,155 +1,31 @@
|
|
|
1
|
+
import { isPlainObject } from '../types';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
* and merges in a fallback object for default values.
|
|
4
|
-
*
|
|
5
|
-
* Features:
|
|
6
|
-
* - Circular reference detection to prevent infinite loops
|
|
7
|
-
* - Proper handling of special objects (Date, Map, Set, RegExp, etc.)
|
|
8
|
-
* - Strict type safety with improved generics
|
|
9
|
-
* - Better error messages and validation
|
|
10
|
-
* - Support for nested structures with Symbol properties
|
|
11
|
-
* - Optional maximum recursion depth to prevent stack overflow
|
|
12
|
-
* - Configurable null-to-undefined conversion
|
|
3
|
+
* Converts all `null` values to `undefined` in the data structure recursively.
|
|
13
4
|
*
|
|
14
5
|
* @param data - Any input data (object, array, primitive)
|
|
15
|
-
* @param fallback - Optional fallback values to merge with
|
|
16
|
-
* @param options - Configuration options
|
|
17
|
-
* @param options.maxDepth - Maximum recursion depth (default: 100)
|
|
18
|
-
* @param options.throwOnCircular - Whether to throw on circular refs (default: false)
|
|
19
|
-
* @param options.convertNullToUndefined - Convert null to undefined (default: true)
|
|
20
6
|
* @returns Same type as input, but with all nulls replaced by undefined
|
|
21
7
|
*
|
|
22
|
-
* @throws {TypeError} If data or fallback are invalid types when strict validation is enabled
|
|
23
|
-
* @throws {RangeError} If circular reference detected and throwOnCircular is true
|
|
24
|
-
*
|
|
25
8
|
* @example
|
|
26
|
-
* // Basic usage
|
|
27
9
|
* hydrate({ a: null, b: 'test' }) // { a: undefined, b: 'test' }
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* // With fallback values
|
|
31
|
-
* hydrate({ a: null }, { a: 'default' }) // { a: 'default' }
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* // Keep nulls as-is
|
|
35
|
-
* hydrate({ a: null }, undefined, { convertNullToUndefined: false }) // { a: null }
|
|
10
|
+
* hydrate([null, 1, { c: null }]) // [undefined, 1, { c: undefined }]
|
|
36
11
|
*/
|
|
37
|
-
export function hydrate(data
|
|
38
|
-
|
|
39
|
-
const throwOnCircular = options?.throwOnCircular ?? false;
|
|
40
|
-
const convertNullToUndefined = options?.convertNullToUndefined ?? true;
|
|
41
|
-
// Use WeakSet for O(1) circular reference detection
|
|
42
|
-
const visited = new WeakSet();
|
|
43
|
-
return processValue(data, fallback, 0, visited);
|
|
44
|
-
function processValue(value, fallbackValue, depth, visited) {
|
|
45
|
-
// Check recursion depth
|
|
46
|
-
if (depth > maxDepth) {
|
|
47
|
-
console.warn(`[hydrate] Maximum recursion depth (${maxDepth}) exceeded. Returning value as-is.`);
|
|
48
|
-
return value ?? fallbackValue;
|
|
49
|
-
}
|
|
50
|
-
if (value === null) {
|
|
51
|
-
return convertNullToUndefined ? undefined : (fallbackValue ?? null);
|
|
52
|
-
}
|
|
53
|
-
// Handle undefined - use fallback or return undefined
|
|
54
|
-
if (value === undefined) {
|
|
55
|
-
return fallbackValue ?? undefined;
|
|
56
|
-
}
|
|
57
|
-
// Handle primitives: string, number, boolean, symbol, bigint
|
|
58
|
-
const type = typeof value;
|
|
59
|
-
if (type !== 'object') {
|
|
60
|
-
return value;
|
|
61
|
-
}
|
|
62
|
-
if (visited.has(value)) {
|
|
63
|
-
if (throwOnCircular) {
|
|
64
|
-
throw new RangeError('[hydrate] Circular reference detected');
|
|
65
|
-
}
|
|
66
|
-
// Return the value as-is to break the cycle
|
|
67
|
-
return value;
|
|
68
|
-
}
|
|
69
|
-
// Mark as visited to detect circular references
|
|
70
|
-
visited.add(value);
|
|
71
|
-
if (isSpecialObject(value)) {
|
|
72
|
-
return handleSpecialObject(value, fallbackValue);
|
|
73
|
-
}
|
|
74
|
-
// Handle arrays
|
|
75
|
-
if (Array.isArray(value)) {
|
|
76
|
-
const fallbackArray = Array.isArray(fallbackValue)
|
|
77
|
-
? fallbackValue
|
|
78
|
-
: undefined;
|
|
79
|
-
return value.map((item, index) => processValue(item, fallbackArray?.[index], depth + 1, visited));
|
|
80
|
-
}
|
|
81
|
-
// Handle plain objects
|
|
82
|
-
if (isPlainObject(value)) {
|
|
83
|
-
const fallbackObj = isPlainObject(fallbackValue)
|
|
84
|
-
? fallbackValue
|
|
85
|
-
: {};
|
|
86
|
-
const result = { ...fallbackObj };
|
|
87
|
-
// Process all enumerable properties including symbols
|
|
88
|
-
const keys = [
|
|
89
|
-
...Object.keys(value),
|
|
90
|
-
...Object.getOwnPropertySymbols(value),
|
|
91
|
-
];
|
|
92
|
-
for (const k of keys) {
|
|
93
|
-
const propValue = value[k];
|
|
94
|
-
const fallbackProp = fallbackObj[k];
|
|
95
|
-
result[k] = processValue(propValue, fallbackProp, depth + 1, visited);
|
|
96
|
-
}
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
// For other objects, return as-is (instances, etc.)
|
|
100
|
-
return value ?? fallbackValue;
|
|
101
|
-
}
|
|
12
|
+
export function hydrate(data) {
|
|
13
|
+
return convertNulls(data);
|
|
102
14
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
111
|
-
return false;
|
|
15
|
+
function convertNulls(value) {
|
|
16
|
+
if (value === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (typeof value !== 'object' || value === null)
|
|
19
|
+
return value;
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.map(convertNulls);
|
|
112
22
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
*/
|
|
120
|
-
function isSpecialObject(value) {
|
|
121
|
-
if (typeof value !== 'object' || value === null) {
|
|
122
|
-
return false;
|
|
23
|
+
if (isPlainObject(value)) {
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const key in value) {
|
|
26
|
+
result[key] = convertNulls(value[key]);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
123
29
|
}
|
|
124
|
-
|
|
125
|
-
const specialTypes = [
|
|
126
|
-
'[object Date]',
|
|
127
|
-
'[object RegExp]',
|
|
128
|
-
'[object Map]',
|
|
129
|
-
'[object Set]',
|
|
130
|
-
'[object WeakMap]',
|
|
131
|
-
'[object WeakSet]',
|
|
132
|
-
'[object Promise]',
|
|
133
|
-
'[object Error]',
|
|
134
|
-
'[object ArrayBuffer]',
|
|
135
|
-
];
|
|
136
|
-
return specialTypes.includes(stringTag);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Handles special object types that shouldn't be deeply cloned
|
|
140
|
-
*/
|
|
141
|
-
function handleSpecialObject(value, fallbackValue) {
|
|
142
|
-
// For special types, return the original value or fallback
|
|
143
|
-
// These shouldn't be deeply cloned as they have internal state
|
|
144
|
-
return value ?? fallbackValue;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Type guard utility for checking if a value is an object with a specific shape
|
|
148
|
-
*/
|
|
149
|
-
export function isWithFallbackCompatible(value) {
|
|
150
|
-
return (typeof value === 'object' &&
|
|
151
|
-
(value === null ||
|
|
152
|
-
Array.isArray(value) ||
|
|
153
|
-
(typeof value === 'object' &&
|
|
154
|
-
Object.prototype.toString.call(value) === '[object Object]')));
|
|
30
|
+
return value;
|
|
155
31
|
}
|
package/dist/functions/index.js
CHANGED
package/dist/functions/utils.js
CHANGED
|
@@ -427,7 +427,7 @@ export function normalizeText(str, options = {}) {
|
|
|
427
427
|
const { lowercase = true, removeAccents = true, removeNonAlphanumeric = true, } = options;
|
|
428
428
|
let result = str.normalize('NFC');
|
|
429
429
|
if (removeAccents) {
|
|
430
|
-
result = result.replace(/\p{M}/gu, ''); // remove accents
|
|
430
|
+
result = result.normalize('NFD').replace(/\p{M}/gu, ''); // decompose and remove accents
|
|
431
431
|
}
|
|
432
432
|
if (removeNonAlphanumeric) {
|
|
433
433
|
result = result.replace(/^[^\p{L}\p{N}]*|[^\p{L}\p{N}]*$/gu, ''); // trim edges
|
package/dist/types/guards.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export type Falsy = false | '' | 0 | null | undefined;
|
|
|
3
3
|
export declare const isFalsy: (val: unknown) => val is Falsy;
|
|
4
4
|
export declare const isNullish: (val: unknown) => val is null | undefined;
|
|
5
5
|
export declare const isPrimitive: (val: unknown) => val is Primitive;
|
|
6
|
+
export declare function isPlainObject(value: unknown): value is Record<string, any>;
|
package/dist/types/guards.js
CHANGED
|
@@ -16,3 +16,14 @@ export const isPrimitive = (val) => {
|
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
+
export function isPlainObject(value) {
|
|
20
|
+
if (typeof value !== 'object' || value === null) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// Objects with null prototype are still plain objects
|
|
27
|
+
const proto = Object.getPrototypeOf(value);
|
|
28
|
+
return proto === null || proto === Object.prototype;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sohanemon/utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.2.2",
|
|
4
4
|
"author": "Sohan Emon <sohanemon@outlook.com>",
|
|
5
5
|
"description": "",
|
|
6
6
|
"type": "module",
|
|
@@ -34,19 +34,29 @@
|
|
|
34
34
|
"README.md"
|
|
35
35
|
],
|
|
36
36
|
"scripts": {
|
|
37
|
+
"dev": "tsc --watch",
|
|
37
38
|
"build": "tsc",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
39
|
+
"test": "vitest",
|
|
40
|
+
"test:run": "vitest run",
|
|
41
|
+
"test:list": "vitest list",
|
|
42
|
+
"test:log": "vitest run --reporter verbose",
|
|
43
|
+
"test:ui": "vitest --ui",
|
|
44
|
+
"prepublish": "tsc",
|
|
45
|
+
"publish": "tsc && bun publish"
|
|
40
46
|
},
|
|
41
47
|
"keywords": [
|
|
42
48
|
"utils",
|
|
43
49
|
"cn"
|
|
44
50
|
],
|
|
45
51
|
"license": "ISC",
|
|
52
|
+
"homepage": "https://github.com/sohanemon/utils",
|
|
53
|
+
"website": "https://sohanjs.web.app/utils",
|
|
46
54
|
"devDependencies": {
|
|
47
55
|
"@types/node": "^24.10.0",
|
|
48
56
|
"@types/react": "^19.2.2",
|
|
49
|
-
"
|
|
57
|
+
"@vitest/ui": "^4.0.14",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
59
|
+
"vitest": "^4.0.14"
|
|
50
60
|
},
|
|
51
61
|
"dependencies": {
|
|
52
62
|
"@iconify/react": "^6.0.2",
|