@goodbyenjn/utils 1.3.1 → 26.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +625 -87
- package/dist/chunks/chunk-267b337b.js +2782 -0
- package/dist/{libs/result-c7c586dd.d.ts → chunks/chunk-4b76d1c4.d.ts} +69 -84
- package/dist/chunks/chunk-72aa7743.js +1082 -0
- package/dist/{libs/types-92e74e19.d.ts → chunks/chunk-a07ed28f.d.ts} +4113 -2427
- package/dist/{index.d.ts → common.d.ts} +50 -20
- package/dist/common.js +3 -0
- package/dist/fs.d.ts +187 -32
- package/dist/fs.js +2640 -174
- package/dist/global-types.d.ts +59 -84
- package/dist/remeda.d.ts +6305 -1228
- package/dist/remeda.js +2 -2
- package/dist/result.d.ts +2 -3
- package/dist/result.js +2 -3
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -0
- package/package.json +24 -16
- package/dist/index.js +0 -5
- package/dist/libs/common-7c67f2df.js +0 -334
- package/dist/libs/remeda-16106be4.js +0 -2726
- package/dist/libs/result-a10bbd74.js +0 -243
package/README.md
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/@goodbyenjn%2Futils)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
A modern TypeScript/JavaScript utility library providing a comprehensive collection of type-safe utility functions, functional error handling with the Result pattern, and filesystem operations.
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
- 🚀 **Modern**: Built with TypeScript, targeting ES modules
|
|
11
|
-
- 🔒 **Type-safe**: Full TypeScript support with comprehensive type definitions
|
|
12
|
-
- 📦 **Modular**: Import only what you need with tree-shakable exports
|
|
13
|
-
- 🛡️ **Result
|
|
14
|
-
- 📁 **File System**:
|
|
15
|
-
- 🧰 **Common
|
|
16
|
-
- 📊 **Remeda Extensions**: Extended utilities built on top of Remeda
|
|
10
|
+
- 🚀 **Modern**: Built with TypeScript, targeting ES modules and modern JavaScript
|
|
11
|
+
- 🔒 **Type-safe**: Full TypeScript support with comprehensive type definitions and type inference
|
|
12
|
+
- 📦 **Modular**: Import only what you need with tree-shakable exports and multiple entry points
|
|
13
|
+
- 🛡️ **Result Pattern**: Functional error handling without exceptions, based on Rust-style Result types
|
|
14
|
+
- 📁 **Safe File System**: Type-safe file system operations with Result-based error handling
|
|
15
|
+
- 🧰 **Common Utilities**: String manipulation, math operations, promise utilities, shell commands, and error handling
|
|
16
|
+
- 📊 **Remeda Extensions**: Extended utilities built on top of [Remeda](https://remedajs.com/)
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -27,100 +27,283 @@ yarn add @goodbyenjn/utils
|
|
|
27
27
|
|
|
28
28
|
## Usage
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### Quick Start
|
|
31
31
|
|
|
32
32
|
```typescript
|
|
33
|
-
|
|
33
|
+
// Import what you need from the main module
|
|
34
|
+
import { sleep, template, $ } from "@goodbyenjn/utils";
|
|
35
|
+
import { safeReadFile } from "@goodbyenjn/utils/fs";
|
|
36
|
+
import { ok, Result } from "@goodbyenjn/utils/result";
|
|
37
|
+
```
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
await sleep(1000); // Sleep for 1 second
|
|
39
|
+
### Common Utilities
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
const text = template("Hello, {name}!", { name: "World" });
|
|
40
|
-
console.log(text); // "Hello, World!"
|
|
41
|
+
#### String Operations
|
|
41
42
|
|
|
43
|
+
```typescript
|
|
44
|
+
import { template, unindent, addPrefix, removeSuffix, join, split } from "@goodbyenjn/utils";
|
|
45
|
+
|
|
46
|
+
// String templating
|
|
47
|
+
const greeting = template("Hello, {name}! You are {age} years old.", {
|
|
48
|
+
name: "Alice",
|
|
49
|
+
age: 30,
|
|
50
|
+
});
|
|
51
|
+
console.log(greeting); // "Hello, Alice! You are 30 years old."
|
|
52
|
+
|
|
53
|
+
// Remove common indentation from template strings
|
|
42
54
|
const code = unindent`
|
|
43
55
|
function example() {
|
|
44
56
|
return 'formatted';
|
|
45
57
|
}
|
|
46
58
|
`;
|
|
59
|
+
console.log(code); // properly dedented code
|
|
60
|
+
|
|
61
|
+
// Prefix and suffix operations
|
|
62
|
+
const withPrefix = addPrefix("@", "myfile"); // "@myfile"
|
|
63
|
+
const cleaned = removeSuffix(".js", "script.js"); // "script"
|
|
64
|
+
|
|
65
|
+
// String joining and splitting
|
|
66
|
+
const path = join("/", "home", "user", "docs"); // "/home/user/docs"
|
|
67
|
+
const parts = split("-", "hello-world-js"); // ["hello", "world", "js"]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Promise Utilities
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { sleep, createLock, createSingleton, createPromiseWithResolvers } from "@goodbyenjn/utils";
|
|
74
|
+
|
|
75
|
+
// Sleep/delay execution
|
|
76
|
+
await sleep(1000); // Wait 1 second
|
|
77
|
+
|
|
78
|
+
// Create a reusable mutex lock
|
|
79
|
+
const lock = createLock();
|
|
80
|
+
await lock.acquire();
|
|
81
|
+
try {
|
|
82
|
+
// Critical section
|
|
83
|
+
console.log("Executing exclusively");
|
|
84
|
+
} finally {
|
|
85
|
+
lock.release();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Singleton pattern factory
|
|
89
|
+
const getDatabase = createSingleton(() => {
|
|
90
|
+
console.log("Initializing database...");
|
|
91
|
+
return new Database();
|
|
92
|
+
});
|
|
93
|
+
const db1 = await getDatabase(); // Initializes once
|
|
94
|
+
const db2 = await getDatabase(); // Returns same instance
|
|
95
|
+
|
|
96
|
+
// Promise with external resolvers
|
|
97
|
+
const { promise, resolve, reject } = createPromiseWithResolvers<string>();
|
|
98
|
+
setTimeout(() => resolve("done!"), 1000);
|
|
99
|
+
const result = await promise;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Shell Command Execution
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { $ } from "@goodbyenjn/utils";
|
|
47
106
|
|
|
48
|
-
//
|
|
107
|
+
// Execute shell commands safely using template literals
|
|
49
108
|
const result = await $`ls -la`;
|
|
50
109
|
if (result.isOk()) {
|
|
51
|
-
console.log("
|
|
52
|
-
console.log("
|
|
53
|
-
|
|
54
|
-
|
|
110
|
+
console.log("Command succeeded:");
|
|
111
|
+
console.log("stdout:", result.unwrap().stdout);
|
|
112
|
+
console.log("stderr:", result.unwrap().stderr);
|
|
113
|
+
} else if (result.isErr()) {
|
|
114
|
+
console.error("Command failed:", result.unwrapErr().message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Complex command with options
|
|
118
|
+
const { $ } = await import("@goodbyenjn/utils");
|
|
119
|
+
const deployResult = await $`docker build -t myapp .`;
|
|
120
|
+
if (deployResult.isOk()) {
|
|
121
|
+
const { stdout, stderr } = deployResult.unwrap();
|
|
122
|
+
console.log("Build output:", stdout);
|
|
55
123
|
}
|
|
56
124
|
|
|
57
|
-
//
|
|
58
|
-
|
|
125
|
+
// Using quoteShellArg for safe argument escaping
|
|
126
|
+
import { quoteShellArg } from "@goodbyenjn/utils";
|
|
127
|
+
const userInput = "'; rm -rf /;";
|
|
128
|
+
const safeArg = quoteShellArg(userInput); // Properly escaped for shell
|
|
59
129
|
```
|
|
60
130
|
|
|
61
|
-
|
|
131
|
+
#### Math Utilities
|
|
62
132
|
|
|
63
133
|
```typescript
|
|
64
|
-
import {
|
|
134
|
+
import { linear, scale } from "@goodbyenjn/utils";
|
|
135
|
+
|
|
136
|
+
// Linear interpolation between values
|
|
137
|
+
const mid = linear(0.5, [0, 100]); // 50
|
|
138
|
+
|
|
139
|
+
// Scale a value from one range to another
|
|
140
|
+
const scaledValue = scale(75, [0, 100], [0, 1]); // 0.75
|
|
141
|
+
const percentage = scale(200, [0, 255], [0, 100]); // 78.43...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### Error Handling
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { normalizeError, getErrorMessage } from "@goodbyenjn/utils";
|
|
148
|
+
|
|
149
|
+
// Normalize any value to an Error object
|
|
150
|
+
const error = normalizeError("Something went wrong");
|
|
151
|
+
const typeError = normalizeError({ code: 500 });
|
|
152
|
+
|
|
153
|
+
// Safely extract error message
|
|
154
|
+
const message1 = getErrorMessage(new Error("Oops"));
|
|
155
|
+
const message2 = getErrorMessage("Plain string error");
|
|
156
|
+
const message3 = getErrorMessage(null); // "Unknown error"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Throttling and Debouncing
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { debounce, throttle } from "@goodbyenjn/utils";
|
|
163
|
+
|
|
164
|
+
// Debounce - wait for inactivity before executing
|
|
165
|
+
const debouncedSearch = debounce((query: string) => {
|
|
166
|
+
console.log("Searching for:", query);
|
|
167
|
+
}, 300);
|
|
168
|
+
|
|
169
|
+
// Call multiple times, executes only after 300ms of inactivity
|
|
170
|
+
input.addEventListener("input", e => {
|
|
171
|
+
debouncedSearch((e.target as HTMLInputElement).value);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Throttle - execute at most once per interval
|
|
175
|
+
const throttledScroll = throttle(() => {
|
|
176
|
+
console.log("Scroll position:", window.scrollY);
|
|
177
|
+
}, 100);
|
|
65
178
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
179
|
+
window.addEventListener("scroll", throttledScroll);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### File System Operations
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import {
|
|
186
|
+
safeReadFile,
|
|
187
|
+
safeWriteFile,
|
|
188
|
+
safeExists,
|
|
189
|
+
safeReadJson,
|
|
190
|
+
safeWriteJson,
|
|
191
|
+
safeMkdir,
|
|
192
|
+
safeRm,
|
|
193
|
+
safeReadFileByLine,
|
|
194
|
+
} from "@goodbyenjn/utils/fs";
|
|
195
|
+
|
|
196
|
+
// Read text file safely
|
|
197
|
+
const textResult = await safeReadFile("config.txt", "utf8");
|
|
198
|
+
if (textResult.isOk()) {
|
|
199
|
+
console.log("File content:", textResult.unwrap());
|
|
70
200
|
} else {
|
|
71
|
-
console.error("Failed to read file:",
|
|
201
|
+
console.error("Failed to read file:", textResult.unwrapErr().message);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Read and parse JSON safely
|
|
205
|
+
const jsonResult = await safeReadJson("package.json");
|
|
206
|
+
if (jsonResult.isOk()) {
|
|
207
|
+
const pkg = jsonResult.unwrap();
|
|
208
|
+
console.log("Package name:", pkg.name);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Write JSON file
|
|
212
|
+
const writeResult = await safeWriteJson("data.json", { users: [] }, { pretty: true });
|
|
213
|
+
if (writeResult.isErr()) {
|
|
214
|
+
console.error("Write failed:", writeResult.unwrapErr());
|
|
72
215
|
}
|
|
73
216
|
|
|
74
217
|
// Check if file exists
|
|
75
218
|
const existsResult = await safeExists("path/to/file.txt");
|
|
76
|
-
if (existsResult.isOk() && existsResult.
|
|
219
|
+
if (existsResult.isOk() && existsResult.unwrap()) {
|
|
77
220
|
console.log("File exists!");
|
|
78
221
|
}
|
|
222
|
+
|
|
223
|
+
// Create directories (recursive)
|
|
224
|
+
const mkResult = await safeMkdir("src/components/ui", { recursive: true });
|
|
225
|
+
|
|
226
|
+
// Delete files or directories
|
|
227
|
+
const rmResult = await safeRm("build", { recursive: true, force: true });
|
|
228
|
+
|
|
229
|
+
// Read file line by line
|
|
230
|
+
const lineResult = await safeReadFileByLine("large-file.log");
|
|
231
|
+
if (lineResult.isOk()) {
|
|
232
|
+
for await (const line of lineResult.unwrap()) {
|
|
233
|
+
console.log(line);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
79
236
|
```
|
|
80
237
|
|
|
81
|
-
###
|
|
238
|
+
### Glob Patterns
|
|
82
239
|
|
|
83
240
|
```typescript
|
|
84
|
-
import {
|
|
241
|
+
import { glob, globSync, convertPathToPattern } from "@goodbyenjn/utils/fs";
|
|
85
242
|
|
|
86
|
-
//
|
|
87
|
-
const
|
|
88
|
-
|
|
243
|
+
// Async glob pattern matching
|
|
244
|
+
const files = await glob("src/**/*.{ts,tsx}", { cwd: "." });
|
|
245
|
+
console.log("Found files:", files);
|
|
89
246
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
console.log("Value:", success.value); // 42
|
|
93
|
-
}
|
|
247
|
+
// Synchronous version
|
|
248
|
+
const syncFiles = globSync("**/*.test.ts", { cwd: "tests" });
|
|
94
249
|
|
|
95
|
-
// Convert
|
|
96
|
-
const
|
|
97
|
-
const result = safeParseInt("not-a-number");
|
|
98
|
-
if (result.isErr()) {
|
|
99
|
-
console.log("Parse failed:", result.error);
|
|
100
|
-
}
|
|
250
|
+
// Convert file path to glob pattern
|
|
251
|
+
const pattern = convertPathToPattern("/home/user/project");
|
|
101
252
|
```
|
|
102
253
|
|
|
103
|
-
###
|
|
254
|
+
### Result Pattern - Functional Error Handling
|
|
104
255
|
|
|
105
256
|
```typescript
|
|
106
|
-
import {
|
|
257
|
+
import { err, ok, Result, safeTry } from "@goodbyenjn/utils/result";
|
|
107
258
|
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
259
|
+
// Create results explicitly
|
|
260
|
+
const success = ok(42);
|
|
261
|
+
const failure = err("Something went wrong");
|
|
262
|
+
|
|
263
|
+
// Handle results with chainable methods
|
|
264
|
+
const doubled = success
|
|
265
|
+
.map(value => value * 2)
|
|
266
|
+
.mapErr(err => `Error: ${err}`)
|
|
267
|
+
.unwrapOr(0); // 84
|
|
268
|
+
|
|
269
|
+
// Transform error type
|
|
270
|
+
const result: Result<string, Error> = ok("value");
|
|
271
|
+
const transformed = result.mapErr(() => new Error("Custom error"));
|
|
272
|
+
|
|
273
|
+
// Convert throwing functions to Result
|
|
274
|
+
async function fetchUser(id: string) {
|
|
275
|
+
// If the function throws, it's caught and wrapped in Err
|
|
276
|
+
const user = await Result.fromCallable(() => JSON.parse(userJson));
|
|
277
|
+
|
|
278
|
+
return user.map(u => u.name).mapErr(err => new Error(`Failed to parse user: ${err.message}`));
|
|
112
279
|
}
|
|
113
280
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
281
|
+
// Combine multiple Results
|
|
282
|
+
const results = [ok(1), ok(2), err("oops"), ok(4)];
|
|
283
|
+
const combined = Result.all(...results); // Err("oops")
|
|
284
|
+
|
|
285
|
+
// Safe try-catch alternative
|
|
286
|
+
const safeTryExample = await safeTry(async () => {
|
|
287
|
+
return await fetch("/api/data").then(r => r.json());
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (safeTryExample.isOk()) {
|
|
291
|
+
console.log("Data:", safeTryExample.unwrap());
|
|
292
|
+
} else {
|
|
293
|
+
console.error("Failed:", safeTryExample.unwrapErr());
|
|
117
294
|
}
|
|
118
295
|
```
|
|
119
296
|
|
|
120
297
|
### Type Utilities
|
|
121
298
|
|
|
122
299
|
```typescript
|
|
123
|
-
import type {
|
|
300
|
+
import type {
|
|
301
|
+
Nullable,
|
|
302
|
+
Optional,
|
|
303
|
+
YieldType,
|
|
304
|
+
OmitByKey,
|
|
305
|
+
SetNullable,
|
|
306
|
+
} from "@goodbyenjn/utils/types";
|
|
124
307
|
|
|
125
308
|
// Nullable type for values that can be null or undefined
|
|
126
309
|
type User = {
|
|
@@ -129,6 +312,11 @@ type User = {
|
|
|
129
312
|
email: Nullable<string>; // string | null | undefined
|
|
130
313
|
};
|
|
131
314
|
|
|
315
|
+
// Optional type (undefined but not null)
|
|
316
|
+
type Profile = {
|
|
317
|
+
bio: Optional<string>; // string | undefined
|
|
318
|
+
};
|
|
319
|
+
|
|
132
320
|
// Extract yield type from generators
|
|
133
321
|
function* numberGenerator() {
|
|
134
322
|
yield 1;
|
|
@@ -137,58 +325,386 @@ function* numberGenerator() {
|
|
|
137
325
|
}
|
|
138
326
|
|
|
139
327
|
type NumberType = YieldType<typeof numberGenerator>; // number
|
|
328
|
+
|
|
329
|
+
// Omit properties by their value type
|
|
330
|
+
type Config = {
|
|
331
|
+
name: string;
|
|
332
|
+
debug: boolean;
|
|
333
|
+
verbose: boolean;
|
|
334
|
+
timeout: number;
|
|
335
|
+
};
|
|
336
|
+
type WithoutBooleans = OmitByKey<Config, boolean>; // { name: string; timeout: number }
|
|
337
|
+
|
|
338
|
+
// Set specific properties to nullable
|
|
339
|
+
type APIResponse = {
|
|
340
|
+
id: number;
|
|
341
|
+
name: string;
|
|
342
|
+
email: string;
|
|
343
|
+
};
|
|
344
|
+
type PartialResponse = SetNullable<APIResponse, "email" | "name">; // email and name become nullable
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Extended Remeda Utilities
|
|
348
|
+
|
|
349
|
+
The library includes 100+ utilities from [Remeda](https://remedajs.com/), a functional utility library optimized for TypeScript:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import {
|
|
353
|
+
// Property checking
|
|
354
|
+
hasOwnProperty,
|
|
355
|
+
isFunction,
|
|
356
|
+
isPromiseLike,
|
|
357
|
+
|
|
358
|
+
// Array operations
|
|
359
|
+
chunk,
|
|
360
|
+
compact,
|
|
361
|
+
drop,
|
|
362
|
+
dropLast,
|
|
363
|
+
filter,
|
|
364
|
+
find,
|
|
365
|
+
flatMap,
|
|
366
|
+
flatten,
|
|
367
|
+
map,
|
|
368
|
+
partition,
|
|
369
|
+
reverse,
|
|
370
|
+
slice,
|
|
371
|
+
take,
|
|
372
|
+
uniq,
|
|
373
|
+
|
|
374
|
+
// Object operations
|
|
375
|
+
pick,
|
|
376
|
+
omit,
|
|
377
|
+
merge,
|
|
378
|
+
keys,
|
|
379
|
+
values,
|
|
380
|
+
entries,
|
|
381
|
+
|
|
382
|
+
// Functional composition
|
|
383
|
+
pipe,
|
|
384
|
+
compose,
|
|
385
|
+
|
|
386
|
+
// Utility functions
|
|
387
|
+
clamp,
|
|
388
|
+
groupBy,
|
|
389
|
+
countBy,
|
|
390
|
+
sumBy,
|
|
391
|
+
minBy,
|
|
392
|
+
maxBy,
|
|
393
|
+
} from "@goodbyenjn/utils/remeda";
|
|
394
|
+
|
|
395
|
+
// Type-safe property checking
|
|
396
|
+
const obj = { name: "John", age: 30, active: true };
|
|
397
|
+
if (hasOwnProperty(obj, "name")) {
|
|
398
|
+
console.log(obj.name); // TypeScript type narrowing
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Function type checking
|
|
402
|
+
const maybeCallback: unknown = (x: number) => x * 2;
|
|
403
|
+
if (isFunction(maybeCallback)) {
|
|
404
|
+
maybeCallback(5);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Promise detection
|
|
408
|
+
async function handleValue(value: any) {
|
|
409
|
+
if (isPromiseLike(value)) {
|
|
410
|
+
const result = await value;
|
|
411
|
+
console.log("Async result:", result);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Functional data transformations
|
|
416
|
+
const users = [
|
|
417
|
+
{ id: 1, name: "Alice", role: "admin", active: true },
|
|
418
|
+
{ id: 2, name: "Bob", role: "user", active: false },
|
|
419
|
+
{ id: 3, name: "Charlie", role: "user", active: true },
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
// Chain operations with pipe
|
|
423
|
+
const adminNames = pipe(
|
|
424
|
+
users,
|
|
425
|
+
filter(u => u.role === "admin"),
|
|
426
|
+
map(u => u.name),
|
|
427
|
+
); // ["Alice"]
|
|
428
|
+
|
|
429
|
+
// Group users by role
|
|
430
|
+
const byRole = groupBy(users, u => u.role);
|
|
431
|
+
// { admin: [...], user: [...] }
|
|
432
|
+
|
|
433
|
+
// Sum ages of active users
|
|
434
|
+
const totalAge = sumBy(
|
|
435
|
+
filter(users, u => u.active),
|
|
436
|
+
u => u.age ?? 0,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// Chunk array into groups
|
|
440
|
+
const chunked = chunk(users, 2);
|
|
441
|
+
// [[user1, user2], [user3]]
|
|
140
442
|
```
|
|
141
443
|
|
|
142
444
|
## API Reference
|
|
143
445
|
|
|
144
|
-
###
|
|
446
|
+
### Module Exports
|
|
145
447
|
|
|
146
|
-
|
|
147
|
-
- **File System (`@goodbyenjn/utils/fs`)** - Safe file system operations
|
|
148
|
-
- **Result (`@goodbyenjn/utils/result`)** - Functional error handling
|
|
149
|
-
- **Remeda (`@goodbyenjn/utils/remeda`)** - Extended Remeda utilities
|
|
150
|
-
- **Types (`@goodbyenjn/utils/types`)** - Utility types for TypeScript
|
|
448
|
+
#### Main Module (`@goodbyenjn/utils`)
|
|
151
449
|
|
|
152
|
-
|
|
450
|
+
Common utilities for everyday programming tasks:
|
|
153
451
|
|
|
154
|
-
|
|
452
|
+
```typescript
|
|
453
|
+
// String utilities
|
|
454
|
+
export {
|
|
455
|
+
template,
|
|
456
|
+
unindent,
|
|
457
|
+
addPrefix,
|
|
458
|
+
addSuffix,
|
|
459
|
+
removePrefix,
|
|
460
|
+
removeSuffix,
|
|
461
|
+
split,
|
|
462
|
+
join,
|
|
463
|
+
splitWithSlash,
|
|
464
|
+
joinWithSlash,
|
|
465
|
+
toForwardSlash,
|
|
466
|
+
concatTemplateStrings,
|
|
467
|
+
};
|
|
155
468
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- `addPrefix/removePrefix` - Prefix manipulation
|
|
159
|
-
- `addSuffix/removeSuffix` - Suffix manipulation
|
|
160
|
-
- `toForwardSlash` - Convert backslashes to forward slashes
|
|
469
|
+
// Promise utilities
|
|
470
|
+
export { sleep, createLock, createSingleton, createPromiseWithResolvers };
|
|
161
471
|
|
|
162
|
-
|
|
472
|
+
// Shell command execution
|
|
473
|
+
export { $, quoteShellArg };
|
|
163
474
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
- `createSingleton(factory)` - Create singleton factory
|
|
167
|
-
- `PromiseWithResolvers` - Promise with external resolvers
|
|
475
|
+
// Math utilities
|
|
476
|
+
export { linear, scale };
|
|
168
477
|
|
|
169
|
-
|
|
478
|
+
// Error handling
|
|
479
|
+
export { normalizeError, getErrorMessage };
|
|
170
480
|
|
|
171
|
-
|
|
481
|
+
// Throttling/Debouncing
|
|
482
|
+
export { debounce, throttle };
|
|
172
483
|
|
|
173
|
-
|
|
484
|
+
// JSON utilities
|
|
485
|
+
export { stringify, parse, safeParse };
|
|
174
486
|
|
|
175
|
-
|
|
176
|
-
|
|
487
|
+
// Parsing utilities
|
|
488
|
+
export { parseKeyValuePairs, parseValueToBoolean };
|
|
489
|
+
```
|
|
177
490
|
|
|
178
|
-
####
|
|
491
|
+
#### File System Module (`@goodbyenjn/utils/fs`)
|
|
492
|
+
|
|
493
|
+
Type-safe file system operations with Result pattern error handling:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// Safe operations (return Result types)
|
|
497
|
+
export {
|
|
498
|
+
safeReadFile,
|
|
499
|
+
safeReadFileSync,
|
|
500
|
+
safeReadJson,
|
|
501
|
+
safeReadJsonSync,
|
|
502
|
+
safeReadFileByLine,
|
|
503
|
+
safeWriteFile,
|
|
504
|
+
safeWriteFileSync,
|
|
505
|
+
safeWriteJson,
|
|
506
|
+
safeWriteJsonSync,
|
|
507
|
+
safeAppendFile,
|
|
508
|
+
safeAppendFileSync,
|
|
509
|
+
safeMkdir,
|
|
510
|
+
safeMkdirSync,
|
|
511
|
+
safeRm,
|
|
512
|
+
safeRmSync,
|
|
513
|
+
safeCp,
|
|
514
|
+
safeCpSync,
|
|
515
|
+
safeExists,
|
|
516
|
+
safeExistsSync,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// Glob operations
|
|
520
|
+
export { glob, globSync, convertPathToPattern, escapePath, isDynamicPattern };
|
|
521
|
+
|
|
522
|
+
// Unsafe operations (throw on error)
|
|
523
|
+
export {
|
|
524
|
+
readFile,
|
|
525
|
+
readFileSync,
|
|
526
|
+
readJson,
|
|
527
|
+
readJsonSync,
|
|
528
|
+
writeFile,
|
|
529
|
+
writeFileSync,
|
|
530
|
+
writeJson,
|
|
531
|
+
writeJsonSync,
|
|
532
|
+
mkdir,
|
|
533
|
+
mkdirSync,
|
|
534
|
+
// ... and more
|
|
535
|
+
};
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### Result Module (`@goodbyenjn/utils/result`)
|
|
539
|
+
|
|
540
|
+
Functional error handling without exceptions:
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
// Main types and constructors
|
|
544
|
+
export { Result, ok as Ok, err as Err };
|
|
545
|
+
|
|
546
|
+
// Helper function
|
|
547
|
+
export { safeTry };
|
|
548
|
+
|
|
549
|
+
// Error class
|
|
550
|
+
export { ResultError };
|
|
551
|
+
|
|
552
|
+
// Type utilities
|
|
553
|
+
export type { Ok, Err, InferOkType, InferErrType, ExtractOkTypes, ExtractErrTypes, ResultAll };
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Result Methods:**
|
|
557
|
+
|
|
558
|
+
- `isOk()` / `isErr()` - Type guard checks
|
|
559
|
+
- `map(fn)` - Transform the Ok value
|
|
560
|
+
- `mapErr(fn)` - Transform the Err value
|
|
561
|
+
- `flatMap(fn)` / `andThen(fn)` - Chain operations
|
|
562
|
+
- `unwrap()` - Get value or throw
|
|
563
|
+
- `unwrapOr(default)` - Get value with fallback
|
|
564
|
+
- `expect(msg)` - Unwrap with custom error message
|
|
565
|
+
- `inspect(fn)` - Execute side effect on Ok
|
|
566
|
+
- `inspectErr(fn)` - Execute side effect on Err
|
|
567
|
+
- `static fromCallable(fn, onThrow?)` - Convert throwing function
|
|
568
|
+
|
|
569
|
+
#### Remeda Module (`@goodbyenjn/utils/remeda`)
|
|
570
|
+
|
|
571
|
+
Extended functional utilities from Remeda:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// Custom implementations
|
|
575
|
+
export { hasOwnProperty, isFunction, isPromiseLike };
|
|
576
|
+
|
|
577
|
+
// Re-exported from Remeda (100+ functions)
|
|
578
|
+
export {
|
|
579
|
+
// Predicates
|
|
580
|
+
compact,
|
|
581
|
+
filter,
|
|
582
|
+
// Transformations
|
|
583
|
+
map,
|
|
584
|
+
flatMap,
|
|
585
|
+
flatten,
|
|
586
|
+
// Array operations
|
|
587
|
+
chunk,
|
|
588
|
+
slice,
|
|
589
|
+
take,
|
|
590
|
+
drop,
|
|
591
|
+
uniq,
|
|
592
|
+
reverse,
|
|
593
|
+
// Object operations
|
|
594
|
+
keys,
|
|
595
|
+
values,
|
|
596
|
+
entries,
|
|
597
|
+
pick,
|
|
598
|
+
omit,
|
|
599
|
+
merge,
|
|
600
|
+
// Aggregations
|
|
601
|
+
groupBy,
|
|
602
|
+
countBy,
|
|
603
|
+
sumBy,
|
|
604
|
+
maxBy,
|
|
605
|
+
minBy,
|
|
606
|
+
// Composition
|
|
607
|
+
pipe,
|
|
608
|
+
compose,
|
|
609
|
+
// And 50+ more...
|
|
610
|
+
};
|
|
611
|
+
```
|
|
179
612
|
|
|
180
|
-
|
|
181
|
-
- `getErrorMessage(error)` - Extract error message safely
|
|
613
|
+
#### Types Module (`@goodbyenjn/utils/types`)
|
|
182
614
|
|
|
183
|
-
|
|
615
|
+
Utility types for TypeScript:
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// Type utilities
|
|
619
|
+
export type {
|
|
620
|
+
Nullable, // T | null | undefined
|
|
621
|
+
Optional, // T | undefined
|
|
622
|
+
YieldType, // Extract yield type from generator
|
|
623
|
+
OmitByKey, // Omit properties by their value type
|
|
624
|
+
SetNullable, // Make specific properties nullable
|
|
625
|
+
Fn, // (args: any[]) => any
|
|
626
|
+
AsyncFn, // (...args: any[]) => Promise<any>
|
|
627
|
+
SyncFn, // (...args: any[]) => any
|
|
628
|
+
Promisable, // T | Promise<T>
|
|
629
|
+
NonEmptyTuple, // Tuple with at least one element
|
|
630
|
+
};
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Common Patterns
|
|
634
|
+
|
|
635
|
+
#### Error Handling with Result
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
// Instead of try-catch
|
|
639
|
+
async function loadConfig() {
|
|
640
|
+
const result = await safeReadJson("config.json");
|
|
641
|
+
|
|
642
|
+
// Pattern 1: Check and unwrap
|
|
643
|
+
if (result.isErr()) {
|
|
644
|
+
console.error("Failed to load config:", result.error);
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
return result.value;
|
|
648
|
+
|
|
649
|
+
// Pattern 2: Chain operations
|
|
650
|
+
// return result
|
|
651
|
+
// .map(cfg => validateConfig(cfg))
|
|
652
|
+
// .unwrapOr(defaultConfig);
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
#### File Operations
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
// Safe read with fallback
|
|
660
|
+
const result = await safeReadFile("path.txt", "utf8");
|
|
661
|
+
const content = result.unwrapOr("default content");
|
|
662
|
+
|
|
663
|
+
// Or handle error explicitly
|
|
664
|
+
const jsonResult = await safeReadJson("data.json");
|
|
665
|
+
if (jsonResult.isOk()) {
|
|
666
|
+
processData(jsonResult.value);
|
|
667
|
+
} else {
|
|
668
|
+
logger.error(jsonResult.error);
|
|
669
|
+
}
|
|
670
|
+
```
|
|
184
671
|
|
|
185
|
-
|
|
186
|
-
|
|
672
|
+
#### Functional Composition
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
import { pipe, filter, map } from "@goodbyenjn/utils/remeda";
|
|
676
|
+
|
|
677
|
+
const result = pipe(
|
|
678
|
+
data,
|
|
679
|
+
filter(x => x.active),
|
|
680
|
+
map(x => x.name),
|
|
681
|
+
);
|
|
682
|
+
```
|
|
187
683
|
|
|
188
684
|
## Requirements
|
|
189
685
|
|
|
190
|
-
- Node.js >= 18
|
|
191
|
-
- TypeScript >= 4.5 (for
|
|
686
|
+
- **Node.js**: >= 18.0.0
|
|
687
|
+
- **TypeScript**: >= 4.5 (for development/type checking)
|
|
688
|
+
|
|
689
|
+
Modern browsers are supported through ES module imports.
|
|
690
|
+
|
|
691
|
+
## Versioning
|
|
692
|
+
|
|
693
|
+
**Note:** This project does **not** follow Semantic Versioning (semver). Instead, it uses a calendar-based versioning scheme:
|
|
694
|
+
|
|
695
|
+
**Version Format:** `v<YY>.<M>.<PATCH>`
|
|
696
|
+
|
|
697
|
+
- `<YY>` - Release year (e.g., 26 for 2026)
|
|
698
|
+
- `<M>` - Release month (1-12)
|
|
699
|
+
- `<PATCH>` - Patch/revision number within the same month (starting from 0)
|
|
700
|
+
|
|
701
|
+
**Example versions:**
|
|
702
|
+
|
|
703
|
+
- `v2026.01.0` - First release in January 2026
|
|
704
|
+
- `v2026.01.1` - Second release in January 2026
|
|
705
|
+
- `v2026.02.0` - First release in February 2026
|
|
706
|
+
|
|
707
|
+
This scheme provides clarity on when features were released while allowing multiple updates within the same month.
|
|
192
708
|
|
|
193
709
|
## Development
|
|
194
710
|
|
|
@@ -196,13 +712,30 @@ type NumberType = YieldType<typeof numberGenerator>; // number
|
|
|
196
712
|
# Install dependencies
|
|
197
713
|
pnpm install
|
|
198
714
|
|
|
715
|
+
# Development mode with watch
|
|
716
|
+
pnpm run dev
|
|
717
|
+
|
|
199
718
|
# Build the library
|
|
200
719
|
pnpm run build
|
|
201
720
|
|
|
202
721
|
# Clean build artifacts
|
|
203
722
|
pnpm run clean
|
|
723
|
+
|
|
724
|
+
# Run tests (if configured)
|
|
725
|
+
pnpm run test
|
|
204
726
|
```
|
|
205
727
|
|
|
728
|
+
## Performance Considerations
|
|
729
|
+
|
|
730
|
+
- **Tree-shaking**: All modules are properly configured for tree-shaking. Import only what you need.
|
|
731
|
+
- **Result Pattern**: The Result type has minimal overhead compared to exceptions and enables better error handling.
|
|
732
|
+
- **Functional Composition**: Use Remeda utilities with pipe for efficient data transformations.
|
|
733
|
+
- **Shell Execution**: The `$` function safely escapes arguments and is suitable for production use.
|
|
734
|
+
|
|
735
|
+
## Contributing
|
|
736
|
+
|
|
737
|
+
Contributions are welcome! Please feel free to submit a Pull Request at [GitHub Repository](https://github.com/GoodbyeNJN/utils).
|
|
738
|
+
|
|
206
739
|
## License
|
|
207
740
|
|
|
208
741
|
MIT © [GoodbyeNJN](https://github.com/GoodbyeNJN)
|
|
@@ -211,4 +744,9 @@ MIT © [GoodbyeNJN](https://github.com/GoodbyeNJN)
|
|
|
211
744
|
|
|
212
745
|
- [GitHub Repository](https://github.com/GoodbyeNJN/utils)
|
|
213
746
|
- [npm Package](https://www.npmjs.com/package/@goodbyenjn/utils)
|
|
214
|
-
- [
|
|
747
|
+
- [Issue Tracker](https://github.com/GoodbyeNJN/utils/issues)
|
|
748
|
+
- [Remeda Documentation](https://remedajs.com/)
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
**Maintained with ❤️ by [GoodbyeNJN](https://github.com/GoodbyeNJN)**
|