utilful 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,14 +55,16 @@ Converts an array of objects to a comma-separated values (CSV) string that conta
55
55
  ```ts
56
56
  declare function createCSV<T extends Record<string, unknown>>(
57
57
  data: T[],
58
- columns: (keyof T)[],
58
+ columns: readonly (keyof T)[],
59
59
  options?: {
60
60
  /** @default ',' */
61
61
  delimiter?: string
62
62
  /** @default true */
63
- includeHeaders?: boolean
63
+ addHeader?: boolean
64
64
  /** @default false */
65
65
  quoteAll?: boolean
66
+ /** @default '\n' */
67
+ lineEnding?: string
66
68
  }
67
69
  ): string
68
70
  ```
@@ -94,10 +96,18 @@ type CSVRow<T extends string = string> = Record<T, string>
94
96
  declare function parseCSV<Header extends string>(
95
97
  csv?: string | null | undefined,
96
98
  options?: {
97
- /** @default ',' */
99
+ /** @default ',' */
98
100
  delimiter?: string
99
- /** @default true */
100
- trimValues?: boolean
101
+ /**
102
+ * Trim whitespace from headers and values.
103
+ * @default true
104
+ */
105
+ trim?: boolean
106
+ /**
107
+ * Throw error if row has more fields than headers.
108
+ * @default true
109
+ */
110
+ strict?: boolean
101
111
  }
102
112
  ): CSVRow<Header>[]
103
113
  ```
@@ -105,9 +115,11 @@ declare function parseCSV<Header extends string>(
105
115
  **Example:**
106
116
 
107
117
  ```ts
108
- const csv = `name,age
118
+ const csv = `
119
+ name,age
109
120
  John,30
110
- Jane,25`
121
+ Jane,25
122
+ `
111
123
 
112
124
  const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
