@ix-xs/node-comfort 1.1.31 → 2.0.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -748
  4. package/index.js +84 -13
  5. package/package.json +201 -21
  6. package/src/Arr.js +901 -0
  7. package/src/Async.js +585 -0
  8. package/src/Cache.js +329 -0
  9. package/src/Checker.js +979 -188
  10. package/src/Cli.js +1034 -0
  11. package/src/Color.js +415 -0
  12. package/src/Crypto.js +707 -0
  13. package/src/Emitter.js +289 -0
  14. package/src/Env.js +555 -0
  15. package/src/FS.js +1483 -768
  16. package/src/Func.js +692 -0
  17. package/src/Http.js +538 -0
  18. package/src/Id.js +418 -0
  19. package/src/Logger.js +739 -307
  20. package/src/Num.js +849 -0
  21. package/src/Obj.js +904 -0
  22. package/src/SQLite.js +1308 -994
  23. package/src/Schema.js +1586 -0
  24. package/src/Str.js +999 -0
  25. package/src/Sys.js +511 -0
  26. package/src/Time.js +1323 -0
  27. package/src/Utils.js +321 -325
  28. package/src/errors.js +292 -0
  29. package/types/index.d.ts +693 -120
  30. package/types/src/Arr.d.ts +562 -0
  31. package/types/src/Arr.d.ts.map +1 -0
  32. package/types/src/Async.d.ts +321 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +195 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +609 -81
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +582 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +294 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +534 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +170 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +382 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +709 -201
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +455 -0
  51. package/types/src/Func.d.ts.map +1 -0
  52. package/types/src/Http.d.ts +374 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +200 -0
  55. package/types/src/Id.d.ts.map +1 -0
  56. package/types/src/Logger.d.ts +374 -24
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +578 -0
  59. package/types/src/Num.d.ts.map +1 -0
  60. package/types/src/Obj.d.ts +475 -0
  61. package/types/src/Obj.d.ts.map +1 -0
  62. package/types/src/SQLite.d.ts +647 -305
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +940 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +696 -0
  67. package/types/src/Str.d.ts.map +1 -0
  68. package/types/src/Sys.d.ts +269 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +664 -0
  71. package/types/src/Time.d.ts.map +1 -0
  72. package/types/src/Utils.d.ts +154 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +292 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/tsconfig.json +0 -18
  77. package/types/index.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,748 +1,210 @@
