utilful 2.3.0 → 2.5.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 +83 -113
- package/dist/csv.d.mts +76 -1
- package/dist/csv.mjs +228 -72
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/json.mjs +4 -4
- package/dist/path.d.mts +5 -3
- package/dist/path.mjs +38 -15
- package/dist/result.d.mts +51 -9
- package/dist/result.mjs +67 -3
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -13,7 +13,6 @@ A collection of TypeScript utilities that I use across my projects.
|
|
|
13
13
|
- [JSON](#json)
|
|
14
14
|
- [Module](#module)
|
|
15
15
|
- [Object](#object)
|
|
16
|
-
- [Path](#path)
|
|
17
16
|
- [Result](#result)
|
|
18
17
|
- [String](#string)
|
|
19
18
|
|
|
@@ -150,13 +149,7 @@ Recursively assign default properties. Simplified version based on [unjs/defu](h
|
|
|
150
149
|
|
|
151
150
|
Recursively assigns missing properties from defaults to the source object. The source object takes precedence over defaults.
|
|
152
151
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
- **Nullish handling**: `null` and `undefined` values in source are replaced with defaults
|
|
156
|
-
- **Array concatenation**: Arrays are concatenated (source + defaults)
|
|
157
|
-
- **Deep merging**: Nested objects are recursively merged
|
|
158
|
-
- **Type safety**: Preserves TypeScript types
|
|
159
|
-
- **Prototype pollution protection**: Ignores `__proto__` and `constructor` keys
|
|
152
|
+
The function replaces `null` and `undefined` values in the source with defaults, concatenates arrays (source + defaults), and recursively merges nested objects.
|
|
160
153
|
|
|
161
154
|
```ts
|
|
162
155
|
type PlainObject = Record<PropertyKey, any>
|
|
@@ -358,191 +351,170 @@ Deeply applies a callback to every key-value pair in the given object, as well a
|
|
|
358
351
|
declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: T, key: keyof T, value: T[keyof T]) => void): void
|
|
359
352
|
```
|
|
360
353
|
|
|
361
|
-
###
|
|
362
|
-
|
|
363
|
-
#### `withoutLeadingSlash`
|
|
364
|
-
|
|
365
|
-
Removes the leading slash from the given path if it has one.
|
|
366
|
-
|
|
367
|
-
```ts
|
|
368
|
-
declare function withoutLeadingSlash(path?: string): string
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
#### `withLeadingSlash`
|
|
354
|
+
### Result
|
|
372
355
|
|
|
373
|
-
|
|
356
|
+
The `Result` type represents either success (`Ok`) or failure (`Err`). It provides a type-safe way to handle errors without relying on exceptions.
|
|
374
357
|
|
|
375
358
|
```ts
|
|
376
|
-
|
|
359
|
+
type Result<T, E> = Ok<T, E> | Err<T, E>
|
|
377
360
|
```
|
|
378
361
|
|
|
379
|
-
|
|
362
|
+
Both `Ok` and `Err` carry phantom types for proper type inference in unions.
|
|
380
363
|
|
|
381
|
-
|
|
364
|
+
**Basic example:**
|
|
382
365
|
|
|
383
366
|
```ts
|
|
384
|
-
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
#### `withTrailingSlash`
|
|
367
|
+
import { err, ok } from 'utilful'
|
|
388
368
|
|
|
389
|
-
|
|
369
|
+
function divide(a: number, b: number) {
|
|
370
|
+
if (b === 0) {
|
|
371
|
+
return err('Division by zero')
|
|
372
|
+
}
|
|
373
|
+
return ok(a / b)
|
|
374
|
+
}
|
|
390
375
|
|
|
391
|
-
|
|
392
|
-
|
|
376
|
+
const result = divide(10, 2)
|
|
377
|
+
if (result.ok)
|
|
378
|
+
console.log('Result:', result.value)
|
|
379
|
+
else
|
|
380
|
+
console.error('Error:', result.error)
|
|
393
381
|
```
|
|
394
382
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
Joins the given URL path segments, ensuring that there is only one slash between them.
|
|
383
|
+
**Fluent chaining:**
|
|
398
384
|
|
|
399
385
|
```ts
|
|
400
|
-
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
#### `withBase`
|
|
386
|
+
import { toResult } from 'utilful'
|
|
404
387
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
388
|
+
const name = toResult(() => JSON.parse(jsonString))
|
|
389
|
+
.map(data => data.user)
|
|
390
|
+
.map(user => user.name)
|
|
391
|
+
.unwrapOr('Anonymous')
|
|
409
392
|
```
|
|
410
393
|
|
|
411
|
-
#### `
|
|
394
|
+
#### `ok`
|
|
412
395
|
|
|
413
|
-
|
|
396
|
+
Creates a successful result.
|
|
414
397
|
|
|
415
398
|
```ts
|
|
416
|
-
declare function
|
|
399
|
+
declare function ok<T, E = never>(value: T): Ok<T, E>
|
|
417
400
|
```
|
|
418
401
|
|
|
419
|
-
#### `
|
|
402
|
+
#### `err`
|
|
420
403
|
|
|
421
|
-
|
|
404
|
+
Creates an error result.
|
|
422
405
|
|
|
423
406
|
```ts
|
|
424
|
-
declare function
|
|
407
|
+
declare function err<T = never, E extends string = string>(error: E): Err<T, E>
|
|
408
|
+
declare function err<T = never, E = unknown>(error: E): Err<T, E>
|
|
425
409
|
```
|
|
426
410
|
|
|
427
|
-
#### `
|
|
411
|
+
#### `isOk` / `isErr`
|
|
428
412
|
|
|
429
|
-
|
|
413
|
+
Type guards for narrowing `Result` types.
|
|
430
414
|
|
|
431
415
|
```ts
|
|
432
|
-
declare function
|
|
416
|
+
declare function isOk<T, E>(result: Result<T, E>): result is Ok<T, E>
|
|
417
|
+
declare function isErr<T, E>(result: Result<T, E>): result is Err<T, E>
|
|
433
418
|
```
|
|
434
419
|
|
|
435
420
|
**Example:**
|
|
436
421
|
|
|
437
422
|
```ts
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
// This key is omitted
|
|
443
|
-
baz: undefined,
|
|
444
|
-
// Object values are stringified
|
|
445
|
-
baz: { qux: 'quux' }
|
|
446
|
-
})
|
|
423
|
+
const result = toResult(() => JSON.parse(str))
|
|
424
|
+
if (isOk(result)) {
|
|
425
|
+
console.log(result.value) // TypeScript knows this is Ok
|
|
426
|
+
}
|
|
447
427
|
```
|
|
448
428
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
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.
|
|
429
|
+
#### `Result.map`
|
|
452
430
|
|
|
453
|
-
|
|
431
|
+
Transforms the success value. No-op on `Err`.
|
|
454
432
|
|
|
455
433
|
```ts
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
function divide(a: number, b: number) {
|
|
459
|
-
if (b === 0) {
|
|
460
|
-
return err('Division by zero')
|
|
461
|
-
}
|
|
462
|
-
return ok(a / b)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const result = divide(10, 2)
|
|
466
|
-
if (result.ok)
|
|
467
|
-
console.log('Result:', result.value)
|
|
468
|
-
else
|
|
469
|
-
console.error('Error:', result.error)
|
|
434
|
+
ok(2).map(x => x * 3) // Ok(6)
|
|
435
|
+
err('fail').map(x => x * 3) // Err('fail')
|
|
470
436
|
```
|
|
471
437
|
|
|
472
|
-
#### `Result`
|
|
438
|
+
#### `Result.mapError`
|
|
473
439
|
|
|
474
|
-
|
|
440
|
+
Transforms the error value. No-op on `Ok`.
|
|
475
441
|
|
|
476
442
|
```ts
|
|
477
|
-
|
|
443
|
+
err('fail').mapError(e => e.toUpperCase()) // Err('FAIL')
|
|
444
|
+
ok(42).mapError(e => e.toUpperCase()) // Ok(42)
|
|
478
445
|
```
|
|
479
446
|
|
|
480
|
-
#### `
|
|
447
|
+
#### `Result.andThen`
|
|
481
448
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
**Example:**
|
|
449
|
+
Chains a function that returns a `Result`. Useful for composing fallible operations.
|
|
485
450
|
|
|
486
451
|
```ts
|
|
487
|
-
|
|
452
|
+
ok(2).andThen(x => x > 0 ? ok(x) : err('negative')) // Ok(2)
|
|
453
|
+
err('fail').andThen(x => ok(x * 2)) // Err('fail') - short-circuits
|
|
488
454
|
```
|
|
489
455
|
|
|
490
|
-
#### `
|
|
456
|
+
#### `Result.unwrap`
|
|
491
457
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
**Example:**
|
|
458
|
+
Extracts the value or throws an error.
|
|
495
459
|
|
|
496
460
|
```ts
|
|
497
|
-
|
|
461
|
+
ok(42).unwrap() // 42
|
|
462
|
+
err('fail').unwrap() // throws Error
|
|
463
|
+
err('fail').unwrap('custom message') // throws Error('custom message')
|
|
498
464
|
```
|
|
499
465
|
|
|
500
|
-
#### `
|
|
466
|
+
#### `Result.unwrapOr`
|
|
501
467
|
|
|
502
|
-
|
|
468
|
+
Extracts the value or returns a fallback.
|
|
503
469
|
|
|
504
470
|
```ts
|
|
505
|
-
|
|
471
|
+
ok(42).unwrapOr(0) // 42
|
|
472
|
+
err('fail').unwrapOr(0) // 0
|
|
506
473
|
```
|
|
507
474
|
|
|
508
|
-
#### `
|
|
475
|
+
#### `Result.match`
|
|
509
476
|
|
|
510
|
-
|
|
477
|
+
Pattern matches on the result.
|
|
511
478
|
|
|
512
479
|
```ts
|
|
513
|
-
|
|
514
|
-
|
|
480
|
+
result.match({
|
|
481
|
+
ok: value => `Success: ${value}`,
|
|
482
|
+
err: error => `Error: ${error}`,
|
|
483
|
+
})
|
|
515
484
|
```
|
|
516
485
|
|
|
517
486
|
#### `toResult`
|
|
518
487
|
|
|
519
|
-
Wraps a function that might throw
|
|
488
|
+
Wraps a function or promise that might throw and returns a `Result`.
|
|
520
489
|
|
|
521
490
|
```ts
|
|
522
491
|
declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>
|
|
523
492
|
declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>
|
|
524
493
|
```
|
|
525
494
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
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.
|
|
495
|
+
**Example:**
|
|
529
496
|
|
|
530
497
|
```ts
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
498
|
+
// Synchronous
|
|
499
|
+
const result = toResult(() => JSON.parse('{"foo":"bar"}'))
|
|
500
|
+
|
|
501
|
+
// Asynchronous
|
|
502
|
+
const result = await toResult(fetch('https://api.example.com'))
|
|
534
503
|
```
|
|
535
504
|
|
|
536
|
-
|
|
505
|
+
#### `unwrapResult`
|
|
506
|
+
|
|
507
|
+
Converts a `Result` to a plain object with `value` and `error` properties.
|
|
537
508
|
|
|
538
509
|
```ts
|
|
539
|
-
|
|
540
|
-
|
|
510
|
+
declare function unwrapResult<T, E>(result: Ok<T, E>): { value: T, error: undefined }
|
|
511
|
+
declare function unwrapResult<T, E>(result: Err<T, E>): { value: undefined, error: E }
|
|
512
|
+
declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }
|
|
541
513
|
```
|
|
542
514
|
|
|
543
515
|
#### `tryCatch`
|
|
544
516
|
|
|
545
|
-
|
|
517
|
+
Combines `toResult` and `unwrapResult` into one step. Executes a function and returns `{ value, error }` directly.
|
|
546
518
|
|
|
547
519
|
```ts
|
|
548
520
|
declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
|
|
@@ -552,13 +524,11 @@ declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value:
|
|
|
552
524
|
**Example:**
|
|
553
525
|
|
|
554
526
|
```ts
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// Synchronous usage
|
|
527
|
+
// Synchronous
|
|
558
528
|
const { value, error } = tryCatch(() => JSON.parse('{"foo":"bar"}'))
|
|
559
529
|
|
|
560
|
-
// Asynchronous
|
|
561
|
-
const { value, error } = await tryCatch(fetch('https://api.example.com
|
|
530
|
+
// Asynchronous
|
|
531
|
+
const { value, error } = await tryCatch(fetch('https://api.example.com').then(r => r.json()))
|
|
562
532
|
```
|
|
563
533
|
|
|
564
534
|
### String
|
package/dist/csv.d.mts
CHANGED
|
@@ -55,6 +55,41 @@ interface CSVCreateOptions {
|
|
|
55
55
|
declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], columns: readonly (keyof T)[], options?: CSVCreateOptions): string;
|
|
56
56
|
declare function createCSV<T extends Record<string, unknown>>(data: readonly T[], options?: CSVCreateOptions): string;
|
|
57
57
|
/**
|
|
58
|
+
* Creates a CSV stream from an iterable or async iterable of objects.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* This function yields CSV content as strings, including line endings.
|
|
62
|
+
* Each yielded chunk contains complete lines (header and/or data rows).
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const data = [
|
|
66
|
+
* { name: 'John', age: '30' },
|
|
67
|
+
* { name: 'Jane', age: '25' }
|
|
68
|
+
* ]
|
|
69
|
+
*
|
|
70
|
+
* for await (const chunk of createCSVStream(data, ['name', 'age'])) {
|
|
71
|
+
* console.log(chunk)
|
|
72
|
+
* }
|
|
73
|
+
*/
|
|
74
|
+
declare function createCSVStream<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): AsyncIterable<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Creates a CSV string from an async iterable or iterable of objects.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* This is a convenience wrapper around `createCSVStream` that collects
|
|
80
|
+
* all chunks into a single string. Note that the result will have a
|
|
81
|
+
* trailing line ending, unlike the synchronous `createCSV`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* const data = [
|
|
85
|
+
* { name: 'John', age: '30' },
|
|
86
|
+
* { name: 'Jane', age: '25' }
|
|
87
|
+
* ]
|
|
88
|
+
*
|
|
89
|
+
* const csv = await createCSVAsync(data, ['name', 'age'])
|
|
90
|
+
*/
|
|
91
|
+
declare function createCSVAsync<T extends Record<string, unknown>>(data: AsyncIterable<T> | Iterable<T>, columns: readonly (keyof T)[], options?: CSVCreateOptions): Promise<string>;
|
|
92
|
+
/**
|
|
58
93
|
* Escapes a value for a CSV string.
|
|
59
94
|
*
|
|
60
95
|
* @remarks
|
|
@@ -100,5 +135,45 @@ declare function parseCSV<Header extends string>(csv?: string | null | undefined
|
|
|
100
135
|
*/
|
|
101
136
|
strict?: boolean;
|
|
102
137
|
}): CSVRow<Header>[];
|
|
138
|
+
/**
|
|
139
|
+
* Parses CSV data from an async iterable or iterable of string chunks.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* This function yields CSV rows as they are parsed. Chunks do not need to
|
|
143
|
+
* align with row boundaries; the parser handles quotes and newlines correctly
|
|
144
|
+
* across chunk boundaries.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const chunks = ['name,age\nJo', 'hn,30\nJane,25']
|
|
148
|
+
*
|
|
149
|
+
* for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
|
|
150
|
+
* console.log(row)
|
|
151
|
+
* }
|
|
152
|
+
*/
|
|
153
|
+
declare function parseCSVStream<Header extends string>(chunks: AsyncIterable<string> | Iterable<string>, options?: {
|
|
154
|
+
delimiter?: string;
|
|
155
|
+
trim?: boolean;
|
|
156
|
+
strict?: boolean;
|
|
157
|
+
}): AsyncIterable<CSVRow<Header>>;
|
|
158
|
+
/**
|
|
159
|
+
* Parses CSV data from an async iterable or iterable of lines.
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* This is a convenience wrapper around `parseCSVStream` that treats each line
|
|
163
|
+
* as a chunk. Note that lines do not necessarily correspond to CSV rows due to
|
|
164
|
+
* quoted fields containing newlines. The parser handles this correctly.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* const lines = ['name,age', 'John,30', 'Jane,25']
|
|
168
|
+
*
|
|
169
|
+
* for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
|
|
170
|
+
* console.log(row)
|
|
171
|
+
* }
|
|
172
|
+
*/
|
|
173
|
+
declare function parseCSVFromLines<Header extends string>(lines: AsyncIterable<string> | Iterable<string>, options?: {
|
|
174
|
+
delimiter?: string;
|
|
175
|
+
trim?: boolean;
|
|
176
|
+
strict?: boolean;
|
|
177
|
+
}): AsyncIterable<CSVRow<Header>>;
|
|
103
178
|
//#endregion
|
|
104
|
-
export { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV };
|
|
179
|
+
export { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
|
package/dist/csv.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
//#region src/csv.ts
|
|
2
|
+
const COMMA = ",";
|
|
3
|
+
const DOUBLE_QUOTE = "\"";
|
|
4
|
+
const NEWLINE = "\n";
|
|
5
|
+
const CARRIAGE_RETURN = "\r";
|
|
6
|
+
const ESCAPED_QUOTE = "\"\"";
|
|
7
|
+
const SPACE = " ";
|
|
8
|
+
const TAB = " ";
|
|
2
9
|
function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
3
10
|
let columns;
|
|
4
11
|
let options;
|
|
@@ -10,19 +17,71 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
|
10
17
|
options = columnsOrOptions ?? {};
|
|
11
18
|
}
|
|
12
19
|
if (columns.length === 0 && data.length === 0) return "";
|
|
13
|
-
const { delimiter =
|
|
20
|
+
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
14
21
|
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
15
|
-
const formatCell = (value) => escapeCSVValue(value, {
|
|
16
|
-
delimiter,
|
|
17
|
-
quoteAll
|
|
18
|
-
});
|
|
19
|
-
const rows = data.map((obj) => columns.map((key) => formatCell(obj[key])).join(delimiter));
|
|
20
22
|
if (addHeader) {
|
|
21
|
-
const header = columns.map(
|
|
22
|
-
if (
|
|
23
|
-
|
|
23
|
+
const header = encodeCSVHeader(columns.map(String), delimiter, quoteAll);
|
|
24
|
+
if (data.length === 0) return header;
|
|
25
|
+
const bodyLines = data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll));
|
|
26
|
+
return header + lineEnding + bodyLines.join(lineEnding);
|
|
24
27
|
}
|
|
25
|
-
return
|
|
28
|
+
return data.map((row) => encodeCSVRow(row, columns, delimiter, quoteAll)).join(lineEnding);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates a CSV stream from an iterable or async iterable of objects.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* This function yields CSV content as strings, including line endings.
|
|
35
|
+
* Each yielded chunk contains complete lines (header and/or data rows).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const data = [
|
|
39
|
+
* { name: 'John', age: '30' },
|
|
40
|
+
* { name: 'Jane', age: '25' }
|
|
41
|
+
* ]
|
|
42
|
+
*
|
|
43
|
+
* for await (const chunk of createCSVStream(data, ['name', 'age'])) {
|
|
44
|
+
* console.log(chunk)
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
async function* createCSVStream(data, columns, options = {}) {
|
|
48
|
+
const { delimiter = COMMA, addHeader = true, quoteAll = false, lineEnding = NEWLINE } = options;
|
|
49
|
+
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
50
|
+
if (addHeader) yield encodeCSVHeader(columns.map(String), delimiter, quoteAll) + lineEnding;
|
|
51
|
+
for await (const row of data) yield encodeCSVRow(row, columns, delimiter, quoteAll) + lineEnding;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a CSV string from an async iterable or iterable of objects.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* This is a convenience wrapper around `createCSVStream` that collects
|
|
58
|
+
* all chunks into a single string. Note that the result will have a
|
|
59
|
+
* trailing line ending, unlike the synchronous `createCSV`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const data = [
|
|
63
|
+
* { name: 'John', age: '30' },
|
|
64
|
+
* { name: 'Jane', age: '25' }
|
|
65
|
+
* ]
|
|
66
|
+
*
|
|
67
|
+
* const csv = await createCSVAsync(data, ['name', 'age'])
|
|
68
|
+
*/
|
|
69
|
+
async function createCSVAsync(data, columns, options = {}) {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
for await (const chunk of createCSVStream(data, columns, options)) chunks.push(chunk);
|
|
72
|
+
return chunks.join("");
|
|
73
|
+
}
|
|
74
|
+
function encodeCSVHeader(columns, delimiter, quoteAll) {
|
|
75
|
+
return columns.map((col) => escapeCSVValue(col, {
|
|
76
|
+
delimiter,
|
|
77
|
+
quoteAll
|
|
78
|
+
})).join(delimiter);
|
|
79
|
+
}
|
|
80
|
+
function encodeCSVRow(row, columns, delimiter, quoteAll) {
|
|
81
|
+
return columns.map((key) => escapeCSVValue(row[key], {
|
|
82
|
+
delimiter,
|
|
83
|
+
quoteAll
|
|
84
|
+
})).join(delimiter);
|
|
26
85
|
}
|
|
27
86
|
/**
|
|
28
87
|
* Escapes a value for a CSV string.
|
|
@@ -37,12 +96,118 @@ function createCSV(data, columnsOrOptions, maybeOptions = {}) {
|
|
|
37
96
|
* escapeCSVValue('contains "quotes"') // "contains ""quotes"""
|
|
38
97
|
*/
|
|
39
98
|
function escapeCSVValue(value, options = {}) {
|
|
40
|
-
const { delimiter =
|
|
99
|
+
const { delimiter = COMMA, quoteAll = false } = options;
|
|
41
100
|
if (value == null) return "";
|
|
42
101
|
const coercedValue = String(value);
|
|
43
|
-
|
|
102
|
+
const hasQuote = coercedValue.includes(DOUBLE_QUOTE);
|
|
103
|
+
if (quoteAll || coercedValue.includes(delimiter) || hasQuote || coercedValue.includes(NEWLINE) || coercedValue.includes(CARRIAGE_RETURN)) return `${DOUBLE_QUOTE}${hasQuote ? coercedValue.replaceAll(DOUBLE_QUOTE, ESCAPED_QUOTE) : coercedValue}${DOUBLE_QUOTE}`;
|
|
44
104
|
return coercedValue;
|
|
45
105
|
}
|
|
106
|
+
var CSVParserCore = class {
|
|
107
|
+
delimiter;
|
|
108
|
+
trim;
|
|
109
|
+
strict;
|
|
110
|
+
onRow;
|
|
111
|
+
currentRow = [];
|
|
112
|
+
currentRowQuotedFlags = [];
|
|
113
|
+
currentField = "";
|
|
114
|
+
inQuotes = false;
|
|
115
|
+
isFieldQuoted = false;
|
|
116
|
+
currentRowNumber = 1;
|
|
117
|
+
headerRaw;
|
|
118
|
+
headerQuotedFlags;
|
|
119
|
+
headers;
|
|
120
|
+
constructor(options, onRow) {
|
|
121
|
+
const { delimiter = COMMA, trim = true, strict = true } = options;
|
|
122
|
+
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
123
|
+
this.delimiter = delimiter;
|
|
124
|
+
this.trim = trim;
|
|
125
|
+
this.strict = strict;
|
|
126
|
+
this.onRow = onRow;
|
|
127
|
+
}
|
|
128
|
+
push(chunk) {
|
|
129
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
130
|
+
const character = chunk[i];
|
|
131
|
+
const nextCharacter = i + 1 < chunk.length ? chunk[i + 1] : "";
|
|
132
|
+
if (this.isFieldQuoted && !this.inQuotes && character !== this.delimiter && (character === SPACE || character === TAB)) continue;
|
|
133
|
+
if (character === DOUBLE_QUOTE) if (this.currentField.length === 0 && !this.inQuotes) {
|
|
134
|
+
this.inQuotes = true;
|
|
135
|
+
this.isFieldQuoted = true;
|
|
136
|
+
} else if (this.inQuotes && nextCharacter === DOUBLE_QUOTE) {
|
|
137
|
+
this.currentField += DOUBLE_QUOTE;
|
|
138
|
+
i++;
|
|
139
|
+
} else if (this.inQuotes) this.inQuotes = false;
|
|
140
|
+
else this.currentField += character;
|
|
141
|
+
else if (character === this.delimiter && !this.inQuotes) this.appendField();
|
|
142
|
+
else if ((character === NEWLINE || character === CARRIAGE_RETURN) && !this.inQuotes) {
|
|
143
|
+
if (character === CARRIAGE_RETURN && nextCharacter === NEWLINE) i++;
|
|
144
|
+
this.appendRow();
|
|
145
|
+
} else this.currentField += character;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finish() {
|
|
149
|
+
if (this.inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${this.currentRowNumber}`);
|
|
150
|
+
if (this.currentField !== "" || this.currentRow.length > 0) this.appendRow();
|
|
151
|
+
}
|
|
152
|
+
appendField() {
|
|
153
|
+
this.currentRow.push(this.currentField);
|
|
154
|
+
this.currentRowQuotedFlags.push(this.isFieldQuoted);
|
|
155
|
+
this.currentField = "";
|
|
156
|
+
this.isFieldQuoted = false;
|
|
157
|
+
}
|
|
158
|
+
appendRow() {
|
|
159
|
+
this.appendField();
|
|
160
|
+
if (!this.headerRaw) {
|
|
161
|
+
this.headerRaw = this.currentRow;
|
|
162
|
+
this.headerQuotedFlags = this.currentRowQuotedFlags;
|
|
163
|
+
this.processHeaderRow();
|
|
164
|
+
} else this.processDataRow(this.currentRow, this.currentRowQuotedFlags);
|
|
165
|
+
this.currentRow = [];
|
|
166
|
+
this.currentRowQuotedFlags = [];
|
|
167
|
+
this.currentRowNumber++;
|
|
168
|
+
}
|
|
169
|
+
processHeaderRow() {
|
|
170
|
+
const headerRow = this.headerRaw;
|
|
171
|
+
const headerQuotedFlags = this.headerQuotedFlags ?? [];
|
|
172
|
+
const headers = this.trim ? headerRow.map((h, i) => headerQuotedFlags[i] ? h : h.trim()) : headerRow;
|
|
173
|
+
if (headers.filter((h) => h.length === 0).length > 0) {
|
|
174
|
+
const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
|
|
175
|
+
throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
|
|
176
|
+
}
|
|
177
|
+
const headerSet = /* @__PURE__ */ new Set();
|
|
178
|
+
const duplicateHeaderNames = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
|
|
180
|
+
else headerSet.add(header);
|
|
181
|
+
if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
|
|
182
|
+
this.headers = headers;
|
|
183
|
+
}
|
|
184
|
+
processDataRow(fieldValues, quotedFlags) {
|
|
185
|
+
if (!this.headers) throw new Error("CSVParserCore: headers not initialized");
|
|
186
|
+
const headers = this.headers;
|
|
187
|
+
const isFieldPopulated = this.trim ? (field, wasQuoted) => wasQuoted ? field.length > 0 : field.trim().length > 0 : (field) => field.length > 0;
|
|
188
|
+
const hasMultipleFields = fieldValues.length > 1;
|
|
189
|
+
const hasAnyPopulatedField = fieldValues.some((field, idx) => isFieldPopulated(field, quotedFlags[idx] ?? false));
|
|
190
|
+
if (!hasMultipleFields && !hasAnyPopulatedField) return;
|
|
191
|
+
if (fieldValues.length > headers.length) {
|
|
192
|
+
const fieldsExceedingHeaders = fieldValues.slice(headers.length);
|
|
193
|
+
const excessQuotedFlags = quotedFlags.slice(headers.length);
|
|
194
|
+
const containsNonEmptyOverflow = fieldsExceedingHeaders.some((field, idx) => isFieldPopulated(field, excessQuotedFlags[idx] ?? false));
|
|
195
|
+
if (this.strict && containsNonEmptyOverflow) {
|
|
196
|
+
const expectedCount = headers.length;
|
|
197
|
+
const actualCount = fieldValues.length;
|
|
198
|
+
const excessCount = actualCount - expectedCount;
|
|
199
|
+
throw new SyntaxError(`CSV row ${this.currentRowNumber} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const rowEntries = headers.map((header, columnIndex) => {
|
|
203
|
+
const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
|
|
204
|
+
const wasQuoted = quotedFlags[columnIndex] ?? false;
|
|
205
|
+
return [header, this.trim && !wasQuoted ? untrimmedValue.trim() : untrimmedValue];
|
|
206
|
+
});
|
|
207
|
+
const rowObject = Object.fromEntries(rowEntries);
|
|
208
|
+
this.onRow(rowObject);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
46
211
|
/**
|
|
47
212
|
* Parses a comma-separated values (CSV) string into an array of objects.
|
|
48
213
|
*
|
|
@@ -60,66 +225,57 @@ function escapeCSVValue(value, options = {}) {
|
|
|
60
225
|
function parseCSV(csv, options = {}) {
|
|
61
226
|
if (!csv?.trim()) return [];
|
|
62
227
|
const rows = [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let inQuotes = false;
|
|
66
|
-
let currentRowNumber = 1;
|
|
67
|
-
const { delimiter = ",", trim = true, strict = true } = options;
|
|
68
|
-
if (delimiter.length !== 1) throw new RangeError(`CSV delimiter must be a single character, got "${delimiter}"`);
|
|
69
|
-
const appendField = () => {
|
|
70
|
-
currentRow.push(currentField);
|
|
71
|
-
currentField = "";
|
|
72
|
-
};
|
|
73
|
-
const appendRow = () => {
|
|
74
|
-
appendField();
|
|
75
|
-
rows.push(currentRow);
|
|
76
|
-
currentRow = [];
|
|
77
|
-
};
|
|
78
|
-
for (let i = 0; i < csv.length; i++) {
|
|
79
|
-
const character = csv[i];
|
|
80
|
-
const nextCharacter = i + 1 < csv.length ? csv[i + 1] : "";
|
|
81
|
-
if (character === "\"") if (inQuotes && nextCharacter === "\"") {
|
|
82
|
-
currentField += "\"";
|
|
83
|
-
i++;
|
|
84
|
-
} else inQuotes = !inQuotes;
|
|
85
|
-
else if (character === delimiter && !inQuotes) appendField();
|
|
86
|
-
else if ((character === "\n" || character === "\r" && nextCharacter === "\n") && !inQuotes) {
|
|
87
|
-
if (character === "\r") i++;
|
|
88
|
-
appendRow();
|
|
89
|
-
currentRowNumber++;
|
|
90
|
-
} else currentField += character;
|
|
91
|
-
}
|
|
92
|
-
if (currentField || currentRow.length > 0) appendRow();
|
|
93
|
-
if (inQuotes) throw new SyntaxError(`CSV contains unterminated quoted field at row ${currentRowNumber}`);
|
|
94
|
-
if (rows.length <= 1) return [];
|
|
95
|
-
const [headerRow] = rows;
|
|
96
|
-
if (!headerRow) return [];
|
|
97
|
-
const headers = trim ? headerRow.map((h) => h.trim()) : headerRow;
|
|
98
|
-
if (headers.filter((h) => h.length === 0).length > 0) {
|
|
99
|
-
const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
|
|
100
|
-
throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
|
|
101
|
-
}
|
|
102
|
-
const headerSet = /* @__PURE__ */ new Set();
|
|
103
|
-
const duplicateHeaderNames = /* @__PURE__ */ new Set();
|
|
104
|
-
for (const header of headers) if (headerSet.has(header)) duplicateHeaderNames.add(header);
|
|
105
|
-
else headerSet.add(header);
|
|
106
|
-
if (duplicateHeaderNames.size > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...duplicateHeaderNames].join(", ")}`);
|
|
107
|
-
const isFieldPopulated = trim ? (field) => field.trim().length > 0 : (field) => field.length > 0;
|
|
108
|
-
return rows.slice(1).filter((row) => row.length > 1 || row.some(isFieldPopulated)).map((fieldValues, rowIndex) => {
|
|
109
|
-
if (fieldValues.length > headers.length) {
|
|
110
|
-
const containsNonEmptyOverflow = fieldValues.slice(headers.length).some(isFieldPopulated);
|
|
111
|
-
if (strict && containsNonEmptyOverflow) {
|
|
112
|
-
const expectedCount = headers.length;
|
|
113
|
-
const actualCount = fieldValues.length;
|
|
114
|
-
const excessCount = actualCount - expectedCount;
|
|
115
|
-
throw new SyntaxError(`CSV row ${rowIndex + 2} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return Object.fromEntries(headers.map((header, columnIndex) => {
|
|
119
|
-
const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
|
|
120
|
-
return [header, trim ? untrimmedValue.trim() : untrimmedValue];
|
|
121
|
-
}));
|
|
228
|
+
const parser = new CSVParserCore(options, (row) => {
|
|
229
|
+
rows.push(row);
|
|
122
230
|
});
|
|
231
|
+
parser.push(csv);
|
|
232
|
+
parser.finish();
|
|
233
|
+
return rows;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Parses CSV data from an async iterable or iterable of string chunks.
|
|
237
|
+
*
|
|
238
|
+
* @remarks
|
|
239
|
+
* This function yields CSV rows as they are parsed. Chunks do not need to
|
|
240
|
+
* align with row boundaries; the parser handles quotes and newlines correctly
|
|
241
|
+
* across chunk boundaries.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* const chunks = ['name,age\nJo', 'hn,30\nJane,25']
|
|
245
|
+
*
|
|
246
|
+
* for await (const row of parseCSVStream<'name' | 'age'>(chunks)) {
|
|
247
|
+
* console.log(row)
|
|
248
|
+
* }
|
|
249
|
+
*/
|
|
250
|
+
async function* parseCSVStream(chunks, options = {}) {
|
|
251
|
+
const queue = [];
|
|
252
|
+
const parser = new CSVParserCore(options, (row) => {
|
|
253
|
+
queue.push(row);
|
|
254
|
+
});
|
|
255
|
+
for await (const chunk of chunks) {
|
|
256
|
+
parser.push(chunk);
|
|
257
|
+
while (queue.length > 0) yield queue.shift();
|
|
258
|
+
}
|
|
259
|
+
parser.finish();
|
|
260
|
+
while (queue.length > 0) yield queue.shift();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Parses CSV data from an async iterable or iterable of lines.
|
|
264
|
+
*
|
|
265
|
+
* @remarks
|
|
266
|
+
* This is a convenience wrapper around `parseCSVStream` that treats each line
|
|
267
|
+
* as a chunk. Note that lines do not necessarily correspond to CSV rows due to
|
|
268
|
+
* quoted fields containing newlines. The parser handles this correctly.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* const lines = ['name,age', 'John,30', 'Jane,25']
|
|
272
|
+
*
|
|
273
|
+
* for await (const row of parseCSVFromLines<'name' | 'age'>(lines)) {
|
|
274
|
+
* console.log(row)
|
|
275
|
+
* }
|
|
276
|
+
*/
|
|
277
|
+
async function* parseCSVFromLines(lines, options) {
|
|
278
|
+
yield* parseCSVStream(lines, options);
|
|
123
279
|
}
|
|
124
280
|
/**
|
|
125
281
|
* Infers column names from data by collecting the union of keys
|
|
@@ -137,4 +293,4 @@ function inferColumns(rows) {
|
|
|
137
293
|
}
|
|
138
294
|
|
|
139
295
|
//#endregion
|
|
140
|
-
export { createCSV, escapeCSVValue, parseCSV };
|
|
296
|
+
export { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { MaybeArray, toArray } from "./array.mjs";
|
|
2
|
-
import { CSVCreateOptions, CSVRow, createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
|
|
2
|
+
import { CSVCreateOptions, CSVRow, createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
|
|
3
3
|
import { DefuFn, DefuMerger, createDefu, defu } from "./defu.mjs";
|
|
4
4
|
import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter } from "./emitter.mjs";
|
|
5
5
|
import { cloneJSON, tryParseJSON } from "./json.mjs";
|
|
6
6
|
import { interopDefault } from "./module.mjs";
|
|
7
7
|
import { deepApply, memoize, objectEntries, objectKeys } from "./object.mjs";
|
|
8
8
|
import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
9
|
-
import { Err, ErrData, Ok, OkData, Result, ResultData, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
9
|
+
import { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { generateRandomId, template } from "./string.mjs";
|
|
11
11
|
import { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection } from "./types.mjs";
|
|
12
|
-
export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
12
|
+
export { AutocompletableString, BrandedType, CSVCreateOptions, CSVRow, DefuFn, DefuMerger, Emitter, Err, ErrData, EventHandlerList, EventHandlerMap, EventType, Handler, LooseAutocomplete, MaybeArray, Ok, OkData, QueryObject, QueryValue, Result, ResultData, UnifyIntersection, WildCardEventHandlerList, WildcardHandler, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { toArray } from "./array.mjs";
|
|
2
|
-
import { createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
|
|
2
|
+
import { createCSV, createCSVAsync, createCSVStream, escapeCSVValue, parseCSV, parseCSVFromLines, parseCSVStream } from "./csv.mjs";
|
|
3
3
|
import { createDefu, defu } from "./defu.mjs";
|
|
4
4
|
import { createEmitter } from "./emitter.mjs";
|
|
5
5
|
import { cloneJSON, tryParseJSON } from "./json.mjs";
|
|
6
6
|
import { interopDefault } from "./module.mjs";
|
|
7
7
|
import { deepApply, memoize, objectEntries, objectKeys } from "./object.mjs";
|
|
8
8
|
import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.mjs";
|
|
9
|
-
import { Err, Ok, err, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
9
|
+
import { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult } from "./result.mjs";
|
|
10
10
|
import { generateRandomId, template } from "./string.mjs";
|
|
11
11
|
|
|
12
|
-
export { Err, Ok, cloneJSON, createCSV, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
|
12
|
+
export { Err, Ok, cloneJSON, createCSV, createCSVAsync, createCSVStream, createDefu, createEmitter, deepApply, defu, err, escapeCSVValue, generateRandomId, getPathname, interopDefault, isErr, isOk, joinURL, memoize, objectEntries, objectKeys, ok, parseCSV, parseCSVFromLines, parseCSVStream, template, toArray, toResult, tryCatch, tryParseJSON, unwrapResult, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash };
|
package/dist/json.mjs
CHANGED
|
@@ -21,11 +21,11 @@ function tryParseJSON(value) {
|
|
|
21
21
|
*/
|
|
22
22
|
function cloneJSON(value) {
|
|
23
23
|
if (typeof value !== "object" || value === null) return value;
|
|
24
|
-
if (Array.isArray(value)) return value.map((
|
|
24
|
+
if (Array.isArray(value)) return value.map((element) => typeof element !== "object" || element === null ? element : cloneJSON(element));
|
|
25
25
|
const result = {};
|
|
26
|
-
for (const
|
|
27
|
-
const
|
|
28
|
-
result[
|
|
26
|
+
for (const key in value) {
|
|
27
|
+
const propertyValue = value[key];
|
|
28
|
+
result[key] = typeof propertyValue !== "object" || propertyValue === null ? propertyValue : cloneJSON(propertyValue);
|
|
29
29
|
}
|
|
30
30
|
return result;
|
|
31
31
|
}
|
package/dist/path.d.mts
CHANGED
|
@@ -10,11 +10,13 @@ declare function withoutLeadingSlash(path?: string): string;
|
|
|
10
10
|
*/
|
|
11
11
|
declare function withLeadingSlash(path?: string): string;
|
|
12
12
|
/**
|
|
13
|
-
* Removes the trailing slash from the given path if it has one
|
|
13
|
+
* Removes the trailing slash from the given path if it has one,
|
|
14
|
+
* preserving query strings and hash fragments.
|
|
14
15
|
*/
|
|
15
16
|
declare function withoutTrailingSlash(path?: string): string;
|
|
16
17
|
/**
|
|
17
|
-
* Adds a trailing slash to the given path if it does not already have one
|
|
18
|
+
* Adds a trailing slash to the given path if it does not already have one,
|
|
19
|
+
* preserving query strings and hash fragments.
|
|
18
20
|
*/
|
|
19
21
|
declare function withTrailingSlash(path?: string): string;
|
|
20
22
|
/**
|
|
@@ -30,7 +32,7 @@ declare function withBase(input?: string, base?: string): string;
|
|
|
30
32
|
*/
|
|
31
33
|
declare function withoutBase(input?: string, base?: string): string;
|
|
32
34
|
/**
|
|
33
|
-
* Returns the pathname of the given path, which is the path without the query string.
|
|
35
|
+
* Returns the pathname of the given path, which is the path without the query string or hash.
|
|
34
36
|
*/
|
|
35
37
|
declare function getPathname(path?: string): string;
|
|
36
38
|
/**
|
package/dist/path.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Removes the leading slash from the given path if it has one.
|
|
4
4
|
*/
|
|
5
5
|
function withoutLeadingSlash(path) {
|
|
6
|
-
if (!path
|
|
6
|
+
if (!path) return "";
|
|
7
7
|
return path[0] === "/" ? path.slice(1) : path;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
@@ -14,31 +14,48 @@ function withLeadingSlash(path) {
|
|
|
14
14
|
return path[0] === "/" ? path : `/${path}`;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Removes the trailing slash from the given path if it has one
|
|
17
|
+
* Removes the trailing slash from the given path if it has one,
|
|
18
|
+
* preserving query strings and hash fragments.
|
|
18
19
|
*/
|
|
19
20
|
function withoutTrailingSlash(path) {
|
|
20
21
|
if (!path || path === "/") return "/";
|
|
21
|
-
|
|
22
|
+
let pathEnd = path.length;
|
|
23
|
+
const queryIndex = path.indexOf("?");
|
|
24
|
+
const hashIndex = path.indexOf("#");
|
|
25
|
+
if (queryIndex !== -1) pathEnd = queryIndex;
|
|
26
|
+
if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
|
|
27
|
+
if (pathEnd > 0 && path[pathEnd - 1] === "/") {
|
|
28
|
+
if (pathEnd === 1) return path;
|
|
29
|
+
return path.slice(0, pathEnd - 1) + path.slice(pathEnd);
|
|
30
|
+
}
|
|
31
|
+
return path;
|
|
22
32
|
}
|
|
23
33
|
/**
|
|
24
|
-
* Adds a trailing slash to the given path if it does not already have one
|
|
34
|
+
* Adds a trailing slash to the given path if it does not already have one,
|
|
35
|
+
* preserving query strings and hash fragments.
|
|
25
36
|
*/
|
|
26
37
|
function withTrailingSlash(path) {
|
|
27
38
|
if (!path || path === "/") return "/";
|
|
28
|
-
|
|
39
|
+
let pathEnd = path.length;
|
|
40
|
+
const queryIndex = path.indexOf("?");
|
|
41
|
+
const hashIndex = path.indexOf("#");
|
|
42
|
+
if (queryIndex !== -1) pathEnd = queryIndex;
|
|
43
|
+
if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
|
|
44
|
+
if (pathEnd > 0 && path[pathEnd - 1] === "/") return path;
|
|
45
|
+
return `${path.slice(0, pathEnd)}/${path.slice(pathEnd)}`;
|
|
29
46
|
}
|
|
30
47
|
/**
|
|
31
48
|
* Joins the given URL path segments, ensuring that there is only one slash between them.
|
|
32
49
|
*/
|
|
33
50
|
function joinURL(...paths) {
|
|
34
51
|
let result = "";
|
|
35
|
-
for (
|
|
36
|
-
|
|
37
|
-
if (!result
|
|
38
|
-
result = path
|
|
52
|
+
for (const path of paths) {
|
|
53
|
+
if (!path) continue;
|
|
54
|
+
if (!result) {
|
|
55
|
+
result = path;
|
|
39
56
|
continue;
|
|
40
57
|
}
|
|
41
|
-
if (
|
|
58
|
+
if (path === "/") continue;
|
|
42
59
|
const resultHasTrailing = result[result.length - 1] === "/";
|
|
43
60
|
const pathHasLeading = path[0] === "/";
|
|
44
61
|
if (resultHasTrailing && pathHasLeading) result += path.slice(1);
|
|
@@ -53,7 +70,7 @@ function joinURL(...paths) {
|
|
|
53
70
|
function withBase(input = "", base = "") {
|
|
54
71
|
if (!base || base === "/") return input;
|
|
55
72
|
const _base = withoutTrailingSlash(base);
|
|
56
|
-
if (input.startsWith(_base)) return input;
|
|
73
|
+
if (input.startsWith(_base) && (input.length === _base.length || input[_base.length] === "/" || input[_base.length] === "?")) return input;
|
|
57
74
|
return joinURL(_base, input);
|
|
58
75
|
}
|
|
59
76
|
/**
|
|
@@ -62,15 +79,21 @@ function withBase(input = "", base = "") {
|
|
|
62
79
|
function withoutBase(input = "", base = "") {
|
|
63
80
|
if (!base || base === "/") return input;
|
|
64
81
|
const _base = withoutTrailingSlash(base);
|
|
65
|
-
if (!input.startsWith(_base)) return input;
|
|
82
|
+
if (!input.startsWith(_base) || input.length !== _base.length && input[_base.length] !== "/" && input[_base.length] !== "?") return input;
|
|
66
83
|
const trimmed = input.slice(_base.length);
|
|
67
84
|
return trimmed[0] === "/" ? trimmed : `/${trimmed}`;
|
|
68
85
|
}
|
|
69
86
|
/**
|
|
70
|
-
* Returns the pathname of the given path, which is the path without the query string.
|
|
87
|
+
* Returns the pathname of the given path, which is the path without the query string or hash.
|
|
71
88
|
*/
|
|
72
89
|
function getPathname(path = "/") {
|
|
73
|
-
|
|
90
|
+
if (!path.startsWith("/")) return new URL(path, "http://localhost").pathname;
|
|
91
|
+
let pathEnd = path.length;
|
|
92
|
+
const queryIndex = path.indexOf("?");
|
|
93
|
+
const hashIndex = path.indexOf("#");
|
|
94
|
+
if (queryIndex !== -1) pathEnd = queryIndex;
|
|
95
|
+
if (hashIndex !== -1 && hashIndex < pathEnd) pathEnd = hashIndex;
|
|
96
|
+
return path.slice(0, pathEnd) || "/";
|
|
74
97
|
}
|
|
75
98
|
/**
|
|
76
99
|
* Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
|
|
@@ -80,7 +103,7 @@ function withQuery(input, query) {
|
|
|
80
103
|
const searchIndex = input.indexOf("?");
|
|
81
104
|
const hasExistingParams = searchIndex !== -1;
|
|
82
105
|
const base = hasExistingParams ? input.slice(0, searchIndex) : input;
|
|
83
|
-
const searchParams = hasExistingParams ?
|
|
106
|
+
const searchParams = new URLSearchParams(hasExistingParams ? input.slice(searchIndex + 1) : void 0);
|
|
84
107
|
for (const [key, value] of Object.entries(query)) {
|
|
85
108
|
if (value === void 0) {
|
|
86
109
|
searchParams.delete(key);
|
package/dist/result.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/result.d.ts
|
|
2
|
-
type Result<T, E> = Ok<T> | Err<E>;
|
|
2
|
+
type Result<T, E> = Ok<T, E> | Err<T, E>;
|
|
3
3
|
interface OkData<T> {
|
|
4
4
|
value: T;
|
|
5
5
|
error: undefined;
|
|
@@ -9,25 +9,67 @@ interface ErrData<E> {
|
|
|
9
9
|
error: E;
|
|
10
10
|
}
|
|
11
11
|
type ResultData<T, E> = OkData<T> | ErrData<E>;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Successful result variant.
|
|
14
|
+
* @template T Success value type.
|
|
15
|
+
* @template E Error type (phantom - for type unification).
|
|
16
|
+
*/
|
|
17
|
+
declare class Ok<T, E = never> {
|
|
13
18
|
readonly value: T;
|
|
14
19
|
readonly ok: true;
|
|
15
20
|
constructor(value: T);
|
|
21
|
+
/** Transforms the success value. */
|
|
22
|
+
map<U>(fn: (value: T) => U): Ok<U, E>;
|
|
23
|
+
/** No-op on Ok, returns self with new error type. */
|
|
24
|
+
mapError<E2>(_fn: (error: E) => E2): Ok<T, E2>;
|
|
25
|
+
/** Chains a Result-returning function. */
|
|
26
|
+
andThen<U, E2>(fn: (value: T) => Result<U, E2>): Result<U, E | E2>;
|
|
27
|
+
/** Extracts the value. */
|
|
28
|
+
unwrap(_message?: string): T;
|
|
29
|
+
/** Returns the value, ignoring the fallback. */
|
|
30
|
+
unwrapOr<U>(_fallback: U): T;
|
|
31
|
+
/** Pattern matches on the Result. */
|
|
32
|
+
match<R>(handlers: {
|
|
33
|
+
ok: (value: T) => R;
|
|
34
|
+
err: (error: E) => R;
|
|
35
|
+
}): R;
|
|
16
36
|
}
|
|
17
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Error result variant.
|
|
39
|
+
* @template T Success type (phantom - for type unification).
|
|
40
|
+
* @template E Error value type.
|
|
41
|
+
*/
|
|
42
|
+
declare class Err<T, E> {
|
|
18
43
|
readonly error: E;
|
|
19
44
|
readonly ok: false;
|
|
20
45
|
constructor(error: E);
|
|
46
|
+
/** No-op on Err, returns self with new value type. */
|
|
47
|
+
map<U>(_fn: (value: T) => U): Err<U, E>;
|
|
48
|
+
/** Transforms the error value. */
|
|
49
|
+
mapError<E2>(fn: (error: E) => E2): Err<T, E2>;
|
|
50
|
+
/** No-op on Err, returns self with widened error type. */
|
|
51
|
+
andThen<U, E2>(_fn: (value: T) => Result<U, E2>): Err<U, E | E2>;
|
|
52
|
+
/** Throws an error with the given message. */
|
|
53
|
+
unwrap(message?: string): never;
|
|
54
|
+
/** Returns the fallback value. */
|
|
55
|
+
unwrapOr<U>(fallback: U): T | U;
|
|
56
|
+
/** Pattern matches on the Result. */
|
|
57
|
+
match<R>(handlers: {
|
|
58
|
+
ok: (value: T) => R;
|
|
59
|
+
err: (error: E) => R;
|
|
60
|
+
}): R;
|
|
21
61
|
}
|
|
22
|
-
declare function ok<T>(value: T): Ok<T>;
|
|
23
|
-
declare function err<E extends string = string>(error: E): Err<E>;
|
|
24
|
-
declare function err<E = unknown>(error: E): Err<E>;
|
|
62
|
+
declare function ok<T, E = never>(value: T): Ok<T, E>;
|
|
63
|
+
declare function err<T = never, E extends string = string>(error: E): Err<T, E>;
|
|
64
|
+
declare function err<T = never, E = unknown>(error: E): Err<T, E>;
|
|
65
|
+
declare function isOk<T, E>(result: Result<T, E>): result is Ok<T, E>;
|
|
66
|
+
declare function isErr<T, E>(result: Result<T, E>): result is Err<T, E>;
|
|
25
67
|
declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>;
|
|
26
68
|
declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>;
|
|
27
|
-
declare function unwrapResult<T>(result: Ok<T>): OkData<T>;
|
|
28
|
-
declare function unwrapResult<E>(result: Err<E>): ErrData<E>;
|
|
69
|
+
declare function unwrapResult<T, E>(result: Ok<T, E>): OkData<T>;
|
|
70
|
+
declare function unwrapResult<T, E>(result: Err<T, E>): ErrData<E>;
|
|
29
71
|
declare function unwrapResult<T, E>(result: Result<T, E>): ResultData<T, E>;
|
|
30
72
|
declare function tryCatch<T, E = unknown>(fn: () => T): ResultData<T, E>;
|
|
31
73
|
declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<ResultData<T, E>>;
|
|
32
74
|
//#endregion
|
|
33
|
-
export { Err, ErrData, Ok, OkData, Result, ResultData, err, ok, toResult, tryCatch, unwrapResult };
|
|
75
|
+
export { Err, ErrData, Ok, OkData, Result, ResultData, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult };
|
package/dist/result.mjs
CHANGED
|
@@ -1,17 +1,75 @@
|
|
|
1
1
|
//#region src/result.ts
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Successful result variant.
|
|
4
|
+
* @template T Success value type.
|
|
5
|
+
* @template E Error type (phantom - for type unification).
|
|
6
|
+
*/
|
|
7
|
+
var Ok = class Ok {
|
|
3
8
|
value;
|
|
4
9
|
ok = true;
|
|
5
10
|
constructor(value) {
|
|
6
11
|
this.value = value;
|
|
7
12
|
}
|
|
13
|
+
/** Transforms the success value. */
|
|
14
|
+
map(fn) {
|
|
15
|
+
return new Ok(fn(this.value));
|
|
16
|
+
}
|
|
17
|
+
/** No-op on Ok, returns self with new error type. */
|
|
18
|
+
mapError(_fn) {
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
/** Chains a Result-returning function. */
|
|
22
|
+
andThen(fn) {
|
|
23
|
+
return fn(this.value);
|
|
24
|
+
}
|
|
25
|
+
/** Extracts the value. */
|
|
26
|
+
unwrap(_message) {
|
|
27
|
+
return this.value;
|
|
28
|
+
}
|
|
29
|
+
/** Returns the value, ignoring the fallback. */
|
|
30
|
+
unwrapOr(_fallback) {
|
|
31
|
+
return this.value;
|
|
32
|
+
}
|
|
33
|
+
/** Pattern matches on the Result. */
|
|
34
|
+
match(handlers) {
|
|
35
|
+
return handlers.ok(this.value);
|
|
36
|
+
}
|
|
8
37
|
};
|
|
9
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Error result variant.
|
|
40
|
+
* @template T Success type (phantom - for type unification).
|
|
41
|
+
* @template E Error value type.
|
|
42
|
+
*/
|
|
43
|
+
var Err = class Err {
|
|
10
44
|
error;
|
|
11
45
|
ok = false;
|
|
12
46
|
constructor(error) {
|
|
13
47
|
this.error = error;
|
|
14
48
|
}
|
|
49
|
+
/** No-op on Err, returns self with new value type. */
|
|
50
|
+
map(_fn) {
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/** Transforms the error value. */
|
|
54
|
+
mapError(fn) {
|
|
55
|
+
return new Err(fn(this.error));
|
|
56
|
+
}
|
|
57
|
+
/** No-op on Err, returns self with widened error type. */
|
|
58
|
+
andThen(_fn) {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
/** Throws an error with the given message. */
|
|
62
|
+
unwrap(message) {
|
|
63
|
+
throw new Error(message ?? `Unwrap called on Err: ${String(this.error)}`);
|
|
64
|
+
}
|
|
65
|
+
/** Returns the fallback value. */
|
|
66
|
+
unwrapOr(fallback) {
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
/** Pattern matches on the Result. */
|
|
70
|
+
match(handlers) {
|
|
71
|
+
return handlers.err(this.error);
|
|
72
|
+
}
|
|
15
73
|
};
|
|
16
74
|
function ok(value) {
|
|
17
75
|
return new Ok(value);
|
|
@@ -19,6 +77,12 @@ function ok(value) {
|
|
|
19
77
|
function err(error) {
|
|
20
78
|
return new Err(error);
|
|
21
79
|
}
|
|
80
|
+
function isOk(result) {
|
|
81
|
+
return result.ok === true;
|
|
82
|
+
}
|
|
83
|
+
function isErr(result) {
|
|
84
|
+
return result.ok === false;
|
|
85
|
+
}
|
|
22
86
|
function toResult(fnOrPromise) {
|
|
23
87
|
if (fnOrPromise instanceof Promise) return fnOrPromise.then(ok).catch(err);
|
|
24
88
|
try {
|
|
@@ -42,4 +106,4 @@ function tryCatch(fnOrPromise) {
|
|
|
42
106
|
}
|
|
43
107
|
|
|
44
108
|
//#endregion
|
|
45
|
-
export { Err, Ok, err, ok, toResult, tryCatch, unwrapResult };
|
|
109
|
+
export { Err, Ok, err, isErr, isOk, ok, toResult, tryCatch, unwrapResult };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "utilful",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
5
|
-
"packageManager": "pnpm@10.
|
|
4
|
+
"version": "2.5.0",
|
|
5
|
+
"packageManager": "pnpm@10.28.0",
|
|
6
6
|
"description": "A collection of TypeScript utilities",
|
|
7
7
|
"author": "Johann Schopplich <hello@johannschopplich.com>",
|
|
8
8
|
"license": "MIT",
|
|
@@ -78,13 +78,13 @@
|
|
|
78
78
|
"release": "bumpp"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@antfu/eslint-config": "^
|
|
82
|
-
"@types/node": "^
|
|
83
|
-
"bumpp": "^10.
|
|
84
|
-
"eslint": "^9.39.
|
|
85
|
-
"tsdown": "^0.
|
|
81
|
+
"@antfu/eslint-config": "^7.0.1",
|
|
82
|
+
"@types/node": "^25.0.9",
|
|
83
|
+
"bumpp": "^10.4.0",
|
|
84
|
+
"eslint": "^9.39.2",
|
|
85
|
+
"tsdown": "^0.19.0",
|
|
86
86
|
"typescript": "^5.9.3",
|
|
87
|
-
"vitest": "^4.0.
|
|
87
|
+
"vitest": "^4.0.17"
|
|
88
88
|
},
|
|
89
89
|
"pnpm": {
|
|
90
90
|
"onlyBuiltDependencies": [
|