utilful 1.0.0 → 1.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-PRESENT Johann Schopplich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,378 @@
1
+ # utilful
2
+
3
+ A collection of TypeScript utilities that I use across my projects.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [API](#api)
9
+ - [Array](#array)
10
+ - [CSV](#csv)
11
+ - [Emitter](#emitter)
12
+ - [JSON](#json)
13
+ - [Lazy](#lazy)
14
+ - [Module](#module)
15
+ - [Object](#object)
16
+ - [Path](#path)
17
+ - [String](#string)
18
+
19
+ ## Installation
20
+
21
+ Run the following command to add `utilful` to your project.
22
+
23
+ ```bash
24
+ # npm
25
+ npm install -D utilful
26
+
27
+ # pnpm
28
+ pnpm add -D utilful
29
+
30
+ # yarn
31
+ yarn add -D utilful
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### Array
37
+
38
+ #### `toArray`
39
+
40
+ Converts `MaybeArray<T>` to `Array<T>`.
41
+
42
+ ```ts
43
+ type MaybeArray<T> = T | T[]
44
+
45
+ declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[]
46
+ ```
47
+
48
+ ### CSV
49
+
50
+ #### `createCSV`
51
+
52
+ Converts an array of objects to a comma-separated values (CSV) string that contains only the `columns` specified.
53
+
54
+ ```ts
55
+ declare function createCSV<T extends Record<string, unknown>>(
56
+ data: T[],
57
+ columns: (keyof T)[],
58
+ options?: {
59
+ /** @default ',' */
60
+ delimiter?: string
61
+ /** @default true */
62
+ includeHeaders?: boolean
63
+ /** @default false */
64
+ quoteAll?: boolean
65
+ }
66
+ ): string
67
+ ```
68
+
69
+ **Example:**
70
+
71
+ ```ts
72
+ const data = [
73
+ { name: 'John', age: '30', city: 'New York' },
74
+ { name: 'Jane', age: '25', city: 'Boston' }
75
+ ]
76
+
77
+ const csv = createCSV(data, ['name', 'age'])
78
+ // name,age
79
+ // John,30
80
+ // Jane,25
81
+ ```
82
+
83
+ #### `parseCSV`
84
+
85
+ Parses a comma-separated values (CSV) string into an array of objects.
86
+
87
+ > [!NOTE]
88
+ > The first row of the CSV string is used as the header row.
89
+
90
+ ```ts
91
+ type CSVRow<T extends string = string> = Record<T, string>
92
+
93
+ declare function parseCSV<Header extends string>(
94
+ csv?: string | null | undefined,
95
+ options?: {
96
+ /** @default ',' */
97
+ delimiter?: string
98
+ /** @default true */
99
+ trimValues?: boolean
100
+ }
101
+ ): CSVRow<Header>[]
102
+ ```
103
+
104
+ **Example:**
105
+
106
+ ```ts
107
+ const csv = `name,age
108
+ John,30
109
+ Jane,25`
110
+
111
+ const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]
112
+ ```
113
+
114
+ ### Emitter
115
+
116
+ Simple and tiny event emitter library for JavaScript.
117
+
118
+ `createEmitter` accepts interface with event name to listener argument types mapping:
119
+
120
+ ```ts
121
+ import { createEmitter } from 'utilful'
122
+
123
+ interface Events {
124
+ set: (name: string, count: number) => void
125
+ tick: () => void
126
+ }
127
+
128
+ const emitter = createEmitter<Events>()
129
+
130
+ // Correct calls:
131
+ emitter.emit('set', 'prop', 1)
132
+ emitter.emit('tick')
133
+
134
+ // Compilation errors:
135
+ emitter.emit('set', 'prop', '1')
136
+ emitter.emit('tick', 2)
137
+ ```
138
+
139
+ The `on` method returns an `unbind` function. Call it and this listener will be removed from event:
140
+
141
+ ```ts
142
+ const unbind = emitter.on('tick', (number) => {
143
+ console.log(`on ${number}`)
144
+ })
145
+
146
+ emitter.emit('tick', 1)
147
+ // Prints "on 1"
148
+
149
+ unbind()
150
+ emitter.emit('tick', 2)
151
+ // Prints nothing
152
+ ```
153
+
154
+ You can get the used events list by accessing the `events` property:
155
+
156
+ ```ts
157
+ const unbind = emitter.on('tick', () => { })
158
+ emitter.events // => { tick: [ [Function] ] }
159
+ ```
160
+
161
+ ### JSON
162
+
163
+ #### `tryParseJSON`
164
+
165
+ Type-safe wrapper around `JSON.stringify`.
166
+
167
+ Falls back to the original value if the JSON serialization fails or the value is not a string.
168
+
169
+ ```ts
170
+ declare function tryParseJSON<T = unknown>(value: unknown): T
171
+ ```
172
+
173
+ #### `cloneJSON`
174
+
175
+ Clones the given JSON value.
176
+
177
+ > [!NOTE]
178
+ > The value must not contain circular references as JSON does not support them. It also must contain JSON serializable values.
179
+
180
+ ```ts
181
+ declare function cloneJSON<T>(value: T): T
182
+ ```
183
+
184
+ ### Lazy
185
+
186
+ A simple general purpose memoizer utility.
187
+
188
+ - Lazily computes a value when accessed
189
+ - Auto-caches the result by overwriting the getter
190
+ - Typesafe
191
+
192
+ Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invokation, since the getter itself is overwritten with the memoized value.
193
+
194
+ ```ts
195
+ declare function lazy<T>(getter: () => T): { value: T }
196
+ ```
197
+
198
+ **Example:**
199
+
200
+ ```ts
201
+ const myValue = lazy(() => 'Hello, World!')
202
+ console.log(myValue.value) // Computes value, overwrites getter
203
+ console.log(myValue.value) // Returns cached value
204
+ console.log(myValue.value) // Returns cached value
205
+ ```
206
+
207
+ ### Module
208
+
209
+ #### `interopDefault`
210
+
211
+ Interop helper for default exports.
212
+
213
+ ```ts
214
+ declare function interopDefault<T>(m: T | Promise<T>): Promise<T extends {
215
+ default: infer U
216
+ } ? U : T>
217
+ ```
218
+
219
+ **Example:**
220
+
221
+ ```ts
222
+ import { interopDefault } from 'utilful'
223
+
224
+ async function loadModule() {
225
+ const mod = await interopDefault(import('./module.js'))
226
+ }
227
+ ```
228
+
229
+ ### Object
230
+
231
+ #### `objectKeys`
232
+
233
+ Strictly typed `Object.keys`.
234
+
235
+ ```ts
236
+ declare function objectKeys<T extends Record<any, any>>(obj: T): Array<`${keyof T & (string | number | boolean | null | undefined)}`>
237
+ ```
238
+
239
+ #### `objectEntries`
240
+
241
+ Strictly typed `Object.entries`.
242
+
243
+ ```ts
244
+ declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof T, T[keyof T]]>
245
+ ```
246
+
247
+ #### `deepApply`
248
+
249
+ Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays.
250
+
251
+ ```ts
252
+ declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: T, key: keyof T, value: T[keyof T]) => void): void
253
+ ```
254
+
255
+ ### Path
256
+
257
+ #### `withoutLeadingSlash`
258
+
259
+ Removes the leading slash from the given path if it has one.
260
+
261
+ ```ts
262
+ declare function withoutLeadingSlash(path?: string): string
263
+ ```
264
+
265
+ #### `withLeadingSlash`
266
+
267
+ Adds a leading slash to the given path if it does not already have one.
268
+
269
+ ```ts
270
+ declare function withLeadingSlash(path?: string): string
271
+ ```
272
+
273
+ #### `withoutTrailingSlash`
274
+
275
+ Removes the trailing slash from the given path if it has one.
276
+
277
+ ```ts
278
+ declare function withoutTrailingSlash(path?: string): string
279
+ ```
280
+
281
+ #### `withTrailingSlash`
282
+
283
+ Adds a trailing slash to the given path if it does not already have one.
284
+
285
+ ```ts
286
+ declare function withTrailingSlash(path?: string): string
287
+ ```
288
+
289
+ #### `joinURL`
290
+
291
+ Joins the given URL path segments, ensuring that there is only one slash between them.
292
+
293
+ ```ts
294
+ declare function joinURL(...paths: (string | undefined)[]): string
295
+ ```
296
+
297
+ #### `withBase`
298
+
299
+ Adds the base path to the input path, if it is not already present.
300
+
301
+ ```ts
302
+ declare function withBase(input?: string, base?: string): string
303
+ ```
304
+
305
+ #### `withoutBase`
306
+
307
+ Removes the base path from the input path, if it is present.
308
+
309
+ ```ts
310
+ declare function withoutBase(input?: string, base?: string): string
311
+ ```
312
+
313
+ #### `getPathname`
314
+
315
+ Returns the pathname of the given path, which is the path without the query string.
316
+
317
+ ```ts
318
+ declare function getPathname(path?: string): string
319
+ ```
320
+
321
+ #### `withQuery`
322
+
323
+ Returns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
324
+
325
+ ```ts
326
+ declare function withQuery(input: string, query?: QueryObject): string
327
+ ```
328
+
329
+ **Example:**
330
+
331
+ ```ts
332
+ import { withQuery } from 'utilful'
333
+
334
+ const url = withQuery('https://example.com', {
335
+ foo: 'bar',
336
+ // This key is omitted
337
+ baz: undefined,
338
+ // Object values are stringified
339
+ baz: { qux: 'quux' }
340
+ })
341
+ ```
342
+
343
+ ### String
344
+
345
+ #### `template`
346
+
347
+ Simple template engine to replace variables in a string.
348
+
349
+ ```ts
350
+ declare function template(
351
+ str: string,
352
+ variables: Record<string | number, any>,
353
+ fallback?: string | ((key: string) => string)
354
+ ): string
355
+ ```
356
+
357
+ **Example:**
358
+
359
+ ```ts
360
+ import { template } from 'utilful'
361
+
362
+ const str = 'Hello, {name}!'
363
+ const variables = { name: 'world' }
364
+
365
+ console.log(template(str, variables)) // Hello, world!
366
+ ```
367
+
368
+ #### `generateRandomId`
369
+
370
+ Generates a random string. The function is ported from [`nanoid`](https://github.com/ai/nanoid). You can specify the size of the string and the dictionary of characters to use.
371
+
372
+ ```ts
373
+ declare function generateRandomId(size?: number, dict?: string): string
374
+ ```
375
+
376
+ ## License
377
+
378
+ [MIT](./LICENSE) License © 2024-PRESENT [Johann Schopplich](https://github.com/johannschopplich)
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Represents a value that can be either a single value or an array of values.
3
+ */
4
+ type MaybeArray<T> = T | T[];
5
+ /**
6
+ * Converts `MaybeArray<T>` to `Array<T>`.
7
+ */
8
+ declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[];
9
+
10
+ export { type MaybeArray, toArray };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Represents a value that can be either a single value or an array of values.
3
+ */
4
+ type MaybeArray<T> = T | T[];
5
+ /**
6
+ * Converts `MaybeArray<T>` to `Array<T>`.
7
+ */
8
+ declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[];
9
+
10
+ export { type MaybeArray, toArray };
package/dist/array.mjs ADDED
@@ -0,0 +1,6 @@
1
+ function toArray(array) {
2
+ array ??= [];
3
+ return Array.isArray(array) ? array : [array];
4
+ }
5
+
6
+ export { toArray };
package/dist/csv.d.mts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Represents a row in a CSV file with column names of type T.
3
+ */
4
+ type CSVRow<T extends string = string> = Record<T, string>;
5
+ /**
6
+ * Converts an array of objects to a comma-separated values (CSV) string
7
+ * that contains only the `columns` specified.
8
+ *
9
+ * @example
10
+ * const data = [
11
+ * { name: 'John', age: '30', city: 'New York' },
12
+ * { name: 'Jane', age: '25', city: 'Boston' }
13
+ * ]
14
+ *
15
+ * const csv = createCSV(data, ['name', 'age'])
16
+ * // name,age
17
+ * // John,30
18
+ * // Jane,25
19
+ */
20
+ declare function createCSV<T extends Record<string, unknown>>(data: T[], columns: (keyof T)[], options?: {
21
+ /** @default ',' */
22
+ delimiter?: string;
23
+ /** @default true */
24
+ addHeader?: boolean;
25
+ /** @default false */
26
+ quoteAll?: boolean;
27
+ }): string;
28
+ /**
29
+ * Escapes a value for a CSV string.
30
+ *
31
+ * @remarks
32
+ * Returns an empty string if the value is `null` or `undefined`.
33
+ * Values containing delimiters, quotes, or line breaks are quoted.
34
+ * Within quoted values, double quotes are escaped by doubling them.
35
+ *
36
+ * @example
37
+ * escapeCSVValue('hello, world'); // "hello, world"
38
+ * escapeCSVValue('contains "quotes"'); // "contains ""quotes"""
39
+ */
40
+ declare function escapeCSVValue(value: unknown, options?: {
41
+ /** @default ',' */
42
+ delimiter?: string;
43
+ /** @default false */
44
+ quoteAll?: boolean;
45
+ }): string;
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
+ declare function parseCSV<Header extends string>(csv?: string | null | undefined, options?: {
61
+ /** @default ',' */
62
+ delimiter?: string;
63
+ /** @default true */
64
+ trimValues?: boolean;
65
+ }): CSVRow<Header>[];
66
+
67
+ export { type CSVRow, createCSV, escapeCSVValue, parseCSV };
package/dist/csv.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Represents a row in a CSV file with column names of type T.
3
+ */
4
+ type CSVRow<T extends string = string> = Record<T, string>;
5
+ /**
6
+ * Converts an array of objects to a comma-separated values (CSV) string
7
+ * that contains only the `columns` specified.
8
+ *
9
+ * @example
10
+ * const data = [
11
+ * { name: 'John', age: '30', city: 'New York' },
12
+ * { name: 'Jane', age: '25', city: 'Boston' }
13
+ * ]
14
+ *
15
+ * const csv = createCSV(data, ['name', 'age'])
16
+ * // name,age
17
+ * // John,30
18
+ * // Jane,25
19
+ */
20
+ declare function createCSV<T extends Record<string, unknown>>(data: T[], columns: (keyof T)[], options?: {
21
+ /** @default ',' */
22
+ delimiter?: string;
23
+ /** @default true */
24
+ addHeader?: boolean;
25
+ /** @default false */
26
+ quoteAll?: boolean;
27
+ }): string;
28
+ /**
29
+ * Escapes a value for a CSV string.
30
+ *
31
+ * @remarks
32
+ * Returns an empty string if the value is `null` or `undefined`.
33
+ * Values containing delimiters, quotes, or line breaks are quoted.
34
+ * Within quoted values, double quotes are escaped by doubling them.
35
+ *
36
+ * @example
37
+ * escapeCSVValue('hello, world'); // "hello, world"
38
+ * escapeCSVValue('contains "quotes"'); // "contains ""quotes"""
39
+ */
40
+ declare function escapeCSVValue(value: unknown, options?: {
41
+ /** @default ',' */
42
+ delimiter?: string;
43
+ /** @default false */
44
+ quoteAll?: boolean;
45
+ }): string;
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
+ declare function parseCSV<Header extends string>(csv?: string | null | undefined, options?: {
61
+ /** @default ',' */
62
+ delimiter?: string;
63
+ /** @default true */
64
+ trimValues?: boolean;
65
+ }): CSVRow<Header>[];
66
+
67
+ export { type CSVRow, createCSV, escapeCSVValue, parseCSV };
package/dist/csv.mjs ADDED
@@ -0,0 +1,77 @@
1
+ function createCSV(data, columns, options = {}) {
2
+ const {
3
+ delimiter = ",",
4
+ addHeader = true,
5
+ quoteAll = false
6
+ } = options;
7
+ const escapeAndQuote = (value) => escapeCSVValue(value, { delimiter, quoteAll });
8
+ const rows = data.map(
9
+ (obj) => columns.map((key) => escapeAndQuote(obj[key])).join(delimiter)
10
+ );
11
+ if (addHeader) {
12
+ rows.unshift(columns.map(escapeAndQuote).join(delimiter));
13
+ }
14
+ return rows.join("\n");
15
+ }
16
+ function escapeCSVValue(value, options = {}) {
17
+ const {
18
+ delimiter = ",",
19
+ quoteAll = false
20
+ } = options;
21
+ if (value == null) {
22
+ return "";
23
+ }
24
+ const stringValue = String(value);
25
+ const needsQuoting = quoteAll || stringValue.includes(delimiter) || stringValue.includes('"') || stringValue.includes("\n") || stringValue.includes("\r");
26
+ if (needsQuoting) {
27
+ return `"${stringValue.replaceAll('"', '""')}"`;
28
+ }
29
+ return stringValue;
30
+ }
31
+ function parseCSV(csv, options = {}) {
32
+ if (!csv?.trim())
33
+ return [];
34
+ const rows = [];
35
+ let currentRow = [];
36
+ let currentField = "";
37
+ let inQuotes = false;
38
+ const { delimiter = ",", trimValues = true } = options;
39
+ for (let i = 0; i < csv.length; i++) {
40
+ const char = csv[i];
41
+ const nextChar = i + 1 < csv.length ? csv[i + 1] : "";
42
+ if (char === '"') {
43
+ if (inQuotes && nextChar === '"') {
44
+ currentField += '"';
45
+ i++;
46
+ } else {
47
+ inQuotes = !inQuotes;
48
+ }
49
+ } else if (char === delimiter && !inQuotes) {
50
+ currentRow.push(trimValues ? currentField.trim() : currentField);
51
+ currentField = "";
52
+ } else if ((char === "\n" || char === "\r" && nextChar === "\n") && !inQuotes) {
53
+ if (char === "\r")
54
+ i++;
55
+ currentRow.push(trimValues ? currentField.trim() : currentField);
56
+ rows.push(currentRow);
57
+ currentRow = [];
58
+ currentField = "";
59
+ } else {
60
+ currentField += char;
61
+ }
62
+ }
63
+ if (currentField || currentRow.length > 0) {
64
+ currentRow.push(trimValues ? currentField.trim() : currentField);
65
+ rows.push(currentRow);
66
+ }
67
+ if (rows.length <= 1)
68
+ return [];
69
+ const headers = rows[0];
70
+ return rows.slice(1).filter((row) => row.some((field) => field.trim().length > 0)).map((values) => {
71
+ return Object.fromEntries(
72
+ headers.map((header, index) => [header, index < values.length ? values[index] : ""])
73
+ );
74
+ });
75
+ }
76
+
77
+ export { createCSV, escapeCSVValue, parseCSV };
@@ -0,0 +1,72 @@
1
+ interface EventsMap {
2
+ [event: string]: any;
3
+ }
4
+ interface DefaultEvents extends EventsMap {
5
+ [event: string]: (...args: any) => void;
6
+ }
7
+ interface Unsubscribe {
8
+ (): void;
9
+ }
10
+ interface Emitter<Events extends EventsMap = DefaultEvents> {
11
+ /**
12
+ * Calls each of the listeners registered for a given event.
13
+ *
14
+ * @example
15
+ * emitter.emit('tick', tickType, tickDuration)
16
+ *
17
+ * @param event The event name.
18
+ * @param args The arguments for listeners.
19
+ */
20
+ emit: <K extends keyof Events>(this: this, event: K, ...args: Parameters<Events[K]>) => void;
21
+ /**
22
+ * Event names in keys and arrays with listeners in values.
23
+ *
24
+ * @example
25
+ * emitter1.events = emitter2.events
26
+ * emitter2.events = { }
27
+ */
28
+ events: Partial<{
29
+ [E in keyof Events]: Events[E][];
30
+ }>;
31
+ /**
32
+ * Add a listener for a given event.
33
+ *
34
+ * @example
35
+ * const unbind = emitter.on('tick', (tickType, tickDuration) => {
36
+ * count += 1
37
+ * })
38
+ *
39
+ * disable () {
40
+ * unbind()
41
+ * }
42
+ *
43
+ * @param event The event name.
44
+ * @param cb The listener function.
45
+ * @returns Unbind listener from event.
46
+ */
47
+ on: <K extends keyof Events>(this: this, event: K, cb: Events[K]) => Unsubscribe;
48
+ }
49
+ /**
50
+ * Create event emitter.
51
+ *
52
+ * @example
53
+ * import { createEmitter } from 'nanoevents'
54
+ *
55
+ * class Ticker {
56
+ * constructor() {
57
+ * this.emitter = createEmitter()
58
+ * }
59
+ * on(...args) {
60
+ * return this.emitter.on(...args)
61
+ * }
62
+ * tick() {
63
+ * this.emitter.emit('tick')
64
+ * }
65
+ * }
66
+ *
67
+ * @remarks Ported from `nanoevents`.
68
+ * @see https://github.com/ai/nanoevents
69
+ */
70
+ declare function createEmitter<Events extends EventsMap = DefaultEvents>(): Emitter<Events>;
71
+
72
+ export { type Emitter, createEmitter };