113
125
  ```
@@ -18,13 +18,15 @@ type CSVRow<T extends string = string> = Record<T, string>;
18
18
  * // John,30
19
19
  * // Jane,25
20
20
  */
21
- declare function createCSV<T extends Record<string, unknown>>(data: T[], columns: (keyof T)[], options?: {
21
+ declare function createCSV<T extends Record<string, unknown>>(data: T[], columns: readonly (keyof T)[], options?: {
22
22
  /** @default ',' */
23
23
  delimiter?: string;
24
24
  /** @default true */
25
25
  addHeader?: boolean;
26
26
  /** @default false */
27
27
  quoteAll?: boolean;
28
+ /** @default '\n' */
29
+ lineEnding?: string;
28
30
  }): string;
29
31
  /**
30
32
  * Escapes a value for a CSV string.
@@ -61,8 +63,16 @@ declare function escapeCSVValue(value: unknown, options?: {
61
63
  declare function parseCSV<Header extends string>(csv?: string | null | undefined, options?: {
62
64
  /** @default ',' */
63
65
  delimiter?: string;
64
- /** @default true */
65
- trimValues?: boolean;
66
+ /**
67
+ * Trim whitespace from headers and values.
68
+ * @default true
69
+ */
70
+ trim?: boolean;
71
+ /**
72
+ * Throw error if row has more fields than headers.
73
+ * @default true
74
+ */
75
+ strict?: boolean;
66
76
  }): CSVRow<Header>[];
67
77
  //#endregion
68
78
  export { CSVRow, createCSV, escapeCSVValue, parseCSV };
package/dist/csv.mjs ADDED
@@ -0,0 +1,119 @@
1
+ //#region src/csv.ts
2
+ /**
3
+ * Converts an array of objects to a comma-separated values (CSV) string
4
+ * that contains only the `columns` specified.
5
+ *
6
+ * @example
7
+ * const data = [
8
+ * { name: 'John', age: '30', city: 'New York' },
9
+ * { name: 'Jane', age: '25', city: 'Boston' }
10
+ * ]
11
+ *
12
+ * const csv = createCSV(data, ['name', 'age'])
13
+ * // name,age
14
+ * // John,30
15
+ * // Jane,25
16
+ */
17
+ function createCSV(data, columns, options = {}) {
18
+ const { delimiter = ",", addHeader = true, quoteAll = false, lineEnding = "\n" } = options;
19
+ const formatCell = (value) => escapeCSVValue(value, {
20
+ delimiter,
21
+ quoteAll
22
+ });
23
+ const rows = data.map((obj) => columns.map((key) => formatCell(obj[key])).join(delimiter));
24
+ if (addHeader) rows.unshift(columns.map(formatCell).join(delimiter));
25
+ return rows.join(lineEnding);
26
+ }
27
+ /**
28
+ * Escapes a value for a CSV string.
29
+ *
30
+ * @remarks
31
+ * Returns an empty string if the value is `null` or `undefined`.
32
+ * Values containing delimiters, quotes, or line breaks are quoted.
33
+ * Within quoted values, double quotes are escaped by doubling them.
34
+ *
35
+ * @example
36
+ * escapeCSVValue('hello, world') // "hello, world"
37
+ * escapeCSVValue('contains "quotes"') // "contains ""quotes"""
38
+ */
39
+ function escapeCSVValue(value, options = {}) {
40
+ const { delimiter = ",", quoteAll = false } = options;
41
+ if (value == null) return "";
42
+ const coercedValue = String(value);
43
+ if (quoteAll || coercedValue.includes(delimiter) || coercedValue.includes("\"") || coercedValue.includes("\n") || coercedValue.includes("\r")) return `"${coercedValue.replaceAll("\"", "\"\"")}"`;
44
+ return coercedValue;
45
+ }
46
+ /**
47
+ * Parses a comma-separated values (CSV) string into an array of objects.
48
+ *
49
+ * @remarks
50
+ * The first row of the CSV string is used as the header row.
51
+ *
52
+ * @example
53
+ * const csv = `name,age
54
+ * John,30
55
+ * Jane,25`
56
+ *
57
+ * const data = parseCSV<'name' | 'age'>(csv)
58
+ * // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
59
+ */
60
+ function parseCSV(csv, options = {}) {
61
+ if (!csv?.trim()) return [];
62
+ const rows = [];
63
+ let currentRow = [];
64
+ let currentField = "";
65
+ let inQuotes = false;
66
+ const { delimiter = ",", trim = true, strict = true } = options;
67
+ const appendField = () => {
68
+ currentRow.push(currentField);
69
+ currentField = "";
70
+ };
71
+ const appendRow = () => {
72
+ appendField();
73
+ rows.push(currentRow);
74
+ currentRow = [];
75
+ };
76
+ for (let i = 0; i < csv.length; i++) {
77
+ const character = csv[i];
78
+ const nextCharacter = i + 1 < csv.length ? csv[i + 1] : "";
79
+ if (character === "\"") if (inQuotes && nextCharacter === "\"") {
80
+ currentField += "\"";
81
+ i++;
82
+ } else inQuotes = !inQuotes;
83
+ else if (character === delimiter && !inQuotes) appendField();
84
+ else if ((character === "\n" || character === "\r" && nextCharacter === "\n") && !inQuotes) {
85
+ if (character === "\r") i++;
86
+ appendRow();
87
+ } else currentField += character;
88
+ }
89
+ if (currentField || currentRow.length > 0) appendRow();
90
+ if (rows.length <= 1) return [];
91
+ const [headerRow] = rows;
92
+ if (!headerRow) return [];
93
+ const headers = trim ? headerRow.map((h) => h.trim()) : headerRow;
94
+ if (headers.filter((h) => h.length === 0).length > 0) {
95
+ const positions = headers.map((h, i) => h.length === 0 ? i + 1 : -1).filter((i) => i > 0).join(", ");
96
+ throw new SyntaxError(`CSV header row contains empty column name(s) at position(s): ${positions}`);
97
+ }
98
+ const duplicateHeaderNames = headers.filter((h, i) => headers.indexOf(h) !== i);
99
+ if (duplicateHeaderNames.length > 0) throw new SyntaxError(`CSV header row contains duplicate column name(s): ${[...new Set(duplicateHeaderNames)].join(", ")}`);
100
+ const isFieldPopulated = trim ? (field) => field.trim().length > 0 : (field) => field.length > 0;
101
+ return rows.slice(1).filter((row) => row.length > 1 || row.some(isFieldPopulated)).map((fieldValues, rowIndex) => {
102
+ if (fieldValues.length > headers.length) {
103
+ const containsNonEmptyOverflow = fieldValues.slice(headers.length).some(isFieldPopulated);
104
+ if (strict && containsNonEmptyOverflow) {
105
+ const expectedCount = headers.length;
106
+ const actualCount = fieldValues.length;
107
+ const excessCount = actualCount - expectedCount;
108
+ throw new SyntaxError(`CSV row ${rowIndex + 2} has ${excessCount} extra field(s): expected ${expectedCount} column(s), found ${actualCount}`);
109
+ }
110
+ }
111
+ return Object.fromEntries(headers.map((header, columnIndex) => {
112
+ const untrimmedValue = columnIndex < fieldValues.length ? fieldValues[columnIndex] ?? "" : "";
113
+ return [header, trim ? untrimmedValue.trim() : untrimmedValue];
114
+ }));
115
+ });
116
+ }
117
+
118
+ //#endregion
119
+ export { createCSV, escapeCSVValue, parseCSV };
@@ -1,5 +1,4 @@
1
1
  //#region src/defu.d.ts
