@sohanemon/utils 5.2.8 → 6.2.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/README.md CHANGED
@@ -3,22 +3,23 @@
3
3
  ![npm version](https://img.shields.io/npm/v/@sohanemon/utils)
4
4
  ![npm downloads](https://img.shields.io/npm/dm/@sohanemon/utils)
5
5
  ![License](https://img.shields.io/npm/l/@sohanemon/utils)
6
+ ![Tests](https://github.com/sohanemon/sohanemon-utils/actions/workflows/test.yml/badge.svg)
6
7
 
7
8
  ## Description
8
9
 
9
- `sohanemon/utils` is a collection of utility functions and hooks designed to simplify common tasks in modern web development. It includes utilities for handling objects, cookies, class name merging, and more. The library is built with TypeScript and is fully typed, ensuring a smooth and error-free development experience.
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 manipulate and access nested object properties.
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
- - **Responsive Media Queries**: Hooks to detect media queries based on Tailwind CSS breakpoints.
17
- - **Debounce and Throttle**: Utilities to control the rate of function execution.
18
- - **Copy to Clipboard**: A hook to copy text to the clipboard and track the copy status.
19
- - **Local Storage Management**: A hook to persist state in local storage.
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 hooks as needed:
42
+ You can import individual utilities, hooks, components, or types as needed:
42
43
 
43
- ```javascript
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
- ```javascript
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
- ```javascript
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
- #### Cookie Management
72
+ #### Data Transformation
67
73
 
68
- ```javascript
69
- import { setClientSideCookie, getClientSideCookie } from '@sohanemon/utils';
74
+ ```typescript
75
+ import { hydrate, convertToSlug, normalizeText } from '@sohanemon/utils';
70
76
 
71
- setClientSideCookie('username', 'sohanemon', 7);
72
- const { value } = getClientSideCookie('username'); // 'sohanemon'
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
- #### Responsive Media Queries
82
+ #### Async Operations
76
83
 
77
- ```javascript
78
- import { useMediaQuery } from '@sohanemon/utils';
84
+ ```typescript
85
+ import { poll, shield, sleep } from '@sohanemon/utils';
79
86
 
80
- const isMobile = useMediaQuery('sm');
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
- ```javascript
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
- #### Copy to Clipboard
93
-
94
- ```javascript
95
- import { useCopyToClipboard } from '@sohanemon/utils/hooks';
96
-
97
- const { isCopied, copy } = useCopyToClipboard();
113
+ #### React Hooks
98
114
 
99
- return (
100
- <div>
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
- #### Local Storage Management
118
+ const { data, isLoading } = useAsync(async (signal) => {
119
+ return await fetchData(signal);
120
+ }, { mode: 'auto' });
108
121
 
109
- ```javascript
110
- import { useLocalStorage } from '@sohanemon/utils/hooks';
122
+ const [value, setValue] = useLocalStorage('key', { count: 0 });
111
123
 
112
- const [value, setValue] = useLocalStorage('myKey', 'initialValue');
124
+ const isMobile = useMediaQuery('sm');
113
125
 
114
- return (
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
- #### URL Parameter Management
126
-
127
- ```javascript
128
- import { useUrlParams } from '@sohanemon/utils/hooks';
129
+ #### UI Components
129
130
 
130
- const [param, setParam] = useUrlParams('myParam', 'defaultValue');
131
+ ```tsx
132
+ import { HtmlInjector, ResponsiveIndicator, Iconify } from '@sohanemon/utils';
131
133
 
132
- return (
133
- <div>
134
- <input
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
- #### DOM Calculation
139
+ #### TypeScript Types
144
140
 
145
- ```javascript
146
- import { useDomCalculation } from '@sohanemon/utils/hooks';
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
- return (
155
- <div style={{ height, width }}>
156
- Content
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
- ### Class Name Merging
151
+ ### Functions
164
152
 
153
+ #### Class Name Merging
165
154
  ```typescript
166
155
  cn(...inputs: ClassValue[]): string
167
156
  ```
168
157
 
169
- ### Object Utilities
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
- ### Cookie Management
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
- setClientSideCookie(name: string, value: string, days?: number, path?: string): void
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
- ### Responsive Media Queries
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
- ```typescript
207
- useMediaQuery(tailwindBreakpoint: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | `(${string})`): boolean
208
- ```
201
+ convertToSlug(str: string): string
209
202
 
210
- ### Debounce and Throttle
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
- ### Copy to Clipboard
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
- useCopyToClipboard({ timeout?: number }): { isCopied: boolean; copy: (value: string) => void }
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
- ### Local Storage Management
286
+ ### React Hooks
233
287
 
288
+ #### State & Effects
234
289
  ```typescript
235
- useLocalStorage<T extends Record<string, any>>(key: string, defaultValue: T): LocalStorageValue<T>
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
- ### URL Parameter Management
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
- useUrlParams<T extends string | number | boolean>(key: string, defaultValue: T): [T, (value: T) => void]
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
- ### DOM Calculation
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
- useDomCalculation({
248
- blockIds: string[];
249
- dynamic?: boolean | string;
250
- margin?: number;
251
- substract?: boolean;
252
- onChange?: (results: {
253
- blocksHeight: number;
254
- blocksWidth: number;
255
- remainingWidth: number;
256
- remainingHeight: number;
257
- }) => void;
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,46 +1,15 @@
1
- import { DeepNullToUndefined } from '../types/utilities';
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;
2
4
  /**
3
- * Deeply cleans any value by converting all `null` values to `undefined`,
4
- * and merges in a fallback object for default values.
5
- *
6
- * Features:
7
- * - Circular reference detection to prevent infinite loops
8
- * - Proper handling of special objects (Date, Map, Set, RegExp, etc.)
9
- * - Strict type safety with improved generics
10
- * - Better error messages and validation
11
- * - Support for nested structures with Symbol properties
12
- * - Optional maximum recursion depth to prevent stack overflow
13
- * - Configurable null-to-undefined conversion
5
+ * Converts all `null` values to `undefined` in the data structure recursively.
14
6
  *
15
7
  * @param data - Any input data (object, array, primitive)
16
- * @param fallback - Optional fallback values to merge with
17
- * @param options - Configuration options
18
- * @param options.maxDepth - Maximum recursion depth (default: 100)
19
- * @param options.throwOnCircular - Whether to throw on circular refs (default: false)
20
- * @param options.convertNullToUndefined - Convert null to undefined (default: true)
21
8
  * @returns Same type as input, but with all nulls replaced by undefined
22
9
  *
23
- * @throws {TypeError} If data or fallback are invalid types when strict validation is enabled
24
- * @throws {RangeError} If circular reference detected and throwOnCircular is true
25
- *
26
10
  * @example
27
- * // Basic usage
28
11
  * hydrate({ a: null, b: 'test' }) // { a: undefined, b: 'test' }
29
- *
30
- * @example
31
- * // With fallback values
32
- * hydrate({ a: null }, { a: 'default' }) // { a: 'default' }
33
- *
34
- * @example
35
- * // Keep nulls as-is
36
- * hydrate({ a: null }, undefined, { convertNullToUndefined: false }) // { a: null }
37
- */
38
- export declare function hydrate<T>(data: T, fallback?: Partial<T>, options?: {
39
- maxDepth?: number;
40
- throwOnCircular?: boolean;
41
- convertNullToUndefined?: boolean;
42
- }): DeepNullToUndefined<T>;
43
- /**
44
- * 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 }]
45
13
  */
46
- export declare function isWithFallbackCompatible<T extends object>(value: unknown): value is T;
14
+ export declare function hydrate<T>(data: T): Hydrate<T>;
15
+ export {};
@@ -1,155 +1,31 @@
1
+ import { isPlainObject } from '../types';
1
2
  /**
2
- * Deeply cleans any value by converting all `null` values to `undefined`,
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, fallback, options) {
38
- const maxDepth = options?.maxDepth ?? 100;
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
- * Checks if a value is a plain object (not a special type)
105
- */
106
- function isPlainObject(value) {
107
- if (typeof value !== 'object' || value === null) {
108
- return false;
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
- // Objects with null prototype are still plain objects
114
- const proto = Object.getPrototypeOf(value);
115
- return proto === null || proto === Object.prototype;
116
- }
117
- /**
118
- * Checks if a value is a special object type that needs custom handling
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
- const stringTag = Object.prototype.toString.call(value);
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
  }
@@ -1,4 +1,5 @@
1
1
  export * from './cookie';
2
+ export * from './deepmerge';
2
3
  export * from './hydrate';
3
4
  export * from './object';
4
5
  export * from './poll';
@@ -1,4 +1,5 @@
1
1
  export * from './cookie';
2
+ export * from './deepmerge';
2
3
  export * from './hydrate';
3
4
  export * from './object';
4
5
  export * from './poll';
@@ -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
@@ -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>;
@@ -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
+ }
@@ -7,10 +7,7 @@ export type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T
7
7
  export type DeepRequired<T> = T extends Function ? T : T extends Array<infer U> ? Array<DeepRequired<U>> : T extends object ? {
8
8
  [K in keyof T]-?: DeepRequired<T[K]>;
9
9
  } : T;
10
- export type NullToUndefined<T> = T extends null ? undefined : T;
11
- export type DeepNullToUndefined<T> = T extends Function ? T : T extends Array<infer U> ? Array<DeepNullToUndefined<U>> : T extends object ? {
12
- [K in keyof T]: DeepNullToUndefined<NullToUndefined<T[K]>>;
13
- } : NullToUndefined<T>;
10
+ export type SelectiveRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
14
11
  export type Never<T> = {
15
12
  [K in keyof T]: never;
16
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sohanemon/utils",
3
- "version": "5.2.8",
3
+ "version": "6.2.0",
4
4
  "author": "Sohan Emon <sohanemon@outlook.com>",
5
5
  "description": "",
6
6
  "type": "module",
@@ -34,9 +34,15 @@
34
34
  "README.md"
35
35
  ],
36
36
  "scripts": {
37
+ "dev": "tsc --watch",
37
38
  "build": "tsc",
38
- "build:watch": "tsc --watch",
39
- "export": "tsc && npm publish"
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",
@@ -46,13 +52,14 @@
46
52
  "devDependencies": {
47
53
  "@types/node": "^24.10.0",
48
54
  "@types/react": "^19.2.2",
49
- "typescript": "^5.9.3"
55
+ "@vitest/ui": "^4.0.14",
56
+ "typescript": "^5.9.3",
57
+ "vitest": "^4.0.14"
50
58
  },
51
59
  "dependencies": {
52
60
  "@iconify/react": "^6.0.2",
53
61
  "clsx": "^2.1.1",
54
62
  "react": "^19.2.0",
55
63
  "tailwind-merge": "^3.3.1"
56
- },
57
- "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
64
+ }
58
65
  }