1
- # @ix-xs/node-comfort
2
-
3
-
4
- <div align="center">
5
-
6
-
7
- ![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white)
8
- ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E)
9
- ![NPM](https://img.shields.io/badge/NPM-%23CB3837.svg?style=for-the-badge&logo=npm&logoColor=white)
10
- ![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)
11
-
12
-
13
- [![npm version](https://badge.fury.io/js/%40ix-xs%2Fnode-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
14
- [![Downloads](https://img.shields.io/npm/dm/@ix-xs/node-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
15
-
16
-
17
- </div>
18
-
19
-
20
- Small collection of Node.js utilities I use across projects: a logger, a file-system helper, a SQLite wrapper, a runtime type checker, and a few async/process helpers.
21
-
22
-
23
- Everything is plain CommonJS with JSDoc. The package ships with type information for TypeScript and modern editors.
24
-
25
-
26
- ---
27
-
28
-
29
- ## Installation
30
-
31
-
32
- ```bash
33
- npm install @ix-xs/node-comfort
34
- # or
35
- yarn add @ix-xs/node-comfort
36
- # or
37
- pnpm add @ix-xs/node-comfort
38
- ```
39
-
40
-
41
- Requires Node 20+ (uses `node:sqlite` and `process.loadEnvFile`).
42
-
43
-
44
- ---
45
-
46
-
47
- ## Import and global usage
48
-
49
-
50
- The package exposes a **flat API**: all utilities are directly on the `nodeComfort` object.
51
-
52
-
53
- ```js
54
- const nodeComfort = require("@ix-xs/node-comfort");
55
-
56
-
57
- // Logger
58
- nodeComfort.log("Hello");
59
- nodeComfort.log("<% greenBright Success %>");
60
-
61
-
62
- // FS
63
- const files = nodeComfort.getFilesIn(".", true);
64
- nodeComfort.createFolder("./dist");
65
-
66
-
67
- // Checker
68
- if (nodeComfort.isArray(files)) {
69
- nodeComfort.log(`<% cyan Found ${files.length} files %>`);
70
- }
71
-
72
-
73
- // Utils
74
- await nodeComfort.wait(500);
75
- nodeComfort.dontCrash();
76
-
77
-
78
- // SQLite
79
- const db = new nodeComfort.SQLite();
80
- await db.createTable({
81
- name: "users",
82
- columns: {
83
- id: { type: "INTEGER", primaryKey: true, autoincrement: true },
84
- name: { type: "TEXT", notNull: true },
85
- },
86
- });
87
- ```
88
-
89
-
90
- ---
91
-
92
-
93
- ## Logger
94
-
95
-
96
- Colorized console logger with a minimal markup syntax and optional timestamps.
97
-
98
-
99
- You wrap styles in delimiters (by default `<%` and `%>`):
100
-
101
-
102
- - styles: `bold`, `italic`, `underline`, `overline`
103
- - colors: `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`, `black`
104
- - bright: `redBright`, `greenBright`, `yellowBright`, `blueBright`, `magentaBright`, `cyanBright`, `whiteBright`
105
- - background: `bgRed`, `bgBlue`, etc.
106
- - RGB: `rgb(255, 0, 0)`
107
-
108
-
109
- ### Basic usage
110
-
111
-
112
- ```js
113
- nodeComfort.log("Plain log");
114
- nodeComfort.log("<% green Success %>");
115
- nodeComfort.log("<% red bold Error:%> something went wrong");
116
- nodeComfort.log("<% bgBlue white INFO %> message");
117
- ```
118
-
119
-
120
- Multi-line strings keep indentation and timestamps readable:
121
-
122
-
123
- ```js
124
- nodeComfort.log(`
125
- <% yellow Multi-line %>
126
- This is line 2
127
- This is line 3
128
- `);
129
- ```
130
-
131
-
132
- ### Timestamp and delimiters
133
-
134
-
135
- ```js
136
- // Disable timestamps
137
- nodeComfort.setTimestamp(false);
138
-
139
-
140
- // Change delimiters if "<%" conflicts with other template syntax
141
- nodeComfort.setDelimiter({ open: "{{", close: "}}" });
142
- nodeComfort.log("{{red Hello}} world");
143
- ```
144
-
145
-
146
- ### Groups
147
-
148
-
149
- Groups add indentation to related logs.
150
-
151
-
152
- ```js
153
- nodeComfort.group("Startup");
154
-
155
-
156
- nodeComfort.log("Loading config…");
157
- nodeComfort.log("<% green OK %>");
158
-
159
-
160
- nodeComfort.group("DB");
161
- nodeComfort.log("Connecting…");
162
- // ...
163
- nodeComfort.groupEnd(); // end "DB"
164
-
165
-
166
- nodeComfort.groupEnd(); // end "Startup"
167
- ```
168
-
169
-
170
- ---
171
-
172
-
173
- ## FS
174
-
175
-
176
- File-system helper with safe path resolution, recursive walking, copy/move helpers, and a small watcher abstraction.
177
-
178
-
179
- It resolves relative paths from the **caller file**, not just from `process.cwd()`, which makes it nicer inside libraries.
180
-
181
-
182
- ### Path helpers
183
-
184
-
185
- ```js
186
- // Absolute path, even if it doesn't exist yet
187
- const configPath = nodeComfort.createPath("./config/app.json");
188
-
189
-
190
- // Existing folder/file or undefined
191
- const folder = nodeComfort.getFolder("./src");
192
- const file = nodeComfort.getFile("package.json");
193
- ```
194
-
195
-
196
- ### Create folders and files
197
-
198
-
199
- ```js
200
- // Folders
201
- nodeComfort.createFolder("./dist"); // true if created
202
- nodeComfort.createFolder("./dist", true); // force recreate
203
-
204
-
205
- // Files
206
- nodeComfort.createFile("./dist/info.txt", false, "hello");
207
- nodeComfort.createFile("./dist/data.json", true, { ok: true });
208
- ```
209
-
210
-
211
- ### List and delete
212
-
213
-
214
- ```js
215
- // Folders inside a path
216
- const dirs = nodeComfort.getFoldersIn("./src", true);
217
-
218
-
219
- // Delete folders matching a filter
220
- nodeComfort.deleteFoldersIn("./tmp", (folder) =>
221
- folder.endsWith(".cache"),
222
- );
223
-
224
-
225
- // Files
226
- const files = nodeComfort.getFilesIn("./src", true);
227
-
228
-
229
- // Delete files matching a filter
230
- nodeComfort.deleteFilesIn("./logs", false, (file) =>
231
- file.endsWith(".log"),
232
- );
233
- ```
234
-
235
-
236
- ### Copy
237
-
238
-
239
- ```js
240
- // Copy one folder
241
- nodeComfort.copyFolder({
242
- path: "./templates",
243
- dest: "./dist/templates",
244
- recursive: true,
245
- withFiles: true,
246
- force: true,
247
- });
248
-
249
-
250
- // Copy only some folders in a tree
251
- nodeComfort.copyFoldersIn({
252
- path: "./src",
253
- dest: "./dist",
254
- recursive: true,
255
- withFiles: false,
256
- filter: (folder) => folder.endsWith("components"),
257
- });
258
-
259
-
260
- // Copy files
261
- nodeComfort.copyFilesIn({
262
- path: "./src",
263
- dest: "./dist",
264
- recursive: true,
265
- force: true,
266
- filter: (file) => file.endsWith(".js"),
267
- });
268
-
269
-
270
- // Copy a single file
271
- nodeComfort.copyFile({
272
- path: "./src/index.js",
273
- dest: "./dist", // or "./dist/index.js"
274
- force: true,
275
- });
276
- ```
277
-
278
-
279
- ### Move
280
-
281
-
282
- Move helpers are basically copy + delete.
283
-
284
-
285
- ```js
286
- // Move a folder tree
287
- nodeComfort.moveFolder({
288
- path: "./build",
289
- dest: "./dist",
290
- recursive: true,
291
- withFiles: true,
292
- force: true,
293
- });
294
-
295
-
296
- // Move some folders under a path
297
- nodeComfort.moveFoldersIn({
298
- path: "./tmp",
299
- dest: "./archive",
300
- recursive: true,
301
- withFiles: false,
302
- filter: (folder) => folder.includes("session"),
303
- });
304
-
305
-
306
- // Move files
307
- nodeComfort.moveFilesIn({
308
- path: "./logs",
309
- dest: "./logs/archive",
310
- recursive: false,
311
- force: true,
312
- filter: (file) => file.endsWith(".log"),
313
- });
314
-
315
-
316
- // Move a single file
317
- nodeComfort.moveFile({
318
- path: "./logs/app.log",
319
- dest: "./logs/archive/app.log",
320
- force: true,
321
- });
322
- ```
323
-
324
-
325
- ### Read and watch
326
-
327
-
328
- ```js
329
- // Read a UTF-8 file
330
- const content = nodeComfort.readFile("./README.md");
331
-
332
-
333
- // Watch a directory
334
- const watcher = nodeComfort.watch({
335
- path: "./src",
336
- recursive: true,
337
- filter: (event, file) => file.endsWith(".js"),
338
- });
339
-
340
-
341
- watcher
342
- .on("change", (file) => {
343
- nodeComfort.log(`<% cyan File changed: %> ${file}`);
344
- })
345
- .on("rename", (file) => {
346
- nodeComfort.log(`<% yellow File renamed: %> ${file}`);
347
- });
348
- ```
349
-
350
-
351
- You can pause, resume and stop watching:
352
-
353
-
354
- ```js
355
- watcher.pause();
356
- // ...
357
- watcher.resume();
358
- // ...
359
- watcher.stop();
360
- ```
361
-
362
-
363
- ---
364
-
365
-
366
- ## Checker
367
-
368
-
369
- Runtime type-checking helpers. Everything is just a boolean guard that works on any input.
370
-
371
-
372
- ```js
373
- nodeComfort.isArray(); // true[1][2]
374
- nodeComfort.isArray("nope"); // false
375
-
376
-
377
- nodeComfort.isNumber(42); // true
378
- nodeComfort.isNumber(NaN); // false
379
-
380
-
381
- nodeComfort.isFunction(() => {}); // true
382
- nodeComfort.isFunction(class {}); // true
383
-
384
-
385
- nodeComfort.isObject({}); // true
386
- nodeComfort.isObject([]); // true
387
- nodeComfort.isObject(null); // false
388
-
389
-
390
- nodeComfort.isBoolean(false); // true
391
- nodeComfort.isString("hello"); // true
392
- nodeComfort.isUndefined(undefined); // true
393
- nodeComfort.isNull(null); // true
394
-
395
-
396
- nodeComfort.isDate(new Date()); // true
397
- nodeComfort.isMap(new Map()); // true
398
- nodeComfort.isSet(new Set()); // true
399
-
400
-
401
- nodeComfort.isError(new Error("x")); // true
402
- nodeComfort.isError({ message: "x" }); // false
403
- ```
404
-
405
-
406
- ---
407
-
408
-
409
- ## Utils
410
-
411
-
412
- Small async and process helpers.
413
-
414
-
415
- ### wait(ms)
416
-
417
-
418
- ```js
419
- await nodeComfort.wait(500);
420
- // code here runs after ~500ms
421
- ```
422
-
423
-
424
- ### when(predicate, payload?, options?)
425
-
426
-
427
- Polling helper that emits events until a condition is met.
428
-
429
-
430
- ```js
431
- const task = nodeComfort.when(
432
- () => Math.random() > 0.8,
433
- { label: "Lucky" },
434
- { interval: 200, max: 3, timeout: 5000 },
435
- );
436
-
437
-
438
- task
439
- .on("trigger", (payload) => {
440
- nodeComfort.log(`<% green Triggered: %> ${payload.label}`);
441
- })
442
- .on("timeout", () => {
443
- nodeComfort.log("<% red Timeout %>");
444
- })
445
- .on("error", (err) => {
446
- nodeComfort.log(`<% red Error in predicate %>\n${err}`);
447
- })
448
- .start();
449
- ```
450
-
451
-
452
- - `predicate`: boolean, promise, or function (sync/async) returning a boolean.
453
- - `payload`: value passed to listeners on `trigger` / `timeout`.
454
- - `options`:
455
- - `interval` (ms, default `50`)
456
- - `timeout` (ms or `null`)
457
- - `max` (number of triggers or `null` for infinite)
458
-
459
-
460
- ### dontCrash()
461
-
462
-
463
- Installs process-level safety nets for errors, exits and signals.
464
-
465
-
466
- ```js
467
- nodeComfort.dontCrash();
468
-
469
-
470
- // Or customize:
471
- nodeComfort.dontCrash()
472
- .on("error", (err) => {
473
- console.error("Global error:", err);
474
- process.exit(1);
475
- })
476
- .on("exit", (code) => {
477
- nodeComfort.log(`<% gray Process exited with code ${code} %>`);
478
- })
479
- .on("sig", (signal) => {
480
- nodeComfort.log(`<% yellow Caught signal ${signal}, exiting… %>`);
481
- process.exit(0);
482
- });
483
- ```
484
-
485
-
486
- Events:
487
-
488
-
489
- - `"error"` – uncaught exceptions + unhandled rejections
490
- - `"exit"` – `process.on("exit")`
491
- - `"sig"` – `SIGINT`, `SIGTERM`, `SIGQUIT`
492
- - `"beforeExit"` – `process.on("beforeExit")`
493
-
494
-
495
- ### JSON helpers
496
-
497
-
498
- ```js
499
- const json = nodeComfort.JSONString({ hello: "world" });
500
- // pretty-printed with 4 spaces
501
-
502
-
503
- const data = nodeComfort.JSONParse(json);
504
- ```
505
-
506
-
507
- ---
508
-
509
-
510
- ## SQLite
511
-
512
-
513
- Thin wrapper over the built-in `node:sqlite` `DatabaseSync` with a small "model" system and CRUD-style helpers.
514
-
515
-
516
- ### Creating an instance
517
-
518
-
519
- ```js
520
- // Default: ./db.sqlite (relative to cwd)
521
- const db = new nodeComfort.SQLite();
522
-
523
-
524
- // Custom path (folder created if needed)
525
- const db2 = new nodeComfort.SQLite("data/app.sqlite");
526
- ```
527
-
528
-
529
- The constructor ensures the parent folder exists, and enables WAL journal mode and foreign keys by default.
530
-
531
-
532
- ### Defining a table
533
-
534
-
535
- ```js
536
- await db.createTable({
537
- name: "users",
538
- columns: {
539
- id: {
540
- type: "INTEGER",
541
- primaryKey: true,
542
- autoincrement: true,
543
- },
544
- name: {
545
- type: "TEXT",
546
- notNull: true,
547
- },
548
- role: {
549
- type: "TEXT",
550
- values: ["admin", "user"],
551
- defaultValue: "user",
552
- },
553
- createdAt: {
554
- type: "TEXT",
555
- defaultValue: new Date().toISOString(),
556
- },
557
- },
558
- });
559
- ```
560
-
561
-
562
- Models are kept in memory and used for defaults like ordering.
563
-
564
-
565
- The `columns` property is the new, recommended way. The legacy `options` property is still supported for backward compatibility.
566
-
567
-
568
- ### Insert / update / upsert
569
-
570
-
571
- ```js
572
- // Insert a new row
573
- await db.insert("users", { name: "John" });
574
-
575
-
576
- // Update rows matching a where clause
577
- await db.update("users", { role: "admin" }, { name: "John" });
578
-
579
-
580
- // Upsert (INSERT with ON CONFLICT DO UPDATE)
581
- await db.upsert("users", { id: 1, role: "user" }, ["id"]);
582
- ```
583
-
584
-
585
- When `where` or `data.id` is provided, `set` (legacy helper):
586
-
587
-
588
- 1. loads the existing row if it exists,
589
- 2. merges it with `data`,
590
- 3. inserts or updates depending on whether the row exists.
591
-
592
-
593
- ```js
594
- // Insert
595
- await db.set("users", { name: "John" });
596
-
597
-
598
- // Upsert by id
599
- await db.set("users", { id: 1, role: "admin" });
600
-
601
-
602
- // Upsert with custom where
603
- await db.set("users", { role: "user" }, { name: "John" });
604
- ```
605
-
606
-
607
- ### Get / list
608
-
609
-
610
- ```js
611
- // One row or undefined
612
- const user = await db.get("users", { name: "John" });
613
-
614
-
615
- // Multiple rows with pagination and ordering
616
- const admins = await db.getAll(
617
- "users",
618
- { role: "admin" }, // where
619
- {
620
- limit: 100,
621
- offset: 0,
622
- orderBy: "createdAt",
623
- direction: "DESC",
624
- },
625
- );
626
- ```
627
-
628
-
629
- If the table is not registered, `getAll` returns an empty array.
630
-
631
-
632
- ### Delete / clear / count
633
-
634
-
635
- ```js
636
- // Delete rows
637
- const res = await db.delete("users", { role: "user" });
638
- // res.ok, res.changes
639
-
640
-
641
- // Delete all rows
642
- await db.clearTable("users");
643
-
644
-
645
- // Count rows
646
- const totalUsers = await db.count("users");
647
- const adminCount = await db.count("users", { role: "admin" });
648
- ```
649
-
650
-
651
- ### Indexes and constraints
652
-
653
-
654
- ```js
655
- // Create a composite unique index
656
- await db.createIndex("UserAuthMethod", {
657
- columns: ["provider", "providerUserId"],
658
- unique: true,
659
- });
660
-
661
-
662
- // Create a non-unique index
663
- await db.createIndex("Post", {
664
- columns: ["userId"],
665
- });
666
- ```
667
-
668
-
669
- ```js
670
- // Table-level constraints
671
- await db.createTable({
672
- name: "UserAuthMethod",
673
- columns: {
674
- id: { type: "INTEGER", primaryKey: true, autoincrement: true },
675
- userId: { type: "INTEGER", notNull: true },
676
- provider: { type: "TEXT", notNull: true },
677
- providerUserId: { type: "TEXT", notNull: true },
678
- },
679
- constraints: [
680
- {
681
- type: "unique",
682
- columns: ["provider", "providerUserId"],
683
- },
684
- {
685
- type: "foreignKey",
686
- columns: ["userId"],
687
- references: { table: "User", columns: ["id"] },
688
- onDelete: "CASCADE",
689
- },
690
- ],
691
- });
692
- ```
693
-
694
-
695
- ### Transactions
696
-
697
-
698
- ```js
699
- const result = await db.transaction(async () => {
700
- await db.insert("User", {
701
- email: "a@b.com",
702
- createdAt: Date.now(),
703
- });
704
- await db.insert("Profile", {
705
- userId: 1,
706
- nickname: "John",
707
- });
708
- return { ok: true };
709
- });
710
- ```
711
-
712
-
713
- If the callback throws, the transaction is rolled back.
714
-
715
-
716
- ### Raw SQL helpers
717
-
718
-
719
- ```js
720
- // Execute a write statement (INSERT/UPDATE/DELETE/CREATE/PRAGMA)
721
- await db.exec("CREATE INDEX IF NOT EXISTS idx_email ON users (email)");
722
-
723
-
724
- // Execute a query and get one row
725
- const user = await db.queryOne("SELECT * FROM users WHERE id = ?", );[1]
726
-
727
-
728
- // Execute a query and get all rows
729
- const users = await db.queryAll("SELECT * FROM users WHERE role = ?", ["admin"]);
730
- ```
731
-
732
-
733
- ### Drop and close
734
-
735
-
736
- ```js
737
- await db.deleteTable("users");
738
- db.close();
739
- ```
740
-
741
-
742
- ---
743
-
744
-
745
- ## License
746
-
747
-
748
- MIT.
1
+ <div align="center">
2
+
3
+ # node-comfort
4
+
5
+ **The standard library Node.js deserves.**
6
+
7
+ Logging, config, validation, HTTP, SQLite, dates, crypto, CLI tools and 500+ helpers.<br>
8
+ One package, zero dependencies, documented right in your editor.
9
+
10
+ [![npm version](https://img.shields.io/npm/v/@ix-xs/node-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
11
+ [![Downloads](https://img.shields.io/npm/dm/@ix-xs/node-comfort.svg)](https://www.npmjs.com/package/@ix-xs/node-comfort)
12
+ [![CI](https://github.com/ix-xs/node-comfort/actions/workflows/ci.yml/badge.svg)](https://github.com/ix-xs/node-comfort/actions/workflows/ci.yml)
13
+ [![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://www.npmjs.com/package/@ix-xs/node-comfort?activeTab=dependencies)
14
+ [![Node](https://img.shields.io/node/v/@ix-xs/node-comfort.svg)](https://nodejs.org)
15
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
16
+
17
+ [Documentation](https://ix-xs.github.io/node-comfort/) •
18
+ [Getting started](https://ix-xs.github.io/node-comfort/guide/getting-started.html) •
19
+ [API reference](https://ix-xs.github.io/node-comfort/api/index.html) •
20
+ [Recipes](https://ix-xs.github.io/node-comfort/guide/recipes.html) •
21
+ [Changelog](./CHANGELOG.md)
22
+
23
+ </div>
24
+
25
+ ---
26
+
27
+ Most Node.js projects install the same twenty packages before writing a single line of their own: a logger, dotenv, a validation library, an HTTP client, a retry helper, a date library, uuid, bcrypt, a CLI parser, a spinner. Hundreds of transitive dependencies come with them.
28
+
29
+ Node.js can do most of this on its own now. It ships `fetch`, `Intl`, `node:crypto` and even SQLite. node-comfort is the missing layer on top: one consistent, fully typed API, and nothing else to install.
30
+
31
+ ```js
32
+ const nc = require("@ix-xs/node-comfort");
33
+
34
+ const config = nc.env.validate({
35
+ PORT: { type: "port", default: 3000 },
36
+ DATABASE_URL: { type: "url" },
37
+ });
38
+
39
+ const db = new nc.SQLite("./data/app.sqlite");
40
+ const github = nc.http.create({ baseURL: "https://api.github.com/", retry: { attempts: 3 } });
41
+
42
+ const { data: repo } = await github.get("repos/nodejs/node");
43
+ nc.info(`Node.js has <% yellow ${nc.num.abbreviate(repo.stargazers_count)} %> stars`);
44
+
45
+ nc.sys.onShutdown(() => db.close());
46
+ ```
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ npm install @ix-xs/node-comfort
52
+ ```
53
+
54
+ Node.js 22.13 or newer. That's the first version where `node:sqlite` works without a flag.
55
+
56
+ ## Why node-comfort
57
+
58
+ - **Zero dependencies.** Built only on Node's own modules. No install scripts, nothing else to audit or keep up to date.
59
+ - **Your editor already knows it.** Every function, parameter and option is typed and documented, with examples. Type `nc.` and look around. It works in plain JavaScript too.
60
+ - **Fast to load.** Namespaces load the first time you use them. Requiring the package takes about 3 ms, and a script that only logs never loads the database or the HTTP client.
61
+ - **Safe by default.** Prototype-pollution-proof objects, scrypt passwords, AES-256-GCM, constant-time comparisons, parameterized SQL, atomic file writes.
62
+ - **One way of doing things.** Durations are always `"5m"` or milliseconds, options always come last, and every error has a stable `code`.
63
+ - **Tested everywhere.** Linux, macOS and Windows, Node.js 22 and 24, TypeScript 5.9, 6 and 7.
64
+
65
+ ## What's inside
66
+
67
+ | Namespace | What it's for | Often replaces |
68
+ | --- | --- | --- |
69
+ | [Logger](https://ix-xs.github.io/node-comfort/guide/logger.html) | Colorful logs, levels, JSON output, log files with rotation | `chalk`, `pino`, `winston` |
70
+ | [`fs`](https://ix-xs.github.io/node-comfort/guide/fs.html) | Files and folders without try/catch, atomic writes, glob, watch | `fs-extra`, `glob`, `rimraf` |
71
+ | [`checker`](https://ix-xs.github.io/node-comfort/guide/checker.html) | 60+ type guards and validators, `assert` | `is`, `validator` |
72
+ | [`env`](https://ix-xs.github.io/node-comfort/guide/env.html) | `.env` loading, typed and validated configuration | `dotenv`, `envalid` |
73
+ | [`schema`](https://ix-xs.github.io/node-comfort/guide/schema.html) | Data validation with TypeScript inference | `zod`, `yup`, `joi` |
74
+ | [`http`](https://ix-xs.github.io/node-comfort/guide/http.html) | `fetch` with JSON, retries, timeouts, hooks, downloads | `axios`, `got`, `ky` |
75
+ | [`SQLite`](https://ix-xs.github.io/node-comfort/guide/sqlite.html) | CRUD, filters, JSON columns, transactions, migrations | `better-sqlite3` and helpers |
76
+ | [`time`](https://ix-xs.github.io/node-comfort/guide/time.html) | Formatting in any language and time zone, durations, date math, cron | `dayjs`, `ms`, `node-cron` |
77
+ | [`str`](https://ix-xs.github.io/node-comfort/guide/str.html) | Case conversion, slugs, emoji-safe truncation, fuzzy matching, plurals | `lodash`, `slugify`, `leven` |
78
+ | [`num`](https://ix-xs.github.io/node-comfort/guide/num.html) | Exact rounding, statistics, currencies, bytes | `numeral`, `pretty-bytes` |
79
+ | [`arr`](https://ix-xs.github.io/node-comfort/guide/arr.html) | Chunk, group, multi-key sort, paginate, set operations | `lodash` |
80
+ | [`obj`](https://ix-xs.github.io/node-comfort/guide/obj.html) | Deep clone, merge, equal and diff, typed dot paths | `lodash`, `deepmerge`, `dequal` |
81
+ | [`func`](https://ix-xs.github.io/node-comfort/guide/func.html) | Debounce, throttle, memoize, retry with backoff, timeout | `p-retry`, `p-timeout` |
82
+ | [`async`](https://ix-xs.github.io/node-comfort/guide/async.html) | Concurrency limits, queues, mutex, polling | `p-limit`, `p-map`, `p-queue` |
83
+ | [`Cache`](https://ix-xs.github.io/node-comfort/guide/cache.html) | LRU cache with TTL, loaders and statistics | `lru-cache` |
84
+ | [`Emitter`](https://ix-xs.github.io/node-comfort/guide/emitter.html) | Typed events, `waitFor`, async iteration | `mitt`, `emittery` |
85
+ | [`id`](https://ix-xs.github.io/node-comfort/guide/id.html) | UUID v4 and v7, ULID, nanoid, tokens, Snowflakes | `uuid`, `ulid`, `nanoid` |
86
+ | [`crypto`](https://ix-xs.github.io/node-comfort/guide/crypto.html) | Passwords, encryption, JWT, TOTP two-factor codes | `bcrypt`, `jsonwebtoken`, `otplib` |
87
+ | [`cli`](https://ix-xs.github.io/node-comfort/guide/cli.html) | Typed arguments, prompts, menus, spinners, progress bars, tables | `commander`, `inquirer`, `ora` |
88
+ | [`color`](https://ix-xs.github.io/node-comfort/guide/color.html) | Chainable terminal colors, truecolor, gradients, links | `chalk`, `picocolors` |
89
+ | [`sys`](https://ix-xs.github.io/node-comfort/guide/sys.html) | Run commands, `which`, graceful shutdown, platform detection | `execa`, `which` |
90
+ | [`utils`](https://ix-xs.github.io/node-comfort/guide/utils.html) | `wait`, safe JSON, crash guard | |
91
+ | [`errors`](https://ix-xs.github.io/node-comfort/guide/errors.html) | Error classes with stable codes | |
92
+
93
+ The logger, `fs`, `checker` and `utils` helpers are also available at the top level: `nc.info()`, `nc.readJSON()`, `nc.isEmail()`, `nc.wait()`.
94
+
95
+ ## Importing
96
+
97
+ ```js
98
+ // CommonJS
99
+ const nc = require("@ix-xs/node-comfort");
100
+ const { str, isEmail, info } = require("@ix-xs/node-comfort");
101
+
102
+ // ES modules
103
+ import nc from "@ix-xs/node-comfort";
104
+ import { slugify } from "@ix-xs/node-comfort/str"; // loads only what it needs
105
+ ```
106
+
107
+ Every namespace has its own entry point (`@ix-xs/node-comfort/http`, `/sqlite`, `/schema`...). In an ES module, importing from these keeps startup lean. See [Performance](https://ix-xs.github.io/node-comfort/guide/performance.html).
108
+
109
+ TypeScript needs no setup: the types ship with the package.
110
+
111
+ ```ts
112
+ import { schema as s, type Infer } from "@ix-xs/node-comfort";
113
+
114
+ const User = s.object({ email: s.string().email(), age: s.number().int().optional() });
115
+ type User = Infer<typeof User>; // { email: string; age?: number | undefined }
116
+ ```
117
+
118
+ ## A quick tour
119
+
120
+ **Logs** that look good in a terminal and turn into JSON in production:
121
+
122
+ ```js
123
+ nc.info("Server listening on port 3000").success("Connected to the database");
124
+ nc.log("<% cyan bold Tip:%> press <% bgWhite black q %> to quit");
125
+
126
+ const log = nc.createLogger({ scope: "api", file: { path: "./logs/api.log", maxSize: "10MB", maxFiles: 5 } });
127
+ const reqLog = log.child("request", { fields: { requestId: "a1b2" } });
128
+ ```
129
+
130
+ **Validation** with the type inferred for you:
131
+
132
+ ```js
133
+ const User = nc.schema.object({
134
+ email: nc.schema.string().trim().email(),
135
+ age: nc.schema.number().int().min(18).optional(),
136
+ role: nc.schema.enum(["admin", "user"]).default("user"),
137
+ });
138
+
139
+ const result = User.safeParse(req.body);
140
+ if (!result.success) return res.status(400).json(result.error.flatten());
141
+ ```
142
+
143
+ **HTTP** on top of `fetch`, with the parts you always end up writing:
144
+
145
+ ```js
146
+ const api = nc.http.create({
147
+ baseURL: "https://api.example.com/v1/",
148
+ auth: { bearer: process.env.API_TOKEN },
149
+ timeout: "10s",
150
+ retry: { attempts: 3 },
151
+ });
152
+
153
+ const { data: users } = await api.get("users", { query: { page: 2 } });
154
+ await api.post("users", { name: "Ada" });
155
+ ```
156
+
157
+ **SQLite** without compiling anything, with objects in and objects out:
158
+
159
+ ```js
160
+ const db = new nc.SQLite("./data/app.sqlite");
161
+
162
+ db.insert("users", { email: "ada@example.com", settings: { theme: "dark" } });
163
+ const admins = db.getAll("users", { role: ["admin", "owner"], age: { gte: 18 } }, { orderBy: "email", limit: 20 });
164
+ db.transaction(() => {
165
+ db.update("accounts", { balance: 90 }, { id: 1 });
166
+ db.update("accounts", { balance: 110 }, { id: 2 });
167
+ });
168
+ ```
169
+
170
+ **Dates** in any language and time zone:
171
+
172
+ ```js
173
+ nc.time.setLocale("fr").setTimezone("Europe/Paris");
174
+ nc.time.format(new Date(), "dddd D MMMM YYYY HH:mm"); // "vendredi 11 septembre 2026 14:30"
175
+ nc.time.relative(Date.now() - 3 * 3600e3); // "il y a 3 heures"
176
+ nc.time.cron("0 9 * * 1-5", sendReport); // weekdays at 9:00
177
+ ```
178
+
179
+ **Command-line tools** with typed flags, prompts and spinners:
180
+
181
+ ```js
182
+ const { flags } = nc.cli.args({
183
+ port: { type: "number", short: "p", default: 3000, description: "Port to listen on" },
184
+ }, { version: "1.0.0" }); // --help and --version for free
185
+
186
+ const env = await nc.cli.select("Environment?", ["staging", "production"]);
187
+ const spinner = nc.cli.spinner(`Deploying to ${env}`).start();
188
+ spinner.succeed("Deployed");
189
+ ```
190
+
191
+ That's a small part of it. The [guide](https://ix-xs.github.io/node-comfort/guide/introduction.html) walks through every namespace, and the [recipes](https://ix-xs.github.io/node-comfort/guide/recipes.html) show them working together: a small API server, a CLI, a scheduled job.
192
+
193
+ ## Coming from 1.x
194
+
195
+ 2.0 keeps the 1.x API but changes a few behaviors. The main ones:
196
+
197
+ - Node.js 22.13 or newer is required.
198
+ - SQLite methods throw a `SQLiteError` instead of returning `{ error }`, and `getAll()` no longer adds a default limit and order.
199
+ - `warn` and `error` write to stderr.
200
+ - Deep imports (`@ix-xs/node-comfort/src/...`) are replaced by official entry points (`@ix-xs/node-comfort/str`).
201
+
202
+ The [migration guide](https://ix-xs.github.io/node-comfort/guide/migration.html) lists every change with before and after examples.
203
+
204
+ ## Contributing
205
+
206
+ Bug reports, ideas and pull requests are welcome. [CONTRIBUTING.md](./CONTRIBUTING.md) explains how the project is organized. In short: `npm install`, then `npm run check` builds everything and runs the tests.
207
+
208
+ ## License
209
+
210
+ [MIT](./LICENSE) © ix-xs