2
- // Forked from unjs/defu (MIT)
3
2
  type PlainObject = Record<PropertyKey, any>;
4
3
  type DefuMerger<T extends PlainObject = PlainObject> = (target: T, key: PropertyKey, value: any, namespace: string) => boolean | void;
5
4
  /**
@@ -1,13 +1,9 @@
1
1
  //#region src/emitter.d.ts
2
- /* eslint-disable ts/method-signature-style */
3
2
  type EventType = string | symbol;
4
- // An event handler can take an optional event argument
5
- // and should not return a value
6
3
  type Handler<T = unknown> = (event: T) => void;
7
4
  type WildcardHandler<T = Record<string, unknown>> = (type: keyof T, event: T[keyof T]) => void;
8
5
  type EventHandlerList<T = unknown> = Handler<T>[];
9
6
  type WildCardEventHandlerList<T = Record<string, unknown>> = WildcardHandler<T>[];
10
- // A map of event types and their corresponding event handlers.
11
7
  type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<keyof Events | "*", EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>>;
12
8
  interface Emitter<Events extends Record<EventType, unknown>> {
13
9
  events: EventHandlerMap<Events>;
@@ -0,0 +1,12 @@
1
+ import { MaybeArray, toArray } from "./array.mjs";
2
+ import { CSVRow, createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
3
+ import { DefuFn, DefuMerger, createDefu, defu } from "./defu.mjs";
4
+ import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter } from "./emitter.mjs";
5
+ import { cloneJSON, tryParseJSON } from "./json.mjs";
6
+ import { interopDefault } from "./module.mjs";
7
+ import { deepApply, memoize, objectEntries, objectKeys } from "./object.mjs";
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";
10
+ import { generateRandomId, template } from "./string.mjs";
11
+ import { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection } from "./types.mjs";
12
+ export { AutocompletableString, BrandedType, 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 };
@@ -1,12 +1,12 @@
1
- import { toArray } from "./array.js";
2
- import { createCSV, escapeCSVValue, parseCSV } from "./csv.js";
3
- import { createDefu, defu } from "./defu.js";
4
- import { createEmitter } from "./emitter.js";
5
- import { cloneJSON, tryParseJSON } from "./json.js";
6
- import { interopDefault } from "./module.js";
7
- import { deepApply, memoize, objectEntries, objectKeys } from "./object.js";
8
- import { getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.js";
9
- import { Err, Ok, err, ok, toResult, tryCatch, unwrapResult } from "./result.js";
10
- import { generateRandomId, template } from "./string.js";
1
+ import { toArray } from "./array.mjs";
2
+ import { createCSV, escapeCSVValue, parseCSV } from "./csv.mjs";
3
+ import { createDefu, defu } from "./defu.mjs";
4
+ import { createEmitter } from "./emitter.mjs";
5
+ import { cloneJSON, tryParseJSON } from "./json.mjs";
6
+ import { interopDefault } from "./module.mjs";
7
+ import { deepApply, memoize, objectEntries, objectKeys } from "./object.mjs";
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";
10
+ import { generateRandomId, template } from "./string.mjs";
11
11
 
12
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 };
@@ -3,5 +3,9 @@ type AutocompletableString = string & {};
3
3
  type LooseAutocomplete<T extends string> = T | AutocompletableString;
4
4
  /** Also commonly referred to as `Prettify` */
5
5
  type UnifyIntersection<T> = { [K in keyof T]: T[K] } & {};
6
+ declare const brand: unique symbol;
7
+ type BrandedType<T, B> = T & {
8
+ [brand]: B;
9
+ };
6
10
  //#endregion
7
- export { AutocompletableString, LooseAutocomplete, UnifyIntersection };
11
+ export { AutocompletableString, BrandedType, LooseAutocomplete, UnifyIntersection };
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export { };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "utilful",
3
3
  "type": "module",
4
- "version": "2.1.1",
5
- "packageManager": "pnpm@10.11.1",
4
+ "version": "2.2.0",
5
+ "packageManager": "pnpm@10.21.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": "^4.13.3",
82
- "@types/node": "^22.15.29",
83
- "bumpp": "^10.1.1",
84
- "eslint": "^9.28.0",
85
- "tsdown": "^0.12.6",
86
- "typescript": "^5.8.3",
87
- "vitest": "^3.2.1"
81
+ "@antfu/eslint-config": "^6.2.0",
82
+ "@types/node": "^24.10.0",
83
+ "bumpp": "^10.3.1",
84
+ "eslint": "^9.39.1",
85
+ "tsdown": "^0.16.1",
86
+ "typescript": "^5.9.3",
87
+ "vitest": "^4.0.8"
88
88
  },
