utilful 3.0.1 → 3.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.
- package/README.md +95 -26
- package/dist/array.d.mts +0 -6
- package/dist/array.mjs +0 -3
- package/dist/csv.d.mts +7 -16
- package/dist/csv.mjs +6 -6
- package/dist/defu.d.mts +15 -6
- package/dist/defu.mjs +0 -3
- package/dist/emitter.d.mts +4 -4
- package/dist/emitter.mjs +5 -11
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/json.d.mts +1 -1
- package/dist/json.mjs +1 -1
- package/dist/module.d.mts +1 -1
- package/dist/module.mjs +1 -1
- package/dist/object.d.mts +10 -7
- package/dist/object.mjs +9 -6
- package/dist/path.d.mts +21 -4
- package/dist/path.mjs +74 -14
- package/dist/result.d.mts +10 -12
- package/dist/result.mjs +15 -13
- package/dist/string.d.mts +1 -1
- package/dist/string.mjs +1 -1
- package/dist/types.d.mts +2 -4
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -21,13 +21,20 @@ A collection of TypeScript utilities that I use across my projects.
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
# npm
|
|
24
|
-
npm install
|
|
24
|
+
npm install utilful
|
|
25
25
|
|
|
26
26
|
# pnpm
|
|
27
|
-
pnpm add
|
|
27
|
+
pnpm add utilful
|
|
28
28
|
|
|
29
29
|
# yarn
|
|
30
|
-
yarn add
|
|
30
|
+
yarn add utilful
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Every module is also available on its own subpath, so you can import just the part you need:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { defu } from 'utilful' // Everything
|
|
37
|
+
import { joinURL } from 'utilful/path' // Just the path helpers
|
|
31
38
|
```
|
|
32
39
|
|
|
33
40
|
## API
|
|
@@ -113,12 +120,12 @@ interface CSVParseOptions {
|
|
|
113
120
|
/** @default ',' */
|
|
114
121
|
delimiter?: string
|
|
115
122
|
/**
|
|
116
|
-
*
|
|
123
|
+
* Whether to trim whitespace from unquoted headers and values.
|
|
117
124
|
* @default false
|
|
118
125
|
*/
|
|
119
126
|
trim?: boolean
|
|
120
127
|
/**
|
|
121
|
-
*
|
|
128
|
+
* Whether to throw if a row's field count does not match the header row.
|
|
122
129
|
* @default true
|
|
123
130
|
*/
|
|
124
131
|
strict?: boolean
|
|
@@ -130,7 +137,12 @@ declare function parseCSV<Header extends string>(
|
|
|
130
137
|
): CSVRow<Header>[]
|
|
131
138
|
```
|
|
132
139
|
|
|
133
|
-
The parser accepts a few lenient deviations from RFC 4180:
|
|
140
|
+
The parser accepts a few lenient deviations from RFC 4180:
|
|
141
|
+
|
|
142
|
+
- LF, CR, and CRLF line endings are all recognized
|
|
143
|
+
- whitespace between a closing quote and the next delimiter or line break is ignored
|
|
144
|
+
- quotes inside unquoted fields are kept as literal characters, since a field only counts as quoted if it starts with a quote
|
|
145
|
+
- text following a closing quote is appended to the field rather than rejected, so `"ab" cd` parses as `abcd`
|
|
134
146
|
|
|
135
147
|
**Example:**
|
|
136
148
|
|
|
@@ -148,6 +160,9 @@ const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { na
|
|
|
148
160
|
|
|
149
161
|
Creates a CSV stream from an iterable or async iterable of objects. Yields complete lines (header and/or data rows) including line endings – useful for large datasets that should not be buffered in memory.
|
|
150
162
|
|
|
163
|
+
> [!NOTE]
|
|
164
|
+
> Unlike `createCSV`, `columns` is required here. Inferring them would mean reading every row before writing the first one, which is exactly what streaming avoids.
|
|
165
|
+
|
|
151
166
|
```ts
|
|
152
167
|
declare function createCSVStream<T extends Record<string, unknown>>(
|
|
153
168
|
data: AsyncIterable<T> | Iterable<T>,
|
|
@@ -228,7 +243,7 @@ escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
|
|
|
228
243
|
|
|
229
244
|
### Defu
|
|
230
245
|
|
|
231
|
-
|
|
246
|
+
Fills in missing properties from a chain of defaults. A trimmed-down take on [unjs/defu](https://github.com/unjs/defu).
|
|
232
247
|
|
|
233
248
|
#### `defu`
|
|
234
249
|
|
|
@@ -286,7 +301,7 @@ const result = defu(
|
|
|
286
301
|
|
|
287
302
|
#### `createDefu`
|
|
288
303
|
|
|
289
|
-
Creates a
|
|
304
|
+
Creates a `defu` variant that hands every property to your own merger first. Return `true` to signal you handled it; return nothing to fall back to the default behavior.
|
|
290
305
|
|
|
291
306
|
```ts
|
|
292
307
|
type DefuMerger<T extends PlainObject = PlainObject> = (
|
|
@@ -325,8 +340,7 @@ Tiny functional event emitter / pubsub, based on [mitt](https://github.com/devel
|
|
|
325
340
|
```ts
|
|
326
341
|
import { createEmitter } from 'utilful'
|
|
327
342
|
|
|
328
|
-
|
|
329
|
-
type Events = {
|
|
343
|
+
interface Events {
|
|
330
344
|
foo: { a: string }
|
|
331
345
|
}
|
|
332
346
|
|
|
@@ -388,12 +402,9 @@ async function loadModule() {
|
|
|
388
402
|
|
|
389
403
|
#### `memoize`
|
|
390
404
|
|
|
391
|
-
|
|
405
|
+
Defers a computation until the value is first read, then caches it.
|
|
392
406
|
|
|
393
|
-
|
|
394
|
-
- Auto-caches the result by overwriting the getter
|
|
395
|
-
|
|
396
|
-
Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
|
|
407
|
+
Useful for expensive setup you may never need. Unlike a plain getter, there is no runtime cost after the first read, because the getter replaces itself with the computed value.
|
|
397
408
|
|
|
398
409
|
```ts
|
|
399
410
|
declare function memoize<T>(getter: () => T): { value: T }
|
|
@@ -426,15 +437,20 @@ declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof
|
|
|
426
437
|
|
|
427
438
|
#### `deepApply`
|
|
428
439
|
|
|
429
|
-
|
|
440
|
+
Applies a callback to every key-value pair of the given object, and to every pair inside nested objects and arrays (including arrays nested inside arrays).
|
|
441
|
+
|
|
442
|
+
The callback also fires for nested objects, so `item` is whichever object the pair belongs to rather than the one you passed in.
|
|
430
443
|
|
|
431
444
|
```ts
|
|
432
|
-
declare function deepApply<T extends Record<any, any>>(
|
|
445
|
+
declare function deepApply<T extends Record<any, any>>(
|
|
446
|
+
data: T,
|
|
447
|
+
callback: (item: Record<string, any>, key: string, value: any) => void
|
|
448
|
+
): void
|
|
433
449
|
```
|
|
434
450
|
|
|
435
451
|
#### `isObject`
|
|
436
452
|
|
|
437
|
-
Checks
|
|
453
|
+
Checks whether a value is an object. Object literals, class instances, and `null`-prototype objects all count; arrays, `Date`, `RegExp`, and `null` do not.
|
|
438
454
|
|
|
439
455
|
```ts
|
|
440
456
|
declare function isObject(value: unknown): value is Record<any, any>
|
|
@@ -442,7 +458,7 @@ declare function isObject(value: unknown): value is Record<any, any>
|
|
|
442
458
|
|
|
443
459
|
### Path
|
|
444
460
|
|
|
445
|
-
Utilities to build and normalize URL paths.
|
|
461
|
+
Utilities to build and normalize URL paths. They slice strings instead of parsing a full `URL`, which keeps them cheap enough for hot paths. Only `withQuery` and `getQuery` reach for `URLSearchParams`, where correct percent-encoding is worth the allocation.
|
|
446
462
|
|
|
447
463
|
#### `withoutLeadingSlash` / `withLeadingSlash`
|
|
448
464
|
|
|
@@ -478,7 +494,7 @@ joinURL('/api/', '/users', '42') // '/api/users/42'
|
|
|
478
494
|
|
|
479
495
|
#### `withBase` / `withoutBase`
|
|
480
496
|
|
|
481
|
-
Adds or removes a base path – each is a no-op if the base is already present (or absent).
|
|
497
|
+
Adds or removes a base path – each is a no-op if the base is already present (or absent). An absolute URL is returned as it is, since no base path can prefix it.
|
|
482
498
|
|
|
483
499
|
```ts
|
|
484
500
|
declare function withBase(input?: string, base?: string): string
|
|
@@ -489,20 +505,36 @@ declare function withoutBase(input?: string, base?: string): string
|
|
|
489
505
|
|
|
490
506
|
```ts
|
|
491
507
|
withBase('/users', '/api') // '/api/users'
|
|
508
|
+
withBase('https://example.com/users', '/api') // 'https://example.com/users'
|
|
492
509
|
withoutBase('/api/users', '/api') // '/users'
|
|
493
510
|
```
|
|
494
511
|
|
|
495
512
|
#### `getPathname`
|
|
496
513
|
|
|
497
|
-
Returns the pathname of the given path – everything before the query string or hash. Absolute URLs
|
|
514
|
+
Returns the pathname of the given path – everything before the query string or hash. Absolute URLs return the part after the host, whether they carry a scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
|
|
515
|
+
|
|
516
|
+
The pathname is sliced out as written and never normalized, so percent-encoding and `..` segments survive:
|
|
498
517
|
|
|
499
518
|
```ts
|
|
500
519
|
declare function getPathname(path?: string): string
|
|
501
520
|
```
|
|
502
521
|
|
|
522
|
+
```ts
|
|
523
|
+
getPathname('/foo?bar#baz') // '/foo'
|
|
524
|
+
getPathname('https://example.com/foo') // '/foo'
|
|
525
|
+
getPathname('//example.com/foo') // '/foo'
|
|
526
|
+
getPathname('https://example.com') // '/'
|
|
527
|
+
getPathname('https://example.com/a/../b') // '/a/../b' – use `new URL` if you need this resolved
|
|
528
|
+
```
|
|
529
|
+
|
|
503
530
|
#### `withQuery`
|
|
504
531
|
|
|
505
|
-
Returns the URL with the given query parameters merged in.
|
|
532
|
+
Returns the URL with the given query parameters merged in. A fragment stays where it belongs, at the very end.
|
|
533
|
+
|
|
534
|
+
- `undefined` removes the parameter
|
|
535
|
+
- `null` keeps the parameter with an empty value
|
|
536
|
+
- arrays append one entry per item, and empty arrays are skipped
|
|
537
|
+
- objects are JSON-stringified
|
|
506
538
|
|
|
507
539
|
```ts
|
|
508
540
|
type QueryValue = string | number | boolean | QueryValue[] | Record<string, any> | null | undefined
|
|
@@ -515,6 +547,25 @@ declare function withQuery(input: string, query?: QueryObject): string
|
|
|
515
547
|
|
|
516
548
|
```ts
|
|
517
549
|
withQuery('/api/users', { page: 2, tags: ['a', 'b'] }) // '/api/users?page=2&tags=a&tags=b'
|
|
550
|
+
withQuery('/api/users#list', { page: 2 }) // '/api/users?page=2#list'
|
|
551
|
+
withQuery('/api/users?page=2', { page: undefined }) // '/api/users'
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
#### `getQuery`
|
|
555
|
+
|
|
556
|
+
Reads the query parameters back out of a URL, ignoring the fragment. A parameter that appears more than once becomes an array of its values.
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
type ParsedQuery = Record<string, string | string[]>
|
|
560
|
+
|
|
561
|
+
declare function getQuery(input: string): ParsedQuery
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Example:**
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
getQuery('/api/users?page=2&tags=a&tags=b') // { page: '2', tags: ['a', 'b'] }
|
|
568
|
+
getQuery('/api/users') // {}
|
|
518
569
|
```
|
|
519
570
|
|
|
520
571
|
### Result
|
|
@@ -525,7 +576,7 @@ The `Result` type represents either success (`Ok`) or failure (`Err`). It provid
|
|
|
525
576
|
type Result<T, E> = Ok<T, E> | Err<T, E>
|
|
526
577
|
```
|
|
527
578
|
|
|
528
|
-
Both
|
|
579
|
+
Both variants carry the success *and* the error type, so the two stay in sync as you chain `map` and `mapError` calls.
|
|
529
580
|
|
|
530
581
|
**Basic example:**
|
|
531
582
|
|
|
@@ -616,7 +667,7 @@ Chains a function that returns a `Result`. Useful for composing fallible operati
|
|
|
616
667
|
|
|
617
668
|
```ts
|
|
618
669
|
ok(2).andThen(x => x > 0 ? ok(x) : err('negative')) // Ok(2)
|
|
619
|
-
err('fail').andThen(x => ok(x * 2)) // Err('fail')
|
|
670
|
+
err('fail').andThen(x => ok(x * 2)) // Err('fail') – short-circuits
|
|
620
671
|
```
|
|
621
672
|
|
|
622
673
|
#### `Result.unwrap`
|
|
@@ -629,6 +680,16 @@ err('fail').unwrap() // throws Error
|
|
|
629
680
|
err('fail').unwrap('custom message') // throws Error('custom message')
|
|
630
681
|
```
|
|
631
682
|
|
|
683
|
+
#### `Result.unwrapErr`
|
|
684
|
+
|
|
685
|
+
Extracts the error, or throws if the result is `Ok`. The mirror image of `unwrap`.
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
err('fail').unwrapErr() // 'fail'
|
|
689
|
+
ok(42).unwrapErr() // throws Error
|
|
690
|
+
ok(42).unwrapErr('custom message') // throws Error('custom message')
|
|
691
|
+
```
|
|
692
|
+
|
|
632
693
|
#### `Result.unwrapOr`
|
|
633
694
|
|
|
634
695
|
Extracts the value or returns a fallback.
|
|
@@ -690,6 +751,9 @@ declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undef
|
|
|
690
751
|
declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>
|
|
691
752
|
```
|
|
692
753
|
|
|
754
|
+
> [!NOTE]
|
|
755
|
+
> Like `toResult`, the function overload must be synchronous, and passing a function that returns a promise throws a `TypeError` rather than returning it as an error. Pass the promise itself.
|
|
756
|
+
|
|
693
757
|
**Example:**
|
|
694
758
|
|
|
695
759
|
```ts
|
|
@@ -704,7 +768,9 @@ const { value, error } = await tryCatch(fetch('https://api.example.com').then(r
|
|
|
704
768
|
|
|
705
769
|
#### `template`
|
|
706
770
|
|
|
707
|
-
|
|
771
|
+
Replaces `{name}` placeholders in a string with the matching variable.
|
|
772
|
+
|
|
773
|
+
A placeholder with no matching variable is left as its own key, unless you pass a `fallback` – either a fixed string or a function receiving the key. A variable that is present but `null` or `undefined` is treated the same as a missing one. Only own properties are read, so `{constructor}` cannot reach prototype members.
|
|
708
774
|
|
|
709
775
|
```ts
|
|
710
776
|
declare function template(
|
|
@@ -727,7 +793,10 @@ console.log(template(str, variables)) // Hello, world!
|
|
|
727
793
|
|
|
728
794
|
#### `generateRandomId`
|
|
729
795
|
|
|
730
|
-
Generates a random string.
|
|
796
|
+
Generates a random string. Ported from [`nanoid`](https://github.com/ai/nanoid). You can specify the length and the dictionary of characters to draw from.
|
|
797
|
+
|
|
798
|
+
> [!WARNING]
|
|
799
|
+
> Backed by `Math.random()` and therefore not cryptographically secure. Use `crypto.randomUUID()` or `crypto.getRandomValues()` for session tokens, password resets, and anything else an attacker would like to guess.
|
|
731
800
|
|
|
732
801
|
```ts
|
|
733
802
|
declare function generateRandomId(size?: number, dict?: string): string
|
package/dist/array.d.mts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
//#region src/array.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Represents a value that can be either a single value or an array of values.
|
|
4
|
-
*/
|
|
5
2
|
type MaybeArray<T> = T | T[];
|
|
6
|
-
/**
|
|
7
|
-
* Converts `MaybeArray<T>` to `Array<T>`.
|
|
8
|
-
*/
|
|
9
3
|
declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[];
|
|
10
4
|
//#endregion
|
|
11
5
|
export { MaybeArray, toArray };
|
package/dist/array.mjs
CHANGED
package/dist/csv.d.mts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
//#region src/csv.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Represents a row in a CSV file with column names of type T.
|
|
4
|
-
*/
|
|
5
2
|
type CSVRow<T extends string = string> = Record<T, string>;
|
|
6
|
-
/**
|
|
7
|
-
* Options for the CSV creation functions.
|
|
8
|
-
*/
|
|
9
3
|
interface CSVCreateOptions {
|
|
10
4
|
/** @default ',' */
|
|
11
5
|
delimiter?: string;
|
|
@@ -16,19 +10,16 @@ interface CSVCreateOptions {
|
|
|
16
10
|
/** @default '\n' */
|
|
17
11
|
lineEnding?: string;
|
|
18
12
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Options for the CSV parsing functions.
|
|
21
|
-
*/
|
|
22
13
|
interface CSVParseOptions {
|
|
23
14
|
/** @default ',' */
|
|
24
15
|
delimiter?: string;
|
|
25
16
|
/**
|
|
26
|
-
*
|
|
17
|
+
* Whether to trim whitespace from unquoted headers and values.
|
|
27
18
|
* @default false
|
|
28
19
|
*/
|
|
29
20
|
trim?: boolean;
|
|
30
21
|
/**
|
|
31
|
-
*
|
|
22
|
+
* Whether to throw if a row's field count does not match the header row.
|
|
32
23
|
* @default true
|
|
33
24
|
*/
|
|
34
25
|
strict?: boolean;
|
|
@@ -115,8 +106,8 @@ declare function createCSVAsync<T extends Record<string, unknown>>(data: AsyncIt
|
|
|
115
106
|
* Within quoted values, double quotes are escaped by doubling them.
|
|
116
107
|
*
|
|
117
108
|
* @example
|
|
118
|
-
* escapeCSVValue('hello, world') // "hello, world"
|
|
119
|
-
* escapeCSVValue('contains "quotes"') // "contains ""quotes"""
|
|
109
|
+
* escapeCSVValue('hello, world') // '"hello, world"'
|
|
110
|
+
* escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
|
|
120
111
|
*/
|
|
121
112
|
declare function escapeCSVValue(value: unknown, options?: {
|
|
122
113
|
/** @default ',' */
|
|
@@ -132,10 +123,10 @@ declare function escapeCSVValue(value: unknown, options?: {
|
|
|
132
123
|
* UTF-8 byte order mark is stripped.
|
|
133
124
|
*
|
|
134
125
|
* Parsing tolerances (lenient deviations from RFC 4180):
|
|
135
|
-
* - LF, CR, and CRLF line endings are all accepted
|
|
136
|
-
* - Whitespace between a closing quote and the next delimiter or line break is ignored
|
|
126
|
+
* - LF, CR, and CRLF line endings are all accepted.
|
|
127
|
+
* - Whitespace between a closing quote and the next delimiter or line break is ignored.
|
|
137
128
|
* - A field is only treated as quoted if it starts with a quote; quotes inside
|
|
138
|
-
* unquoted fields are kept as literal characters
|
|
129
|
+
* unquoted fields are kept as literal characters.
|
|
139
130
|
*
|
|
140
131
|
* @example
|
|
141
132
|
* const csv = `name,age
|
package/dist/csv.mjs
CHANGED
|
@@ -17,9 +17,9 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
|
17
17
|
columns = inferColumns(data);
|
|
18
18
|
options = columnsOrOptions ?? {};
|
|
19
19
|
}
|
|
20
|
-
if (columns.length === 0 && data.length === 0) return "";
|
|
21
20
|
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
22
21
|
assertValidCSVDelimiter(delimiter);
|
|
22
|
+
if (columns.length === 0) return "";
|
|
23
23
|
if (addHeader) {
|
|
24
24
|
const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
|
|
25
25
|
if (data.length === 0) return header;
|
|
@@ -97,8 +97,8 @@ function encodeCSVRow(row, columns, delimiter, quoteAll) {
|
|
|
97
97
|
* Within quoted values, double quotes are escaped by doubling them.
|
|
98
98
|
*
|
|
99
99
|
* @example
|
|
100
|
-
* escapeCSVValue('hello, world') // "hello, world"
|
|
101
|
-
* escapeCSVValue('contains "quotes"') // "contains ""quotes"""
|
|
100
|
+
* escapeCSVValue('hello, world') // '"hello, world"'
|
|
101
|
+
* escapeCSVValue('contains "quotes"') // '"contains ""quotes"""'
|
|
102
102
|
*/
|
|
103
103
|
function escapeCSVValue(value, options = {}) {
|
|
104
104
|
const { delimiter = COMMA, quoteAll = false } = options;
|
|
@@ -211,10 +211,10 @@ var CSVParserCore = class {
|
|
|
211
211
|
* UTF-8 byte order mark is stripped.
|
|
212
212
|
*
|
|
213
213
|
* Parsing tolerances (lenient deviations from RFC 4180):
|
|
214
|
-
* - LF, CR, and CRLF line endings are all accepted
|
|
215
|
-
* - Whitespace between a closing quote and the next delimiter or line break is ignored
|
|
214
|
+
* - LF, CR, and CRLF line endings are all accepted.
|
|
215
|
+
* - Whitespace between a closing quote and the next delimiter or line break is ignored.
|
|
216
216
|
* - A field is only treated as quoted if it starts with a quote; quotes inside
|
|
217
|
-
* unquoted fields are kept as literal characters
|
|
217
|
+
* unquoted fields are kept as literal characters.
|
|
218
218
|
*
|
|
219
219
|
* @example
|
|
220
220
|
* const csv = `name,age
|
package/dist/defu.d.mts
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
//#region src/defu.d.ts
|
|
2
2
|
type PlainObject = Record<PropertyKey, any>;
|
|
3
3
|
type DefuMerger<T extends PlainObject = PlainObject> = (target: T, key: PropertyKey, value: any, namespace: string) => boolean | void;
|
|
4
|
+
type Nullish = null | undefined | void;
|
|
5
|
+
/**
|
|
6
|
+
* Values that `isPlainObject` rejects at runtime, so the merged type keeps them
|
|
7
|
+
* intact rather than recursing into them. `PlainObject` cannot draw this line
|
|
8
|
+
* itself: its `any` value type makes every object type satisfy it, classes and
|
|
9
|
+
* built-ins included.
|
|
10
|
+
*/
|
|
11
|
+
type NonPlainObject = ((...args: any[]) => any) | {
|
|
12
|
+
[Symbol.iterator]: any;
|
|
13
|
+
} | Date | RegExp | Promise<any> | Error | WeakMap<object, any> | WeakSet<object>;
|
|
4
14
|
/**
|
|
5
15
|
* Deeply merged result type of a source object over a list of defaults.
|
|
6
16
|
*/
|
|
7
17
|
type Defu<Source, Defaults extends any[]> = Defaults extends [infer First, ...infer Rest] ? Defu<MergedObject<Source, First>, Rest> : Source;
|
|
8
|
-
type MergedObject<Source, Defaults> = Source extends PlainObject ? Defaults extends PlainObject ? { [Key in keyof Source | keyof Defaults]: MergedValue<Key extends keyof Source ? Source[Key] : undefined, Key extends keyof Defaults ? Defaults[Key] : undefined>; } : Source : Source;
|
|
9
|
-
type MergedValue<SourceValue, DefaultValue> = SourceValue extends null | undefined ? DefaultValue : SourceValue extends any[] ? DefaultValue extends any[] ? Array<SourceValue[number] | DefaultValue[number]> : SourceValue : SourceValue extends ((...args: any[]) => any) ? SourceValue : SourceValue extends PlainObject ? MergedObject<SourceValue, DefaultValue> : SourceValue;
|
|
10
18
|
/**
|
|
11
|
-
*
|
|
19
|
+
* Source merged over one defaults object, rebuilding only the shared keys.
|
|
20
|
+
* Everything else passes through `Omit`, which is what keeps optionality,
|
|
21
|
+
* `readonly` and the nominal identity of classes intact.
|
|
12
22
|
*/
|
|
23
|
+
type MergedObject<Source, Defaults> = Source extends PlainObject ? Defaults extends PlainObject ? Source extends Defaults ? Source : Omit<Source, keyof Source & keyof Defaults> & Omit<Defaults, keyof Source & keyof Defaults> & { -readonly [Key in keyof Source & keyof Defaults]: MergedValue<Source[Key], Defaults[Key]>; } : Source : Source;
|
|
24
|
+
type MergedValue<SourceValue, DefaultValue> = SourceValue extends Nullish ? DefaultValue : DefaultValue extends Nullish ? SourceValue : SourceValue extends readonly any[] ? DefaultValue extends readonly any[] ? Array<SourceValue[number] | DefaultValue[number]> : SourceValue : SourceValue extends NonPlainObject ? SourceValue : DefaultValue extends NonPlainObject ? SourceValue : MergedObject<SourceValue, DefaultValue>;
|
|
13
25
|
type DefuFn = <Source extends PlainObject, Defaults extends PlainObject[]>(source: Source, ...defaults: Defaults) => Defu<Source, Defaults>;
|
|
14
|
-
/**
|
|
15
|
-
* Create a defu function with optional custom merger
|
|
16
|
-
*/
|
|
17
26
|
declare function createDefu(merger?: DefuMerger): DefuFn;
|
|
18
27
|
declare const defu: DefuFn;
|
|
19
28
|
//#endregion
|
package/dist/defu.mjs
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
//#region src/defu.ts
|
|
2
|
-
/**
|
|
3
|
-
* Create a defu function with optional custom merger
|
|
4
|
-
*/
|
|
5
2
|
function createDefu(merger) {
|
|
6
3
|
return ((source, ...defaults) => {
|
|
7
4
|
return defaults.reduce((mergedResult, currentDefaults) => _defu(mergedResult, currentDefaults ?? {}, "", merger), source ?? {});
|
package/dist/emitter.d.mts
CHANGED
|
@@ -4,8 +4,8 @@ type Handler<T = unknown> = (event: T) => void;
|
|
|
4
4
|
type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
|
|
5
5
|
type EventHandlerList<T = unknown> = Handler<T>[];
|
|
6
6
|
type WildCardEventHandlerList<T = Record<string, unknown>> = WildcardHandler<T>[];
|
|
7
|
-
type EventHandlerMap<Events extends Record<EventType,
|
|
8
|
-
interface Emitter<Events extends Record<EventType,
|
|
7
|
+
type EventHandlerMap<Events extends Record<EventType, any>> = Map<keyof Events | "*", EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>>;
|
|
8
|
+
interface Emitter<Events extends Record<EventType, any>> {
|
|
9
9
|
events: EventHandlerMap<Events>;
|
|
10
10
|
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
|
|
11
11
|
on(type: "*", handler: WildcardHandler<Events>): void;
|
|
@@ -15,11 +15,11 @@ interface Emitter<Events extends Record<EventType, unknown>> {
|
|
|
15
15
|
emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Creates a functional pubsub event emitter.
|
|
19
19
|
*
|
|
20
20
|
* @remarks Ported from `mitt`.
|
|
21
21
|
* @see https://github.com/developit/mitt
|
|
22
22
|
*/
|
|
23
|
-
declare function createEmitter<Events extends Record<EventType,
|
|
23
|
+
declare function createEmitter<Events extends Record<EventType, any>>(events?: EventHandlerMap<Events>): Emitter<Events>;
|
|
24
24
|
//#endregion
|
|
25
25
|
export { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter };
|
package/dist/emitter.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/emitter.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Creates a functional pubsub event emitter.
|
|
4
4
|
*
|
|
5
5
|
* @remarks Ported from `mitt`.
|
|
6
6
|
* @see https://github.com/developit/mitt
|
|
@@ -13,9 +13,7 @@ function createEmitter(events) {
|
|
|
13
13
|
*/
|
|
14
14
|
events,
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @memberOf createEmitter
|
|
16
|
+
* Registers an event handler for the given type.
|
|
19
17
|
*/
|
|
20
18
|
on(type, handler) {
|
|
21
19
|
const handlers = events.get(type);
|
|
@@ -23,12 +21,10 @@ function createEmitter(events) {
|
|
|
23
21
|
else events.set(type, [handler]);
|
|
24
22
|
},
|
|
25
23
|
/**
|
|
26
|
-
*
|
|
24
|
+
* Removes an event handler for the given type.
|
|
27
25
|
*
|
|
28
26
|
* @remarks
|
|
29
27
|
* If `handler` is omitted, all handlers of the given type are removed.
|
|
30
|
-
*
|
|
31
|
-
* @memberOf createEmitter
|
|
32
28
|
*/
|
|
33
29
|
off(type, handler) {
|
|
34
30
|
const handlers = events.get(type);
|
|
@@ -36,13 +32,11 @@ function createEmitter(events) {
|
|
|
36
32
|
else events.set(type, []);
|
|
37
33
|
},
|
|
38
34
|
/**
|
|
39
|
-
*
|
|
35
|
+
* Invokes all handlers for the given type.
|
|
40
36
|
*
|
|
41
37
|
* @remarks
|
|
42
38
|
* If present, `'*'` handlers are invoked after type-matched handlers.
|
|
43
|
-
* Manually firing '*' handlers is not supported.
|
|
44
|
-
*
|
|
45
|
-
* @memberOf createEmitter
|
|
39
|
+
* Manually firing `'*'` handlers is not supported.
|
|
46
40
|
*/
|
|
47
41
|
emit(type, evt) {
|
|
48
42
|
let handlers = events.get(type);
|
package/dist/index.d.mts
CHANGED
|
@@ -5,8 +5,8 @@ import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCar
|
|
|
5
5
|
import { tryParseJSON } from "./json.mjs";
|
|
6
6
|
import { interopDefault } from "./module.mjs";
|
|
7
7
|
import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
|
|
8
|
-
import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
8
|
+
import { ParsedQuery, QueryObject, QueryValue, getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
9
9
|
import { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
|
|
11
|
-
import {
|
|
12
|
-
export {
|
|
11
|
+
import { BrandedType, UnifyIntersection } from "./types.mjs";
|
|
12
|
+
export { BrandedType, CSVCreateOptions, CSVParseOptions, CSVRow, Defu, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, MaybeArray, Ok, OkData, ParsedQuery, QueryObject, QueryValue, Result, ResultData, TEMPLATE_PLACEHOLDER_RE, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, getQuery, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { createEmitter } from "./emitter.mjs";
|
|
|
5
5
|
import { tryParseJSON } from "./json.mjs";
|
|
6
6
|
import { interopDefault } from "./module.mjs";
|
|
7
7
|
import { deepApply, isObject, memoize, objectEntries, objectKeys } from "./object.mjs";
|
|
8
|
-
import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
8
|
+
import { getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
9
9
|
import { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { TEMPLATE_PLACEHOLDER_RE, generateRandomId, template } from "./string.mjs";
|
|
11
|
-
export { Err, Ok, TEMPLATE_PLACEHOLDER_RE, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
11
|
+
export { Err, Ok, TEMPLATE_PLACEHOLDER_RE, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, getQuery, interopDefault, isErr, isObject, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/json.d.mts
CHANGED
package/dist/json.mjs
CHANGED
package/dist/module.d.mts
CHANGED
package/dist/module.mjs
CHANGED
package/dist/object.d.mts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
//#region src/object.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* - Lazily computes a value when accessed
|
|
5
|
-
* - Auto-caches the result by overwriting the getter
|
|
3
|
+
* Wraps a getter so its value is computed on first access and cached from then on.
|
|
6
4
|
*
|
|
7
5
|
* @remarks
|
|
8
6
|
* Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
|
|
@@ -17,17 +15,22 @@ declare function memoize<T>(getter: () => T): {
|
|
|
17
15
|
value: T;
|
|
18
16
|
};
|
|
19
17
|
/**
|
|
20
|
-
*
|
|
18
|
+
* Wraps `Object.keys` with a stricter return type.
|
|
21
19
|
*/
|
|
22
20
|
declare function objectKeys<T extends Record<any, any>>(obj: T): Array<`${Extract<keyof T, string | number>}`>;
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
22
|
+
* Wraps `Object.entries` with a stricter return type.
|
|
25
23
|
*/
|
|
26
24
|
declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof T, T[keyof T]]>;
|
|
27
25
|
/**
|
|
28
|
-
*
|
|
26
|
+
* Applies a callback to every key-value pair of the given object, and to every pair
|
|
27
|
+
* inside nested objects and arrays (including arrays nested inside arrays).
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* The callback also fires for nested objects, so `item` is whichever object the pair
|
|
31
|
+
* belongs to rather than the one that was passed in.
|
|
29
32
|
*/
|
|
30
|
-
declare function deepApply<T extends Record<any, any>>(data: T, callback: (item:
|
|
33
|
+
declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: Record<string, any>, key: string, value: any) => void): void;
|
|
31
34
|
/**
|
|
32
35
|
* Checks if a value is an object with the plain `[object Object]` tag.
|
|
33
36
|
*
|
package/dist/object.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
//#region src/object.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* - Lazily computes a value when accessed
|
|
5
|
-
* - Auto-caches the result by overwriting the getter
|
|
3
|
+
* Wraps a getter so its value is computed on first access and cached from then on.
|
|
6
4
|
*
|
|
7
5
|
* @remarks
|
|
8
6
|
* Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invocation, since the getter itself is overwritten with the memoized value.
|
|
@@ -21,19 +19,24 @@ function memoize(getter) {
|
|
|
21
19
|
} };
|
|
22
20
|
}
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
22
|
+
* Wraps `Object.keys` with a stricter return type.
|
|
25
23
|
*/
|
|
26
24
|
function objectKeys(obj) {
|
|
27
25
|
return Object.keys(obj);
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
30
|
-
*
|
|
28
|
+
* Wraps `Object.entries` with a stricter return type.
|
|
31
29
|
*/
|
|
32
30
|
function objectEntries(obj) {
|
|
33
31
|
return Object.entries(obj);
|
|
34
32
|
}
|
|
35
33
|
/**
|
|
36
|
-
*
|
|
34
|
+
* Applies a callback to every key-value pair of the given object, and to every pair
|
|
35
|
+
* inside nested objects and arrays (including arrays nested inside arrays).
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* The callback also fires for nested objects, so `item` is whichever object the pair
|
|
39
|
+
* belongs to rather than the one that was passed in.
|
|
37
40
|
*/
|
|
38
41
|
function deepApply(data, callback) {
|
|
39
42
|
for (const [key, value] of Object.entries(data)) {
|
package/dist/path.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//#region src/path.d.ts
|
|
2
2
|
type QueryValue = string | number | boolean | QueryValue[] | Record<string, any> | null | undefined;
|
|
3
3
|
type QueryObject = Record<string, QueryValue | QueryValue[]>;
|
|
4
|
+
/** Query parameters as read back from a URL, where a repeated key holds all its values. */
|
|
5
|
+
type ParsedQuery = Record<string, string | string[]>;
|
|
4
6
|
/**
|
|
5
7
|
* Removes the leading slash from the given path if it has one.
|
|
6
8
|
*/
|
|
@@ -25,6 +27,11 @@ declare function withTrailingSlash(path?: string): string;
|
|
|
25
27
|
declare function joinURL(...paths: (string | undefined)[]): string;
|
|
26
28
|
/**
|
|
27
29
|
* Adds the base path to the input path, if it is not already present.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* An absolute URL is returned as it is, since a base path cannot prefix one –
|
|
33
|
+
* whether it carries a scheme (`https://example.com/foo`) or is protocol-relative
|
|
34
|
+
* (`//example.com/foo`).
|
|
28
35
|
*/
|
|
29
36
|
declare function withBase(input?: string, base?: string): string;
|
|
30
37
|
/**
|
|
@@ -35,13 +42,23 @@ declare function withoutBase(input?: string, base?: string): string;
|
|
|
35
42
|
* Returns the pathname of the given path, which is the path without the query string or hash.
|
|
36
43
|
*
|
|
37
44
|
* @remarks
|
|
38
|
-
* Absolute URLs
|
|
39
|
-
*
|
|
45
|
+
* Absolute URLs return the segment after the authority, whether they carry a
|
|
46
|
+
* scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
|
|
47
|
+
* The result is sliced out verbatim, never normalized, so percent-encoding and
|
|
48
|
+
* `..` segments survive as written.
|
|
40
49
|
*/
|
|
41
50
|
declare function getPathname(path?: string): string;
|
|
42
51
|
/**
|
|
43
|
-
* Returns the URL with the given query parameters. If a query parameter is undefined
|
|
52
|
+
* Returns the URL with the given query parameters. If a query parameter is `undefined`, it is omitted.
|
|
44
53
|
*/
|
|
45
54
|
declare function withQuery(input: string, query?: QueryObject): string;
|
|
55
|
+
/**
|
|
56
|
+
* Reads the query parameters of the given URL, ignoring the fragment.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* A parameter that appears more than once becomes an array of its values,
|
|
60
|
+
* in the order they appear. Values are percent-decoded.
|
|
61
|
+
*/
|
|
62
|
+
declare function getQuery(input: string): ParsedQuery;
|
|
46
63
|
//#endregion
|
|
47
|
-
export { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
64
|
+
export { ParsedQuery, QueryObject, QueryValue, getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/path.mjs
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
//#region src/path.ts
|
|
2
|
+
const URL_SCHEME_RE = /^[a-z][\w+.-]*:\/\//i;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the index at which the authority of an absolute URL starts, or `-1`
|
|
5
|
+
* for a path that carries none. A protocol-relative input such as
|
|
6
|
+
* `//example.com/foo` counts as absolute: its authority follows the leading `//`.
|
|
7
|
+
*/
|
|
8
|
+
function getAuthorityStart(path) {
|
|
9
|
+
if (path.startsWith("//")) return 2;
|
|
10
|
+
return URL_SCHEME_RE.test(path) ? path.indexOf("://") + 3 : -1;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Removes the leading slash from the given path if it has one.
|
|
4
14
|
*/
|
|
@@ -66,9 +76,14 @@ function joinURL(...paths) {
|
|
|
66
76
|
}
|
|
67
77
|
/**
|
|
68
78
|
* Adds the base path to the input path, if it is not already present.
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* An absolute URL is returned as it is, since a base path cannot prefix one –
|
|
82
|
+
* whether it carries a scheme (`https://example.com/foo`) or is protocol-relative
|
|
83
|
+
* (`//example.com/foo`).
|
|
69
84
|
*/
|
|
70
85
|
function withBase(input = "", base = "") {
|
|
71
|
-
if (!base || base === "/") return input;
|
|
86
|
+
if (!base || base === "/" || getAuthorityStart(input) !== -1) return input;
|
|
72
87
|
const _base = withoutTrailingSlash(base);
|
|
73
88
|
if (input.startsWith(_base) && (input.length === _base.length || input[_base.length] === "/" || input[_base.length] === "?" || input[_base.length] === "#")) return input;
|
|
74
89
|
return joinURL(_base, input);
|
|
@@ -83,32 +98,47 @@ function withoutBase(input = "", base = "") {
|
|
|
83
98
|
const trimmed = input.slice(_base.length);
|
|
84
99
|
return trimmed[0] === "/" ? trimmed : `/${trimmed}`;
|
|
85
100
|
}
|
|
86
|
-
const URL_SCHEME_RE = /^[a-z][\w+.-]*:\/\//i;
|
|
87
101
|
/**
|
|
88
102
|
* Returns the pathname of the given path, which is the path without the query string or hash.
|
|
89
103
|
*
|
|
90
104
|
* @remarks
|
|
91
|
-
* Absolute URLs
|
|
92
|
-
*
|
|
105
|
+
* Absolute URLs return the segment after the authority, whether they carry a
|
|
106
|
+
* scheme (`https://example.com/foo`) or are protocol-relative (`//example.com/foo`).
|
|
107
|
+
* The result is sliced out verbatim, never normalized, so percent-encoding and
|
|
108
|
+
* `..` segments survive as written.
|
|
93
109
|
*/
|
|
94
110
|
function getPathname(path = "/") {
|
|
95
|
-
|
|
111
|
+
let pathStart = 0;
|
|
112
|
+
const authorityStart = getAuthorityStart(path);
|
|
113
|
+
if (authorityStart !== -1) {
|
|
114
|
+
let authorityEnd = path.length;
|
|
115
|
+
for (let i = authorityStart; i < path.length; i++) {
|
|
116
|
+
const character = path[i];
|
|
117
|
+
if (character === "/" || character === "?" || character === "#") {
|
|
118
|
+
authorityEnd = i;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (path[authorityEnd] !== "/") return "/";
|
|
123
|
+
pathStart = authorityEnd;
|
|
124
|
+
}
|
|
96
125
|
let pathEnd = path.length;
|
|
97
|
-
const queryIndex = path.indexOf("?");
|
|
98
|
-
const hashIndex = path.indexOf("#");
|
|
126
|
+
const queryIndex = path.indexOf("?", pathStart);
|
|
127
|
+
const hashIndex = path.indexOf("#", pathStart);
|
|
99
128
|
if (queryIndex !== -1) pathEnd = queryIndex;
|
|
100
129
|
if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
|
|
101
|
-
return path.slice(
|
|
130
|
+
return path.slice(pathStart, pathEnd) || "/";
|
|
102
131
|
}
|
|
103
132
|
/**
|
|
104
|
-
* Returns the URL with the given query parameters. If a query parameter is undefined
|
|
133
|
+
* Returns the URL with the given query parameters. If a query parameter is `undefined`, it is omitted.
|
|
105
134
|
*/
|
|
106
135
|
function withQuery(input, query) {
|
|
107
136
|
if (!query || Object.keys(query).length === 0) return input;
|
|
108
|
-
const
|
|
137
|
+
const { beforeHash, hash } = splitFragment(input);
|
|
138
|
+
const searchIndex = beforeHash.indexOf("?");
|
|
109
139
|
const hasExistingParams = searchIndex !== -1;
|
|
110
|
-
const base = hasExistingParams ?
|
|
111
|
-
const searchParams = new URLSearchParams(hasExistingParams ?
|
|
140
|
+
const base = hasExistingParams ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
141
|
+
const searchParams = new URLSearchParams(hasExistingParams ? beforeHash.slice(searchIndex + 1) : void 0);
|
|
112
142
|
for (const [key, value] of Object.entries(query)) {
|
|
113
143
|
if (value === void 0) {
|
|
114
144
|
searchParams.delete(key);
|
|
@@ -120,7 +150,37 @@ function withQuery(input, query) {
|
|
|
120
150
|
} else searchParams.set(key, normalizeQueryValue(value));
|
|
121
151
|
}
|
|
122
152
|
const queryString = searchParams.toString();
|
|
123
|
-
return queryString ? `${base}?${queryString}` : base;
|
|
153
|
+
return queryString ? `${base}?${queryString}${hash}` : base + hash;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reads the query parameters of the given URL, ignoring the fragment.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* A parameter that appears more than once becomes an array of its values,
|
|
160
|
+
* in the order they appear. Values are percent-decoded.
|
|
161
|
+
*/
|
|
162
|
+
function getQuery(input) {
|
|
163
|
+
const { beforeHash } = splitFragment(input);
|
|
164
|
+
const searchIndex = beforeHash.indexOf("?");
|
|
165
|
+
if (searchIndex === -1) return {};
|
|
166
|
+
const query = Object.create(null);
|
|
167
|
+
for (const [key, value] of new URLSearchParams(beforeHash.slice(searchIndex + 1))) {
|
|
168
|
+
const existingValue = query[key];
|
|
169
|
+
if (existingValue === void 0) query[key] = value;
|
|
170
|
+
else if (Array.isArray(existingValue)) existingValue.push(value);
|
|
171
|
+
else query[key] = [existingValue, value];
|
|
172
|
+
}
|
|
173
|
+
return { ...query };
|
|
174
|
+
}
|
|
175
|
+
function splitFragment(input) {
|
|
176
|
+
const hashIndex = input.indexOf("#");
|
|
177
|
+
return hashIndex === -1 ? {
|
|
178
|
+
beforeHash: input,
|
|
179
|
+
hash: ""
|
|
180
|
+
} : {
|
|
181
|
+
beforeHash: input.slice(0, hashIndex),
|
|
182
|
+
hash: input.slice(hashIndex)
|
|
183
|
+
};
|
|
124
184
|
}
|
|
125
185
|
function normalizeQueryValue(value) {
|
|
126
186
|
if (value === null) return "";
|
|
@@ -129,4 +189,4 @@ function normalizeQueryValue(value) {
|
|
|
129
189
|
return String(value);
|
|
130
190
|
}
|
|
131
191
|
//#endregion
|
|
132
|
-
export { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
192
|
+
export { getPathname, getQuery, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/result.d.mts
CHANGED
|
@@ -11,8 +11,8 @@ interface ErrData<E> {
|
|
|
11
11
|
type ResultData<T, E> = OkData<T> | ErrData<E>;
|
|
12
12
|
/**
|
|
13
13
|
* Successful result variant.
|
|
14
|
-
* @template T Success value type
|
|
15
|
-
* @template E Error type (phantom
|
|
14
|
+
* @template T Success value type
|
|
15
|
+
* @template E Error type (phantom – for type unification)
|
|
16
16
|
*/
|
|
17
17
|
declare class Ok<T, E = never> {
|
|
18
18
|
readonly value: T;
|
|
@@ -20,15 +20,14 @@ declare class Ok<T, E = never> {
|
|
|
20
20
|
constructor(value: T);
|
|
21
21
|
/** Transforms the success value. */
|
|
22
22
|
map<U>(fn: (value: T) => U): Ok<U, E>;
|
|
23
|
-
/**
|
|
23
|
+
/** Returns this `Ok` unchanged, with a new error type. */
|
|
24
24
|
mapError<E2>(_fn: (error: E) => E2): Ok<T, E2>;
|
|
25
|
-
/** Chains a Result
|
|
25
|
+
/** Chains a `Result`-returning function. */
|
|
26
26
|
andThen<U, E2>(fn: (value: T) => Result<U, E2>): Result<U, E | E2>;
|
|
27
|
-
/** Extracts the value. */
|
|
28
27
|
unwrap(_message?: string): T;
|
|
28
|
+
unwrapErr(message?: string): never;
|
|
29
29
|
/** Returns the value, ignoring the fallback. */
|
|
30
30
|
unwrapOr<U>(_fallback: U): T;
|
|
31
|
-
/** Pattern matches on the Result. */
|
|
32
31
|
match<R>(handlers: {
|
|
33
32
|
ok: (value: T) => R;
|
|
34
33
|
err: (error: E) => R;
|
|
@@ -36,24 +35,23 @@ declare class Ok<T, E = never> {
|
|
|
36
35
|
}
|
|
37
36
|
/**
|
|
38
37
|
* Error result variant.
|
|
39
|
-
* @template T Success type (phantom
|
|
40
|
-
* @template E Error value type
|
|
38
|
+
* @template T Success type (phantom – for type unification)
|
|
39
|
+
* @template E Error value type
|
|
41
40
|
*/
|
|
42
41
|
declare class Err<T, E> {
|
|
43
42
|
readonly error: E;
|
|
44
43
|
readonly ok = false;
|
|
45
44
|
constructor(error: E);
|
|
46
|
-
/**
|
|
45
|
+
/** Returns this `Err` unchanged, with a new value type. */
|
|
47
46
|
map<U>(_fn: (value: T) => U): Err<U, E>;
|
|
48
47
|
/** Transforms the error value. */
|
|
49
48
|
mapError<E2>(fn: (error: E) => E2): Err<T, E2>;
|
|
50
|
-
/**
|
|
49
|
+
/** Returns this `Err` unchanged, with a widened error type. */
|
|
51
50
|
andThen<U, E2>(_fn: (value: T) => Result<U, E2>): Err<U, E | E2>;
|
|
52
|
-
/** Throws an error with the given message. */
|
|
53
51
|
unwrap(message?: string): never;
|
|
52
|
+
unwrapErr(_message?: string): E;
|
|
54
53
|
/** Returns the fallback value. */
|
|
55
54
|
unwrapOr<U>(fallback: U): T | U;
|
|
56
|
-
/** Pattern matches on the Result. */
|
|
57
55
|
match<R>(handlers: {
|
|
58
56
|
ok: (value: T) => R;
|
|
59
57
|
err: (error: E) => R;
|
package/dist/result.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//#region src/result.ts
|
|
2
2
|
/**
|
|
3
3
|
* Successful result variant.
|
|
4
|
-
* @template T Success value type
|
|
5
|
-
* @template E Error type (phantom
|
|
4
|
+
* @template T Success value type
|
|
5
|
+
* @template E Error type (phantom – for type unification)
|
|
6
6
|
*/
|
|
7
7
|
var Ok = class Ok {
|
|
8
8
|
constructor(value) {
|
|
@@ -13,38 +13,39 @@ var Ok = class Ok {
|
|
|
13
13
|
map(fn) {
|
|
14
14
|
return new Ok(fn(this.value));
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/** Returns this `Ok` unchanged, with a new error type. */
|
|
17
17
|
mapError(_fn) {
|
|
18
18
|
return this;
|
|
19
19
|
}
|
|
20
|
-
/** Chains a Result
|
|
20
|
+
/** Chains a `Result`-returning function. */
|
|
21
21
|
andThen(fn) {
|
|
22
22
|
return fn(this.value);
|
|
23
23
|
}
|
|
24
|
-
/** Extracts the value. */
|
|
25
24
|
unwrap(_message) {
|
|
26
25
|
return this.value;
|
|
27
26
|
}
|
|
27
|
+
unwrapErr(message) {
|
|
28
|
+
throw new Error(message ?? `unwrapErr called on Ok: ${String(this.value)}`);
|
|
29
|
+
}
|
|
28
30
|
/** Returns the value, ignoring the fallback. */
|
|
29
31
|
unwrapOr(_fallback) {
|
|
30
32
|
return this.value;
|
|
31
33
|
}
|
|
32
|
-
/** Pattern matches on the Result. */
|
|
33
34
|
match(handlers) {
|
|
34
35
|
return handlers.ok(this.value);
|
|
35
36
|
}
|
|
36
37
|
};
|
|
37
38
|
/**
|
|
38
39
|
* Error result variant.
|
|
39
|
-
* @template T Success type (phantom
|
|
40
|
-
* @template E Error value type
|
|
40
|
+
* @template T Success type (phantom – for type unification)
|
|
41
|
+
* @template E Error value type
|
|
41
42
|
*/
|
|
42
43
|
var Err = class Err {
|
|
43
44
|
constructor(error) {
|
|
44
45
|
this.ok = false;
|
|
45
46
|
this.error = error;
|
|
46
47
|
}
|
|
47
|
-
/**
|
|
48
|
+
/** Returns this `Err` unchanged, with a new value type. */
|
|
48
49
|
map(_fn) {
|
|
49
50
|
return this;
|
|
50
51
|
}
|
|
@@ -52,19 +53,20 @@ var Err = class Err {
|
|
|
52
53
|
mapError(fn) {
|
|
53
54
|
return new Err(fn(this.error));
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
+
/** Returns this `Err` unchanged, with a widened error type. */
|
|
56
57
|
andThen(_fn) {
|
|
57
58
|
return this;
|
|
58
59
|
}
|
|
59
|
-
/** Throws an error with the given message. */
|
|
60
60
|
unwrap(message) {
|
|
61
|
-
throw new Error(message ?? `
|
|
61
|
+
throw new Error(message ?? `unwrap called on Err: ${String(this.error)}`);
|
|
62
|
+
}
|
|
63
|
+
unwrapErr(_message) {
|
|
64
|
+
return this.error;
|
|
62
65
|
}
|
|
63
66
|
/** Returns the fallback value. */
|
|
64
67
|
unwrapOr(fallback) {
|
|
65
68
|
return fallback;
|
|
66
69
|
}
|
|
67
|
-
/** Pattern matches on the Result. */
|
|
68
70
|
match(handlers) {
|
|
69
71
|
return handlers.err(this.error);
|
|
70
72
|
}
|
package/dist/string.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//#region src/string.d.ts
|
|
2
2
|
declare const TEMPLATE_PLACEHOLDER_RE: RegExp;
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Replaces `{name}` placeholders in a string with the matching variable.
|
|
5
5
|
*
|
|
6
6
|
* @remarks
|
|
7
7
|
* Only own properties of `variables` are substituted, so placeholders like
|
package/dist/string.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const URL_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
3
3
|
const TEMPLATE_PLACEHOLDER_RE = /\{(\w+)\}/g;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Replaces `{name}` placeholders in a string with the matching variable.
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
8
|
* Only own properties of `variables` are substituted, so placeholders like
|
package/dist/types.d.mts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
|
-
|
|
3
|
-
type LooseAutocomplete<T extends string> = T | AutocompletableString;
|
|
4
|
-
/** Also commonly referred to as `Prettify` */
|
|
2
|
+
/** Also commonly referred to as `Prettify`. */
|
|
5
3
|
type UnifyIntersection<T> = { [K in keyof T]: T[K]; } & {};
|
|
6
4
|
declare const brand: unique symbol;
|
|
7
5
|
type BrandedType<T, B> = T & {
|
|
8
6
|
[brand]: B;
|
|
9
7
|
};
|
|
10
8
|
//#endregion
|
|
11
|
-
export {
|
|
9
|
+
export { BrandedType, UnifyIntersection };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "utilful",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"packageManager": "pnpm@11.15.1",
|
|
6
6
|
"description": "A collection of TypeScript utilities",
|
|
7
7
|
"author": "Johann Schopplich <hello@johannschopplich.com>",
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/johannschopplich/utilful/issues"
|
|
16
16
|
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"csv",
|
|
19
|
+
"defu",
|
|
20
|
+
"emitter",
|
|
21
|
+
"result",
|
|
22
|
+
"typescript",
|
|
23
|
+
"url",
|
|
24
|
+
"utilities"
|
|
25
|
+
],
|
|
17
26
|
"sideEffects": false,
|
|
18
27
|
"exports": {
|
|
19
28
|
".": {
|