@goodbyenjn/utils 26.3.0 → 26.4.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/README.md CHANGED
@@ -11,9 +11,9 @@ A modern TypeScript/JavaScript utility library providing a comprehensive collect
11
11
  - 🔒 **Type-safe**: Full TypeScript support with comprehensive type definitions and type inference
12
12
  - 📦 **Modular**: Import only what you need with tree-shakable exports and multiple entry points
13
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
- - 🐚 **Shell Execution**: Powerful and flexible shell command execution with piping support
16
- - 🧰 **Common Utilities**: String manipulation, math operations, promise utilities, and error handling
14
+ - 📁 **VFile & FS**: Type-safe file system operations and a powerful virtual file object
15
+ - 🐚 **Exec**: Powerful and flexible command execution with safe and unsafe variants
16
+ - 🧰 **Common Utilities**: String manipulation, math operations, promise utilities, and JSON handling
17
17
  - 📊 **Remeda Extensions**: Extended utilities built on top of [Remeda](https://remedajs.com/)
18
18
 
19
19
  ## Installation
@@ -33,9 +33,10 @@ yarn add @goodbyenjn/utils
33
33
  ```typescript
34
34
  // Import what you need from the main module
35
35
  import { sleep, template } from "@goodbyenjn/utils";
36
- import { $ } from "@goodbyenjn/utils/shell";
37
- import { safeReadFile } from "@goodbyenjn/utils/fs";
36
+ import { exec, safeExec } from "@goodbyenjn/utils/exec";
37
+ import { BaseVFile } from "@goodbyenjn/utils/fs";
38
38
  import { ok, Result } from "@goodbyenjn/utils/result";
39
+ import { parse, safeParse } from "@goodbyenjn/utils/json";
39
40
  ```
40
41
 
41
42
  ### Common Utilities
@@ -116,6 +117,10 @@ const parts = split("-", "hello-world-js"); // ["hello", "world", "js"]
116
117
  // Split string by line breaks (handles both \n and \r\n)
117
118
  const lines = splitByLineBreak("line1\nline2\r\nline3");
118
119
  console.log(lines); // ["line1", "line2", "line3"]
120
+
121
+ // Parse boolean value with custom default
122
+ const isEnabled = parseValueToBoolean("yes", false); // true
123
+ const debugMode = parseValueToBoolean("invalid", "auto"); // "auto"
119
124
  ```
120
125
 
121
126
  #### Promise Utilities
@@ -150,34 +155,39 @@ setTimeout(() => resolve("done!"), 1000);
150
155
  const result = await promise;
151
156
  ```
152
157
 
153
- #### Shell Command Execution
158
+ ### Command Execution
154
159
 
155
160
  ```typescript
156
- import { $ } from "@goodbyenjn/utils/shell";
161
+ import { exec, safeExec } from "@goodbyenjn/utils/exec";
157
162
 
158
- // Execute shell commands with template literals
159
- const result = await $`npm install`;
160
- console.log(result.stdout);
161
- console.log(result.stderr);
163
+ // 1. Unsafe Execution (throws on failure)
164
+ const output = await exec`npm install`;
165
+ console.log(output.stdout);
162
166
 
163
167
  // String command with args
164
- const output = await $("ls", ["-la"]);
165
- console.log(output.stdout);
168
+ const lsOutput = await exec("ls", ["-la"]);
169
+ console.log(lsOutput.stdout);
170
+
171
+ // 2. Safe Execution (returns Result)
172
+ const safeOutput = await safeExec`npm install`;
173
+ if (safeOutput.isOk()) {
174
+ console.log("Success:", safeOutput.unwrap().stdout);
175
+ } else {
176
+ // Result contains error information (e.g., NonZeroExitError)
177
+ console.error("Failed:", safeOutput.unwrapErr().message);
178
+ }
166
179
 
167
- // Pipe commands
168
- const piped = await $`echo "hello"`.pipe`cat`;
180
+ // 3. Pipe Output
181
+ const piped = await exec`echo "hello"`.pipe`cat`;
169
182
  console.log(piped.stdout);
170
183
 
171
- // Iterate output line by line
172
- for await (const line of $`cat large-file.txt`) {
184
+ // 4. Iterate Output
185
+ for await (const line of exec`cat large-file.txt`) {
173
186
  console.log(line);
174
187
  }
175
188
 
176
- // Using options
177
- const result2 = await $("npm", ["install"], { cwd: "/path/to/project" });
178
-
179
- // Factory function with options
180
- const withCwd = $({ cwd: "/path/to/project" });
189
+ // 5. Configuration Factory
190
+ const withCwd = exec({ cwd: "/path/to/project" });
181
191
  const result3 = await withCwd`npm install`;
182
192
  ```
183
193
 
@@ -232,6 +242,25 @@ const throttledScroll = throttle(() => {
232
242
  window.addEventListener("scroll", throttledScroll);
233
243
  ```
234
244
 
245
+ #### JSON Handling
246
+
247
+ ```typescript
248
+ import { parse, stringify, safeParse, safeStringify } from "@goodbyenjn/utils/json";
249
+
250
+ // Standard JSON parsing (returns value or nil)
251
+ const data = parse('{"a": 1}'); // { a: 1 }
252
+ const invalid = parse("bad"); // nil
253
+
254
+ // Safe JSON parsing (returns Result)
255
+ const result = safeParse('{"a": 1}');
256
+ if (result.isOk()) {
257
+ console.log(result.unwrap().a);
258
+ }
259
+
260
+ // Safe stringify
261
+ const json = safeStringify({ a: 1 }); // Result<string, Error>
262
+ ```
263
+
235
264
  ### File System Operations
236
265
 
237
266
  ```typescript
@@ -244,33 +273,50 @@ import {
244
273
  safeMkdir,
245
274
  safeRm,
246
275
  safeReadFileByLine,
276
+ BaseVFile,
247
277
  } from "@goodbyenjn/utils/fs";
248
278
 
249
- // Read text file safely
250
- const textResult = await safeReadFile("config.txt", "utf8");
279
+ // ... (safe operations)
280
+
281
+ // BaseVFile - Unified file handling
282
+ const vfile = new BaseVFile("example.json");
283
+
284
+ // Fluid path manipulation
285
+ vfile.filename("data").extname("ts");
286
+ console.log(vfile.basename()); // "data.ts"
287
+
288
+ // Cross-platform path handling
289
+ const relative = vfile.pathname.relative(); // "data.ts" (relative to cwd)
290
+ const absolute = vfile.pathname(); // "/full/path/to/data.ts"
291
+
292
+ // Built-in operations (available in extended VFile implementations)
293
+ // await vfile.read(); // Get content
294
+ // await vfile.write(); // Write content
295
+ ```
296
+
251
297
  if (textResult.isOk()) {
252
- console.log("File content:", textResult.unwrap());
298
+ console.log("File content:", textResult.unwrap());
253
299
  } else {
254
- console.error("Failed to read file:", textResult.unwrapErr().message);
300
+ console.error("Failed to read file:", textResult.unwrapErr().message);
255
301
  }
256
302
 
257
303
  // Read and parse JSON safely
258
304
  const jsonResult = await safeReadJson("package.json");
259
305
  if (jsonResult.isOk()) {
260
- const pkg = jsonResult.unwrap();
261
- console.log("Package name:", pkg.name);
306
+ const pkg = jsonResult.unwrap();
307
+ console.log("Package name:", pkg.name);
262
308
  }
263
309
 
264
310
  // Write JSON file
265
311
  const writeResult = await safeWriteJson("data.json", { users: [] }, { pretty: true });
266
312
  if (writeResult.isErr()) {
267
- console.error("Write failed:", writeResult.unwrapErr());
313
+ console.error("Write failed:", writeResult.unwrapErr());
268
314
  }
269
315
 
270
316
  // Check if file exists
271
317
  const existsResult = await safeExists("path/to/file.txt");
272
318
  if (existsResult.isOk() && existsResult.unwrap()) {
273
- console.log("File exists!");
319
+ console.log("File exists!");
274
320
  }
275
321
 
276
322
  // Create directories (recursive)
@@ -282,11 +328,12 @@ const rmResult = await safeRm("build", { recursive: true, force: true });
282
328
  // Read file line by line
283
329
  const lineResult = await safeReadFileByLine("large-file.log");
284
330
  if (lineResult.isOk()) {
285
- for await (const line of lineResult.unwrap()) {
286
- console.log(line);
287
- }
331
+ for await (const line of lineResult.unwrap()) {
332
+ console.log(line);
333
+ }
288
334
  }
289
- ```
335
+
336
+ ````
290
337
 
291
338
  ### Glob Patterns
292
339
 
@@ -302,12 +349,12 @@ const syncFiles = globSync("**/*.test.ts", { cwd: "tests" });
302
349
 
303
350
  // Convert file path to glob pattern
304
351
  const pattern = convertPathToPattern("/home/user/project");
305
- ```
352
+ ````
306
353
 
307
354
  ### Result Pattern - Functional Error Handling
308
355
 
309
356
  ```typescript
310
- import { err, ok, Result, safeTry } from "@goodbyenjn/utils/result";
357
+ import { err, ok, Result } from "@goodbyenjn/utils/result";
311
358
 
312
359
  // Create results explicitly
313
360
  const success = ok(42);
@@ -315,7 +362,7 @@ const failure = err("Something went wrong");
315
362
 
316
363
  // Handle results with chainable methods
317
364
  const doubled = success
318
- .map(value => value * 2)
365
+ .map(value => value * 2) // Supports async: .map(async v => v * 2) returns Promise<Result>
319
366
  .mapErr(err => `Error: ${err}`)
320
367
  .unwrapOr(0); // 84
321
368
 
@@ -323,28 +370,36 @@ const doubled = success
323
370
  const result: Result<string, Error> = ok("value");
324
371
  const transformed = result.mapErr(() => new Error("Custom error"));
325
372
 
326
- // Convert throwing functions to Result
373
+ // Convert throwing functions or promises to Result
327
374
  async function fetchUser(id: string) {
328
- // If the function throws, it's caught and wrapped in Err
329
- const user = await Result.fromCallable(() => JSON.parse(userJson));
375
+ // Result.try catches thrown errors
376
+ const user = await Result.try(() => JSON.parse(userJson));
377
+ // Or handle promise rejections
378
+ const user = await Result.try(fetch(`/api/users/${id}`));
330
379
 
331
380
  return user.map(u => u.name).mapErr(err => new Error(`Failed to parse user: ${err.message}`));
332
381
  }
333
382
 
383
+ // Wrap a function to always return a Result
384
+ const safeParse = Result.wrap(JSON.parse, Error);
385
+ const data = safeParse('{"valid": true}'); // Result<any, Error>
386
+
334
387
  // Combine multiple Results
335
388
  const results = [ok(1), ok(2), err("oops"), ok(4)];
336
389
  const combined = Result.all(...results); // Err("oops")
337
390
 
338
- // Safe try-catch alternative
339
- const safeTryExample = await safeTry(async () => {
340
- return await fetch("/api/data").then(r => r.json());
391
+ // Generator-based "do" notation for flattening Results
392
+ const finalResult = Result.gen(function* () {
393
+ const a = yield* ok(10);
394
+ const b = yield* ok(20);
395
+ return a + b;
396
+ }); // ok(30)
397
+
398
+ // Supports async generators
399
+ const asyncFinal = await Result.gen(async function* () {
400
+ const user = yield* await fetchUser("1");
401
+ return user.name;
341
402
  });
342
-
343
- if (safeTryExample.isOk()) {
344
- console.log("Data:", safeTryExample.unwrap());
345
- } else {
346
- console.error("Failed:", safeTryExample.unwrapErr());
347
- }
348
403
  ```
349
404
 
350
405
  ### Type Utilities
@@ -356,46 +411,56 @@ import type {
356
411
  YieldType,
357
412
  OmitByKey,
358
413
  SetNullable,
414
+ TemplateFn,
359
415
  } from "@goodbyenjn/utils/types";
360
416
 
417
+ // ... (other types)
418
+
419
+ // Template string function type
420
+ const myTag: TemplateFn<string> = (strings, ...values) => {
421
+ return strings[0] + values[0];
422
+ };
423
+ ```
424
+
361
425
  // Nullable type for values that can be null or undefined
362
426
  type User = {
363
- id: string;
364
- name: string;
365
- email: Nullable<string>; // string | null | undefined
427
+ id: string;
428
+ name: string;
429
+ email: Nullable<string>; // string | null | undefined
366
430
  };
367
431
 
368
432
  // Optional type (undefined but not null)
369
433
  type Profile = {
370
- bio: Optional<string>; // string | undefined
434
+ bio: Optional<string>; // string | undefined
371
435
  };
372
436
 
373
437
  // Extract yield type from generators
374
- function* numberGenerator() {
375
- yield 1;
376
- yield 2;
377
- yield 3;
438
+ function\* numberGenerator() {
439
+ yield 1;
440
+ yield 2;
441
+ yield 3;
378
442
  }
379
443
 
380
444
  type NumberType = YieldType<typeof numberGenerator>; // number
381
445
 
382
446
  // Omit properties by their value type
383
447
  type Config = {
384
- name: string;
385
- debug: boolean;
386
- verbose: boolean;
387
- timeout: number;
448
+ name: string;
449
+ debug: boolean;
450
+ verbose: boolean;
451
+ timeout: number;
388
452
  };
389
453
  type WithoutBooleans = OmitByKey<Config, boolean>; // { name: string; timeout: number }
390
454
 
391
455
  // Set specific properties to nullable
392
456
  type APIResponse = {
393
- id: number;
394
- name: string;
395
- email: string;
457
+ id: number;
458
+ name: string;
459
+ email: string;
396
460
  };
397
461
  type PartialResponse = SetNullable<APIResponse, "email" | "name">; // email and name become nullable
398
- ```
462
+
463
+ ````
399
464
 
400
465
  ### Extended Remeda Utilities
401
466
 
@@ -492,7 +557,7 @@ const totalAge = sumBy(
492
557
  // Chunk array into groups
493
558
  const chunked = chunk(users, 2);
494
559
  // [[user1, user2], [user3]]
495
- ```
560
+ ````
496
561
 
497
562
  ## API Reference
498
563