89
89
  "pnpm": {
90
90
  "onlyBuiltDependencies": [
package/dist/csv.js DELETED
@@ -1,98 +0,0 @@
1
- //#region src/csv.ts
2
- /**
3
- * Converts an array of objects to a comma-separated values (CSV) string
4
- * that contains only the `columns` specified.
5
- *
6
- * @example
7
- * const data = [
8
- * { name: 'John', age: '30', city: 'New York' },
9
- * { name: 'Jane', age: '25', city: 'Boston' }
10
- * ]
11
- *
12
- * const csv = createCSV(data, ['name', 'age'])
13
- * // name,age
14
- * // John,30
15
- * // Jane,25
16
- */
17
- function createCSV(data, columns, options = {}) {
18
- const { delimiter = ",", addHeader = true, quoteAll = false } = options;
19
- const escapeAndQuote = (value) => escapeCSVValue(value, {
20
- delimiter,
21
- quoteAll
22
- });
23
- const rows = data.map((obj) => columns.map((key) => escapeAndQuote(obj[key])).join(delimiter));
24
- if (addHeader) rows.unshift(columns.map(escapeAndQuote).join(delimiter));
25
- return rows.join("\n");
26
- }
27
- /**
28
- * Escapes a value for a CSV string.
29
- *
30
- * @remarks
31
- * Returns an empty string if the value is `null` or `undefined`.
32
- * Values containing delimiters, quotes, or line breaks are quoted.
33
- * Within quoted values, double quotes are escaped by doubling them.
34
- *
35
- * @example
36
- * escapeCSVValue('hello, world') // "hello, world"
37
- * escapeCSVValue('contains "quotes"') // "contains ""quotes"""
38
- */
39
- function escapeCSVValue(value, options = {}) {
40
- const { delimiter = ",", quoteAll = false } = options;
41
- if (value == null) return "";
42
- const stringValue = String(value);
43
- const needsQuoting = quoteAll || stringValue.includes(delimiter) || stringValue.includes("\"") || stringValue.includes("\n") || stringValue.includes("\r");
44
- if (needsQuoting) return `"${stringValue.replaceAll("\"", "\"\"")}"`;
45
- return stringValue;
46
- }
47
- /**
48
- * Parses a comma-separated values (CSV) string into an array of objects.
49
- *
50
- * @remarks
51
- * The first row of the CSV string is used as the header row.
52
- *
53
- * @example
54
- * const csv = `name,age
55
- * John,30
56
- * Jane,25`
57
- *
58
- * const data = parseCSV<'name' | 'age'>(csv)
59
- * // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
60
- */
61
- function parseCSV(csv, options = {}) {
62
- if (!csv?.trim()) return [];
63
- const rows = [];
64
- let currentRow = [];
65
- let currentField = "";
66
- let inQuotes = false;
67
- const { delimiter = ",", trimValues = true } = options;
68
- for (let i = 0; i < csv.length; i++) {
69
- const char = csv[i];
70
- const nextChar = i + 1 < csv.length ? csv[i + 1] : "";
71
- if (char === "\"") if (inQuotes && nextChar === "\"") {
72
- currentField += "\"";
73
- i++;
74
- } else inQuotes = !inQuotes;
75
- else if (char === delimiter && !inQuotes) {
76
- currentRow.push(trimValues ? currentField.trim() : currentField);
77
- currentField = "";
78
- } else if ((char === "\n" || char === "\r" && nextChar === "\n") && !inQuotes) {
79
- if (char === "\r") i++;
80
- currentRow.push(trimValues ? currentField.trim() : currentField);
81
- rows.push(currentRow);
82
- currentRow = [];
83
- currentField = "";
84
- } else currentField += char;
85
- }
86
- if (currentField || currentRow.length > 0) {
87
- currentRow.push(trimValues ? currentField.trim() : currentField);
88
- rows.push(currentRow);
89
- }
90
- if (rows.length <= 1) return [];
91
- const headers = rows[0];
92
- return rows.slice(1).filter((row) => row.some((field) => field.trim().length > 0)).map((values) => {
93
- return Object.fromEntries(headers.map((header, index) => [header, index < values.length ? values[index] : ""]));
94
- });
95
- }
96
-
97
- //#endregion
98
- export { createCSV, escapeCSVValue, parseCSV };
package/dist/index.d.ts DELETED
@@ -1,12 +0,0 @@
1
- import { MaybeArray, toArray } from "./array.js";
2
- import { CSVRow, createCSV, escapeCSVValue, parseCSV } from "./csv.js";
3
- import { DefuFn, DefuMerger, createDefu, defu } from "./defu.js";
4
- import { Emitter, EventHandlerList, EventHandlerMap, EventType, Handler, WildCardEventHandlerList, WildcardHandler, createEmitter } from "./emitter.js";
5
- import { cloneJSON, tryParseJSON } from "./json.js";
6
- import { interopDefault } from "./module.js";
7
- import { deepApply, memoize, objectEntries, objectKeys } from "./object.js";
8
- import { QueryObject, QueryValue, getPathname, joinURL, withBase, withLeadingSlash, withQuery, withTrailingSlash, withoutBase, withoutLeadingSlash, withoutTrailingSlash } from "./path.js";
9
- import { Err, ErrData, Ok, OkData, Result, ResultData, err, ok, toResult, tryCatch, unwrapResult } from "./result.js";
10
- import { generateRandomId, template } from "./string.js";
11
- import { AutocompletableString, LooseAutocomplete, UnifyIntersection } from "./types.js";
12
- export { AutocompletableString, 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 };
package/dist/types.js DELETED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes