utilful 1.2.1 → 2.1.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.
Files changed (49) hide show
  1. package/README.md +240 -24
  2. package/dist/array.d.ts +7 -5
  3. package/dist/array.js +11 -0
  4. package/dist/csv.d.ts +57 -57
  5. package/dist/csv.js +98 -0
  6. package/dist/defu.d.ts +16 -0
  7. package/dist/defu.js +34 -0
  8. package/dist/emitter.d.ts +20 -14
  9. package/dist/emitter.js +32 -0
  10. package/dist/index.d.ts +12 -10
  11. package/dist/index.js +12 -0
  12. package/dist/json.d.ts +13 -11
  13. package/dist/json.js +34 -0
  14. package/dist/module.d.ts +9 -7
  15. package/dist/module.js +14 -0
  16. package/dist/object.d.ts +26 -7
  17. package/dist/object.js +51 -0
  18. package/dist/path.d.ts +21 -19
  19. package/dist/path.js +105 -0
  20. package/dist/result.d.ts +34 -0
  21. package/dist/result.js +45 -0
  22. package/dist/string.d.ts +16 -14
  23. package/dist/string.js +32 -0
  24. package/dist/types.d.ts +4 -4
  25. package/dist/types.js +0 -0
  26. package/package.json +23 -18
  27. package/dist/array.d.mts +0 -10
  28. package/dist/array.mjs +0 -6
  29. package/dist/csv.d.mts +0 -67
  30. package/dist/csv.mjs +0 -77
  31. package/dist/emitter.d.mts +0 -24
  32. package/dist/emitter.mjs +0 -63
  33. package/dist/index.d.mts +0 -10
  34. package/dist/index.mjs +0 -9
  35. package/dist/json.d.mts +0 -16
  36. package/dist/json.mjs +0 -26
  37. package/dist/lazy.d.mts +0 -20
  38. package/dist/lazy.d.ts +0 -20
  39. package/dist/lazy.mjs +0 -11
  40. package/dist/module.d.mts +0 -11
  41. package/dist/module.mjs +0 -6
  42. package/dist/object.d.mts +0 -14
  43. package/dist/object.mjs +0 -25
  44. package/dist/path.d.mts +0 -40
  45. package/dist/path.mjs +0 -113
  46. package/dist/string.d.mts +0 -19
  47. package/dist/string.mjs +0 -16
  48. package/dist/types.d.mts +0 -8
  49. package/dist/types.mjs +0 -1
package/README.md CHANGED
@@ -8,12 +8,13 @@ A collection of TypeScript utilities that I use across my projects.
8
8
  - [API](#api)
9
9
  - [Array](#array)
10
10
  - [CSV](#csv)
11
+ - [Defu](#defu)
11
12
  - [Emitter](#emitter)
12
13
  - [JSON](#json)
13
- - [Lazy](#lazy)
14
14
  - [Module](#module)
15
15
  - [Object](#object)
16
16
  - [Path](#path)
17
+ - [Result](#result)
17
18
  - [String](#string)
18
19
 
19
20
  ## Installation
@@ -111,6 +112,95 @@ Jane,25`
111
112
  const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
112
113
  ```
113
114
 
115
+ ### Defu
116
+
117
+ Recursively assign default properties. Simplified version based on [unjs/defu](https://github.com/unjs/defu).
118
+
119
+ #### `defu`
120
+
121
+ Recursively assigns missing properties from defaults to the source object. The source object takes precedence over defaults.
122
+
123
+ **Key Features:**
124
+
125
+ - **Null/undefined handling**: `null` and `undefined` values in source are replaced with defaults
126
+ - **Array concatenation**: Arrays are concatenated (source + defaults)
127
+ - **Deep merging**: Nested objects are recursively merged
128
+ - **Type safety**: Preserves TypeScript types
129
+ - **Prototype pollution protection**: Ignores `__proto__` and `constructor` keys
130
+
131
+ ```ts
132
+ type PlainObject = Record<PropertyKey, any>
133
+
134
+ declare function defu<T extends PlainObject>(
135
+ source: T,
136
+ ...defaults: PlainObject[]
137
+ ): T
138
+ ```
139
+
140
+ **Example:**
141
+
142
+ ```ts
143
+ import { defu } from 'utilful'
144
+
145
+ const result = defu(
146
+ { a: 1, b: { x: 1 } },
147
+ { a: 2, b: { y: 2 }, c: 3 }
148
+ )
149
+ // Result: { a: 1, b: { x: 1, y: 2 }, c: 3 }
150
+ ```
151
+
152
+ **Array concatenation example:**
153
+
154
+ ```ts
155
+ const result = defu(
156
+ { items: ['a', 'b'] },
157
+ { items: ['c', 'd'] }
158
+ )
159
+ // Result: { items: ['a', 'b', 'c', 'd'] }
160
+ ```
161
+
162
+ **Null/undefined handling:**
163
+
164
+ ```ts
165
+ const result = defu(
166
+ { name: null, age: undefined },
167
+ { name: 'John', age: 30, city: 'NYC' }
168
+ )
169
+ // Result: { name: 'John', age: 30, city: 'NYC' }
170
+ ```
171
+
172
+ #### `createDefu`
173
+
174
+ Creates a custom defu function with a custom merger.
175
+
176
+ ```ts
177
+ type DefuMerger<T extends PlainObject = PlainObject> = (
178
+ target: T,
179
+ key: PropertyKey,
180
+ value: any,
181
+ namespace: string,
182
+ ) => boolean | void
183
+
184
+ declare function createDefu(merger?: DefuMerger): DefuFn
185
+ ```
186
+
187
+ **Example:**
188
+
189
+ ```ts
190
+ import { createDefu } from 'utilful'
191
+
192
+ // Custom merger that adds numbers instead of replacing them
193
+ const addNumbers = createDefu((obj, key, val) => {
194
+ if (typeof val === 'number' && typeof obj[key] === 'number') {
195
+ obj[key] += val
196
+ return true // Indicates the merger handled this property
197
+ }
198
+ })
199
+
200
+ const result = addNumbers({ cost: 15 }, { cost: 10 })
201
+ // Result: { cost: 25 }
202
+ ```
203
+
114
204
  ### Emitter
115
205
 
116
206
  Tiny functional event emitter / pubsub, based on [mitt](https://github.com/developit/mitt).
@@ -168,29 +258,6 @@ Clones the given JSON value.
168
258
  declare function cloneJSON<T>(value: T): T
169
259
  ```
170
260
 
171
- ### Lazy
172
-
173
- A simple general purpose memoizer utility.
174
-
175
- - Lazily computes a value when accessed
176
- - Auto-caches the result by overwriting the getter
177
- - Typesafe
178
-
179
- Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invokation, since the getter itself is overwritten with the memoized value.
180
-
181
- ```ts
182
- declare function lazy<T>(getter: () => T): { value: T }
183
- ```
184
-
185
- **Example:**
186
-
187
- ```ts
188
- const myValue = lazy(() => 'Hello, World!')
189
- console.log(myValue.value) // Computes value, overwrites getter
190
- console.log(myValue.value) // Returns cached value
191
- console.log(myValue.value) // Returns cached value
192
- ```
193
-
194
261
  ### Module
195
262
 
196
263
  #### `interopDefault`
@@ -215,6 +282,28 @@ async function loadModule() {
215
282
 
216
283
  ### Object
217
284
 
285
+ #### `memoize`
286
+
287
+ A simple general purpose memoizer utility.
288
+
289
+ - Lazily computes a value when accessed
290
+ - Auto-caches the result by overwriting the getter
291
+
292
+ Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invokation, since the getter itself is overwritten with the memoized value.
293
+
294
+ ```ts
295
+ declare function memoize<T>(getter: () => T): { value: T }
296
+ ```
297
+
298
+ **Example:**
299
+
300
+ ```ts
301
+ const myValue = lazy(() => 'Hello, World!')
302
+ console.log(myValue.value) // Computes value, overwrites getter
303
+ console.log(myValue.value) // Returns cached value
304
+ console.log(myValue.value) // Returns cached value
305
+ ```
306
+
218
307
  #### `objectKeys`
219
308
 
220
309
  Strictly typed `Object.keys`.
@@ -327,6 +416,133 @@ const url = withQuery('https://example.com', {
327
416
  })
328
417
  ```
329
418
 
419
+ ### Result
420
+
421
+ The `Result` type that represents either success (`Ok`) or failure (`Err`). It helps to handle errors in a more explicit and type-safe way, without relying on exceptions.
422
+
423
+ A common use case for `Result` is error handling in functions that might fail. Here's an example of a function that divides two numbers and returns a `Result`:
424
+
425
+ ```ts
426
+ import { err, ok } from 'utilful'
427
+
428
+ function divide(a: number, b: number) {
429
+ if (b === 0) {
430
+ return err('Division by zero')
431
+ }
432
+ return ok(a / b)
433
+ }
434
+
435
+ const result = divide(10, 2)
436
+ if (result.ok)
437
+ console.log('Result:', result.value)
438
+ else
439
+ console.error('Error:', result.error)
440
+ ```
441
+
442
+ #### `Result`
443
+
444
+ The `Result` type represents either success (`Ok`) or failure (`Err`).
445
+
446
+ **Type Definition:**
447
+
448
+ ```ts
449
+ type Result<T, E> = Ok<T> | Err<E>
450
+ ```
451
+
452
+ #### `Ok`
453
+
454
+ The `Ok` type wraps a successful value.
455
+
456
+ **Example:**
457
+
458
+ ```ts
459
+ const result = new Ok(42)
460
+ ```
461
+
462
+ #### `Err`
463
+
464
+ The `Err` type wraps an error value.
465
+
466
+ **Example:**
467
+
468
+ ```ts
469
+ const result = new Err('Something went wrong')
470
+ ```
471
+
472
+ #### `ok`
473
+
474
+ Shorthand function to create an `Ok` result. Use it to wrap a successful value.
475
+
476
+ **Type Definition:**
477
+
478
+ ```ts
479
+ declare function ok<T>(value: T): Ok<T>
480
+ ```
481
+
482
+ #### `err`
483
+
484
+ Shorthand function to create an `Err` result. Use it to wrap an error value.
485
+
486
+ **Type Definition:**
487
+
488
+ ```ts
489
+ declare function err<E extends string = string>(err: E): Err<E>
490
+ declare function err<E = unknown>(err: E): Err<E>
491
+ ```
492
+
493
+ #### `toResult`
494
+
495
+ Wraps a function that might throw an error and returns a `Result` with the result of the function.
496
+
497
+ **Type Definition:**
498
+
499
+ ```ts
500
+ declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>
501
+ declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>
502
+ ```
503
+
504
+ #### `unwrapResult`
505
+
506
+ Unwraps a `Result`, `Ok`, or `Err` value and returns the value or error in an object. If the result is an `Ok`, the object contains the value and an `undefined` error. If the result is an `Err`, the object contains an `undefined` value and the error.
507
+
508
+ **Example:**
509
+
510
+ ```ts
511
+ const result = toResult(() => JSON.parse('{"foo":"bar"}'))
512
+ const { value, error } = unwrapResult(result)
513
+ ```
514
+
515
+ **Type Definition:**
516
+
517
+ ```ts
518
+ declare function unwrapResult<T>(result: Ok<T>): { value: T, error: undefined }
519
+ declare function unwrapResult<E>(result: Err<E>): { value: undefined, error: E }
520
+ declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }
521
+ ```
522
+
523
+ #### `tryCatch`
524
+
525
+ A simpler alternative to `toResult` + `unwrapResult`. It executes a function that might throw an error and directly returns the result in a `ResultData` format. Works with both synchronous functions and promises.
526
+
527
+ **Example:**
528
+
529
+ ```ts
530
+ import { tryCatch } from 'utilful'
531
+
532
+ // Synchronous usage
533
+ const { value, error } = tryCatch(() => JSON.parse('{"foo":"bar"}'))
534
+
535
+ // Asynchronous usage
536
+ const { value, error } = await tryCatch(fetch('https://api.example.com/data').then(r => r.json()))
537
+ ```
538
+
539
+ **Type Definition:**
540
+
541
+ ```ts
542
+ declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
543
+ declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>
544
+ ```
545
+
330
546
  ### String
331
547
 
332
548
  #### `template`
package/dist/array.d.ts CHANGED
@@ -1,10 +1,12 @@
1
+ //#region src/array.d.ts
1
2
  /**
2
- * Represents a value that can be either a single value or an array of values.
3
- */
3
+ * Represents a value that can be either a single value or an array of values.
4
+ */
4
5
  type MaybeArray<T> = T | T[];
5
6
  /**
6
- * Converts `MaybeArray<T>` to `Array<T>`.
7
- */
7
+ * Converts `MaybeArray<T>` to `Array<T>`.
8
+ */
8
9
  declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[];
9
10
 
10
- export { type MaybeArray, toArray };
11
+ //#endregion
12
+ export { MaybeArray, toArray };
package/dist/array.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region src/array.ts
2
+ /**
3
+ * Converts `MaybeArray<T>` to `Array<T>`.
4
+ */
5
+ function toArray(array) {
6
+ array ??= [];
7
+ return Array.isArray(array) ? array : [array];
8
+ }
9
+
10
+ //#endregion
11
+ export { toArray };
package/dist/csv.d.ts CHANGED
@@ -1,67 +1,67 @@
1
+ //#region src/csv.d.ts
1
2
  /**
2
- * Represents a row in a CSV file with column names of type T.
3
- */
3
+ * Represents a row in a CSV file with column names of type T.
4
+ */
4
5
  type CSVRow<T extends string = string> = Record<T, string>;
5
6
  /**
6
- * Converts an array of objects to a comma-separated values (CSV) string
7
- * that contains only the `columns` specified.
8
- *
9
- * @example
10
- * const data = [
11
- * { name: 'John', age: '30', city: 'New York' },
12
- * { name: 'Jane', age: '25', city: 'Boston' }
13
- * ]
14
- *
15
- * const csv = createCSV(data, ['name', 'age'])
16
- * // name,age
17
- * // John,30
18
- * // Jane,25
19
- */
7
+ * Converts an array of objects to a comma-separated values (CSV) string
8
+ * that contains only the `columns` specified.
9
+ *
10
+ * @example
11
+ * const data = [
12
+ * { name: 'John', age: '30', city: 'New York' },
13
+ * { name: 'Jane', age: '25', city: 'Boston' }
14
+ * ]
15
+ *
16
+ * const csv = createCSV(data, ['name', 'age'])
17
+ * // name,age
18
+ * // John,30
19
+ * // Jane,25
20
+ */
20
21
  declare function createCSV<T extends Record<string, unknown>>(data: T[], columns: (keyof T)[], options?: {
21
- /** @default ',' */
22
- delimiter?: string;
23
- /** @default true */
24
- addHeader?: boolean;
25
- /** @default false */
26
- quoteAll?: boolean;
22
+ /** @default ',' */
23
+ delimiter?: string;
24
+ /** @default true */
25
+ addHeader?: boolean;
26
+ /** @default false */
27
+ quoteAll?: boolean;
27
28
  }): string;
28
29
  /**
29
- * Escapes a value for a CSV string.
30
- *
31
- * @remarks
32
- * Returns an empty string if the value is `null` or `undefined`.
33
- * Values containing delimiters, quotes, or line breaks are quoted.
34
- * Within quoted values, double quotes are escaped by doubling them.
35
- *
36
- * @example
37
- * escapeCSVValue('hello, world'); // "hello, world"
38
- * escapeCSVValue('contains "quotes"'); // "contains ""quotes"""
39
- */
30
+ * Escapes a value for a CSV string.
31
+ *
32
+ * @remarks
33
+ * Returns an empty string if the value is `null` or `undefined`.
34
+ * Values containing delimiters, quotes, or line breaks are quoted.
35
+ * Within quoted values, double quotes are escaped by doubling them.
36
+ *
37
+ * @example
38
+ * escapeCSVValue('hello, world') // "hello, world"
39
+ * escapeCSVValue('contains "quotes"') // "contains ""quotes"""
40
+ */
40
41
  declare function escapeCSVValue(value: unknown, options?: {
41
- /** @default ',' */
42
- delimiter?: string;
43
- /** @default false */
44
- quoteAll?: boolean;
42
+ /** @default ',' */
43
+ delimiter?: string;
44
+ /** @default false */
45
+ quoteAll?: boolean;
45
46
  }): string;
46
47
  /**
47
- * Parses a comma-separated values (CSV) string into an array of objects.
48
- *
49
- * @remarks
50
- * The first row of the CSV string is used as the header row.
51
- *
52
- * @example
53
- * const csv = `name,age
54
- * John,30
55
- * Jane,25`
56
- *
57
- * const data = parseCSV<'name' | 'age'>(csv)
58
- * // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
59
- */
48
+ * Parses a comma-separated values (CSV) string into an array of objects.
49
+ *
50
+ * @remarks
51
+ * The first row of the CSV string is used as the header row.
52
+ *
53
+ * @example
54
+ * const csv = `name,age
55
+ * John,30
56
+ * Jane,25`
57
+ *
58
+ * const data = parseCSV<'name' | 'age'>(csv)
59
+ * // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
60
+ */
60
61
  declare function parseCSV<Header extends string>(csv?: string | null | undefined, options?: {
61
- /** @default ',' */
62
- delimiter?: string;
63
- /** @default true */
64
- trimValues?: boolean;
65
- }): CSVRow<Header>[];
66
-
67
- export { type CSVRow, createCSV, escapeCSVValue, parseCSV };
62
+ /** @default ',' */
63
+ delimiter?: string;
64
+ /** @default true */
65
+ trimValues?: boolean;
66
+ }): CSVRow<Header>[]; //#endregion
67
+ export { CSVRow, createCSV, escapeCSVValue, parseCSV };
package/dist/csv.js ADDED
@@ -0,0 +1,98 @@
1
+ //#region src/csv.ts
2
+ /**
3
+ * Converts an array of objects to a comma-separated values (CSV) string
4
+ * that contains only the `columns` specified.
5
+ *
6
+ * @example
7
+ * const data = [
8
+ * { name: 'John', age: '30', city: 'New York' },
9
+ * { name: 'Jane', age: '25', city: 'Boston' }
10
+ * ]
11
+ *
12
+ * const csv = createCSV(data, ['name', 'age'])
13
+ * // name,age
14
+ * // John,30
15
+ * // Jane,25
16
+ */
17
+ function createCSV(data, columns, options = {}) {
18
+ const { delimiter = ",", addHeader = true, quoteAll = false } = options;
19
+ const escapeAndQuote = (value) => escapeCSVValue(value, {
20
+ delimiter,
21
+ quoteAll
22
+ });
23
+ const rows = data.map((obj) => columns.map((key) => escapeAndQuote(obj[key])).join(delimiter));
24
+ if (addHeader) rows.unshift(columns.map(escapeAndQuote).join(delimiter));
25
+ return rows.join("\n");
26
+ }
27
+ /**
28
+ * Escapes a value for a CSV string.
29
+ *
30
+ * @remarks
31
+ * Returns an empty string if the value is `null` or `undefined`.
32
+ * Values containing delimiters, quotes, or line breaks are quoted.
33
+ * Within quoted values, double quotes are escaped by doubling them.
34
+ *
35
+ * @example
36
+ * escapeCSVValue('hello, world') // "hello, world"
37
+ * escapeCSVValue('contains "quotes"') // "contains ""quotes"""
38
+ */
39
+ function escapeCSVValue(value, options = {}) {
40
+ const { delimiter = ",", quoteAll = false } = options;
41
+ if (value == null) return "";
42
+ const stringValue = String(value);
43
+ const needsQuoting = quoteAll || stringValue.includes(delimiter) || stringValue.includes("\"") || stringValue.includes("\n") || stringValue.includes("\r");
44
+ if (needsQuoting) return `"${stringValue.replaceAll("\"", "\"\"")}"`;
45
+ return stringValue;
46
+ }
47
+ /**
48
+ * Parses a comma-separated values (CSV) string into an array of objects.
49
+ *
50
+ * @remarks
51
+ * The first row of the CSV string is used as the header row.
52
+ *
53
+ * @example
54
+ * const csv = `name,age
55
+ * John,30
56
+ * Jane,25`
57
+ *
58
+ * const data = parseCSV<'name' | 'age'>(csv)
59
+ * // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
60
+ */
61
+ function parseCSV(csv, options = {}) {
62
+ if (!csv?.trim()) return [];
63
+ const rows = [];
64
+ let currentRow = [];
65
+ let currentField = "";
66
+ let inQuotes = false;
67
+ const { delimiter = ",", trimValues = true } = options;
68
+ for (let i = 0; i < csv.length; i++) {
69
+ const char = csv[i];
70
+ const nextChar = i + 1 < csv.length ? csv[i + 1] : "";
71
+ if (char === "\"") if (inQuotes && nextChar === "\"") {
72
+ currentField += "\"";
73
+ i++;
74
+ } else inQuotes = !inQuotes;
75
+ else if (char === delimiter && !inQuotes) {
76
+ currentRow.push(trimValues ? currentField.trim() : currentField);
77
+ currentField = "";
78
+ } else if ((char === "\n" || char === "\r" && nextChar === "\n") && !inQuotes) {
79
+ if (char === "\r") i++;
80
+ currentRow.push(trimValues ? currentField.trim() : currentField);
81
+ rows.push(currentRow);
82
+ currentRow = [];
83
+ currentField = "";
84
+ } else currentField += char;
85
+ }
86
+ if (currentField || currentRow.length > 0) {
87
+ currentRow.push(trimValues ? currentField.trim() : currentField);
88
+ rows.push(currentRow);
89
+ }
90
+ if (rows.length <= 1) return [];
91
+ const headers = rows[0];
92
+ return rows.slice(1).filter((row) => row.some((field) => field.trim().length > 0)).map((values) => {
93
+ return Object.fromEntries(headers.map((header, index) => [header, index < values.length ? values[index] : ""]));
94
+ });
95
+ }
96
+
97
+ //#endregion
98
+ export { createCSV, escapeCSVValue, parseCSV };
package/dist/defu.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ //#region src/defu.d.ts
2
+ // Forked from unjs/defu (MIT)
3
+ type PlainObject = Record<PropertyKey, any>;
4
+ type DefuMerger<T extends PlainObject = PlainObject> = (target: T, key: PropertyKey, value: any, namespace: string) => boolean | void;
5
+ /**
6
+ * Defu function type that accepts a source and multiple defaults
7
+ */
8
+ type DefuFn = <T extends PlainObject>(source: T, ...defaults: PlainObject[]) => T;
9
+ /**
10
+ * Create a defu function with optional custom merger
11
+ */
12
+ declare function createDefu(merger?: DefuMerger): DefuFn;
13
+ declare const defu: DefuFn;
14
+
15
+ //#endregion
16
+ export { DefuFn, DefuMerger, createDefu as createDefu$1, defu as defu$1 };
package/dist/defu.js ADDED
@@ -0,0 +1,34 @@
1
+ //#region src/defu.ts
2
+ /**
3
+ * Create a defu function with optional custom merger
4
+ */
5
+ function createDefu(merger) {
6
+ return (source, ...defaults) => {
7
+ return defaults.reduce((acc, current) => _defu(acc, current ?? {}, "", merger), source ?? {});
8
+ };
9
+ }
10
+ const defu = createDefu();
11
+ function _defu(source, defaults, namespace = "", merger) {
12
+ if (!isPlainObject(defaults)) return source;
13
+ const result = { ...defaults };
14
+ for (const [key, value] of Object.entries(source)) {
15
+ if (key === "__proto__" || key === "constructor") continue;
16
+ if (value == null) continue;
17
+ if (merger?.(result, key, value, namespace)) continue;
18
+ const currentNamespace = namespace ? `${namespace}.${key}` : key;
19
+ if (Array.isArray(value) && Array.isArray(result[key])) result[key] = [...value, ...result[key]];
20
+ else if (isPlainObject(value) && isPlainObject(result[key])) result[key] = _defu(value, result[key], currentNamespace, merger);
21
+ else result[key] = value;
22
+ }
23
+ return result;
24
+ }
25
+ function isPlainObject(value) {
26
+ if (value === null || typeof value !== "object") return false;
27
+ const prototype = Object.getPrototypeOf(value);
28
+ if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) return false;
29
+ if (Symbol.iterator in value) return false;
30
+ return true;
31
+ }
32
+
33
+ //#endregion
34
+ export { createDefu, defu };
package/dist/emitter.d.ts CHANGED
@@ -1,24 +1,30 @@
1
+ //#region src/emitter.d.ts
2
+ /* eslint-disable ts/method-signature-style */
1
3
  type EventType = string | symbol;
4
+ // An event handler can take an optional event argument
5
+ // and should not return a value
2
6
  type Handler<T = unknown> = (event: T) => void;
3
7
  type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
4
8
  type EventHandlerList<T = unknown> = Handler<T>[];
5
9
  type WildCardEventHandlerList<T = Record<string, unknown>> = WildcardHandler<T>[];
6
- type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<keyof Events | '*', EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>>;
10
+ // A map of event types and their corresponding event handlers.
11
+ type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<keyof Events | "*", EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>>;
7
12
  interface Emitter<Events extends Record<EventType, unknown>> {
8
- events: EventHandlerMap<Events>;
9
- on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
10
- on(type: '*', handler: WildcardHandler<Events>): void;
11
- off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
12
- off(type: '*', handler: WildcardHandler<Events>): void;
13
- emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
14
- emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
13
+ events: EventHandlerMap<Events>;
14
+ on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
15
+ on(type: "*", handler: WildcardHandler<Events>): void;
16
+ off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
17
+ off(type: "*", handler: WildcardHandler<Events>): void;
18
+ emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
19
+ emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
15
20
  }
16
21
  /**
17
- * Simple functional event emitter / pubsub.
18
- *
19
- * @remarks Ported from `mitt`.
20
- * @see https://github.com/developit/mitt
21
- */
22
+ * Simple functional event emitter / pubsub.
23
+ *
24
+ * @remarks Ported from `mitt`.
25
+ * @see https://github.com/developit/mitt
26
+ */
22
27
  declare function createEmitter<Events extends Record<EventType, unknown>>(events?: EventHandlerMap<Events>): Emitter<Events>;
23
28
 
24
- export { type Emitter, type EventHandlerList, type EventHandlerMap, type EventType, type Handler, type WildCardEventHandlerList, type WildcardHandler, createEmitter };
29
+ //#endregion
30
+ export { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter };
@@ -0,0 +1,32 @@
1
+ //#region src/emitter.ts
2
+ /**
3
+ * Simple functional event emitter / pubsub.
4
+ *
5
+ * @remarks Ported from `mitt`.
6
+ * @see https://github.com/developit/mitt
7
+ */
8
+ function createEmitter(events) {
9
+ events ||= new Map();
10
+ return {
11
+ events,
12
+ on(type, handler) {
13
+ const handlers = events.get(type);
14
+ if (handlers) handlers.push(handler);
15
+ else events.set(type, [handler]);
16
+ },
17
+ off(type, handler) {
18
+ const handlers = events.get(type);
19
+ if (handlers) if (handler) handlers.splice(handlers.indexOf(handler) >>> 0, 1);
20
+ else events.set(type, []);
21
+ },
22
+ emit(type, evt) {
23
+ let handlers = events.get(type);
24
+ if (handlers) for (const handler of [...handlers]) handler(evt);
25
+ handlers = events.get("*");
26
+ if (handlers) for (const handler of [...handlers]) handler(type, evt);
27
+ }
28
+ };
29
+ }
30
+
31
+ //#endregion
32
+ export { createEmitter };