@ix-xs/node-comfort 1.2.0 → 2.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,622 +1,211 @@
1
- # @ix-xs/node-comfort
2
-
3
1
  <div align="center">
4
2
 
5
- ![Node.js](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white)
6
- ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E)
7
- ![NPM](https://img.shields.io/badge/NPM-%23CB3837.svg?style=for-the-badge&logo=npm&logoColor=white)
8
- ![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)
9
-
10
- [![npm version](https://badge.fury.io/js/%40ix-xs%2Fnode-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
- [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://www.npmjs.com/package/@ix-xs/node-comfort)
13
-
14
- </div>
3
+ # node-comfort
15
4
 
16
- A **zero-dependency** comfort belt for Node.js • the helpers you rewrite in every
17
- project, in one small, well-typed package:
18
-
19
- - 🎨 **Logger** • colorful console logging with a tiny markup syntax and levels
20
- - 📁 **FS** • safe filesystem operations, JSON I/O, watching, hashing
21
- - 🧪 **Checker** • 35 runtime type guards (`isEmail`, `isEmpty`, `isPlainObject`, …)
22
- - ⏳ **Utils** • `wait`, `when`, `dontCrash`, JSON helpers
23
- - 🔤 **str** • case conversion, slugs, truncation, templating
24
- - 🔢 **num** • clamp, round, ranges, byte/duration/number formatting
25
- - 📚 **arr** • chunk, groupBy, unique, set operations, shuffle
26
- - 🧩 **obj** • deep clone/merge/equal, dot-path get/set, pick/omit
27
- - 🛠️ **func** • debounce, throttle, memoize, retry, timeout, pipe/compose
28
- - 🕒 **time** • durations, relative time, date math • locale & time-zone aware
29
- - 🆔 **id** • secure UUID / ULID / nanoid / tokens, hashing, constant-time compare
30
- - 🗄️ **SQLite** • a small model + CRUD wrapper over Node's built-in `node:sqlite`
31
-
32
- Everything is plain CommonJS with JSDoc, and the package ships full TypeScript
33
- type definitions.
5
+ **The standard library Node.js deserves.**
34
6
 
35
- ---
7
+ Logging, config, validation, HTTP, SQLite, dates, crypto, CLI tools and 500+ helpers.<br>
8
+ One package, zero dependencies, documented right in your editor.
36
9
 
37
- ## Installation
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)
38
16
 
39
- ```bash
40
- npm install @ix-xs/node-comfort
41
- # or
42
- yarn add @ix-xs/node-comfort
43
- # or
44
- pnpm add @ix-xs/node-comfort
45
- ```
17
+ [Documentation](https://ix-xs.github.io/node-comfort/) •
18
+ [Getting started](https://ix-xs.github.io/node-comfort/start/getting-started/) •
19
+ [API reference](https://ix-xs.github.io/node-comfort/reference/) •
20
+ [Recipes](https://ix-xs.github.io/node-comfort/recipes/) •
21
+ [Changelog](./CHANGELOG.md)
46
22
 
47
- Requires **Node 20.12+** (uses `node:sqlite`, `process.loadEnvFile`, `structuredClone`).
23
+ </div>
48
24
 
49
25
  ---
50
26
 
51
- ## Quick start
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.
52
28
 
53
- The package exposes a **flat API** for the historical modules (Logger, FS,
54
- Checker, Utils) and **namespaces** for the newer utility collections.
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.
55
30
 
56
31
  ```js
57
32
  const nc = require("@ix-xs/node-comfort");
58
33
 
59
- // Logger (flat)
60
- nc.log("<% greenBright Success %>");
61
- nc.info("server started");
62
- nc.error(new Error("boom"));
63
-
64
- // FS (flat)
65
- nc.writeJSON("./data/config.json", { ready: true });
66
- const cfg = nc.readJSON("./data/config.json", {});
67
-
68
- // Checker (flat)
69
- nc.isEmail("john@example.com"); // true
70
- nc.isEmpty([]); // true
71
-
72
- // Utils (flat)
73
- await nc.wait(500);
74
-
75
- // Namespaced collections
76
- nc.str.slugify("Héllo World"); // "hello-world"
77
- nc.num.formatBytes(1536); // "1.5 KB"
78
- nc.arr.chunk([1, 2, 3, 4], 2); // [[1, 2], [3, 4]]
79
- nc.obj.get(data, "a.b[0].c", "def");
80
- nc.func.debounce(fn, 200);
81
- nc.time.relative(Date.now() - 3600e3); // "1 hour ago"
82
- nc.id.uuid();
83
-
84
- // SQLite (class)
85
- const db = new nc.SQLite("data/app.sqlite");
86
- ```
87
-
88
- > The flat modules are also available as namespaces if you prefer explicit
89
- > grouping: `nc.logger`, `nc.fs`, `nc.checker`, `nc.utils`.
90
-
91
- ---
92
-
93
- ## API at a glance
94
-
95
- | Namespace | Highlights |
96
- | --- | --- |
97
- | flat / `logger` | `log` `info` `success` `warn` `error` `debug` `group` `groupEnd` `setTimestamp` `setDelimiter` |
98
- | flat / `fs` | `readFile` `readJSON` `writeJSON` `appendFile` `createFile` `createFolder` `copy*` `move*` `delete*` `getFilesIn` `getFoldersIn` `exists` `stat` `fileSize` `hashFile` `emptyFolder` `watch` `getEnv` |
99
- | flat / `checker` | `isArray` `isNumber` `isInteger` `isString` `isPlainObject` `isPromise` `isEmpty` `isEmail` `isURL` `isUUID` `isJSON` `isNumeric` … (35 total) |
100
- | flat / `utils` | `wait` `when` `dontCrash` `JSONString` `JSONParse` |
101
- | `str` | `capitalize` `camelCase` `snakeCase` `kebabCase` `slugify` `truncate` `template` `escapeHTML` `words` … |
102
- | `num` | `clamp` `round` `range` `sum` `average` `median` `formatBytes` `abbreviate` `thousands` `ordinal` `percent` … |
103
- | `arr` | `chunk` `unique` `groupBy` `keyBy` `partition` `sortBy` `difference` `intersection` `union` `zip` `shuffle` `sample` … |
104
- | `obj` | `clone` `merge` `equal` `get` `set` `has` `pick` `omit` `mapValues` `flatten` `unflatten` `deepFreeze` … |
105
- | `func` | `debounce` `throttle` `once` `memoize` `retry` `timeout` `pipe` `compose` `curry` `promisify` `attempt` … |
106
- | `time` | `parseDuration` `formatDuration` `relative` `format` `add` `subtract` `diff` `startOf` `isSameDay` `stopwatch` `setLocale` `setTimezone` … (i18n + time-zone aware) |
107
- | `id` | `uuid` `ulid` `nano` `token` `code` `hash` `hmac` `safeEqual` `seq` |
108
- | `SQLite` | `createTable` `insert` `update` `upsert` `get` `getAll` `delete` `count` `transaction` `createIndex` … |
109
-
110
- ---
111
-
112
- ## Logger
113
-
114
- Colorized console logger with a minimal markup syntax, optional timestamps,
115
- groups, and level helpers.
116
-
117
- Wrap styles in delimiters (default `<%` … `%>`):
118
-
119
- - **styles**: `bold`, `italic`, `underline`, `overline`
120
- - **colors**: `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`, `black`
121
- - **bright**: `redBright`, `greenBright`, … `whiteBright`
122
- - **background**: `bgRed`, `bgBlue`, …
123
- - **RGB**: `rgb(255, 0, 0)`
124
-
125
- ```js
126
- nc.log("Plain log");
127
- nc.log("<% green Success %>");
128
- nc.log("<% red bold Error:%> something went wrong");
129
- nc.log("<% bgBlue white INFO %> message");
130
- nc.log("<% rgb(255,128,0) Orange text %>");
131
- ```
132
-
133
- ### Level helpers
134
-
135
- ```js
136
- nc.info("Loading configuration…"); // ℹ INFO
137
- nc.success("Database connected"); // ✔ OK
138
- nc.warn("Cache miss"); // ⚠ WARN
139
- nc.error(new Error("Boom")); // ✖ ERROR (prints the stack)
140
- nc.debug({ requestId: 42 }); // ● DEBUG (only when DEBUG/NODE_DEBUG is set)
141
- ```
142
-
143
- ### Timestamp, delimiters & groups
144
-
145
- ```js
146
- nc.setTimestamp(false);
147
- nc.setDelimiter({ open: "{{", close: "}}" });
148
- nc.log("{{red Hello}} world");
149
-
150
- nc.group("Startup");
151
- nc.log("Loading config…");
152
- nc.group("DB");
153
- nc.log("Connecting…");
154
- nc.groupEnd(); // end "DB"
155
- nc.groupEnd(); // end "Startup"
156
- ```
157
-
158
- ---
159
-
160
- ## FS
161
-
162
- File-system helper with safe path resolution, recursive walking, copy/move
163
- helpers, JSON I/O, hashing and a small watcher. Relative paths (`./`, `../`) are
164
- resolved from the **caller file**, which is handy inside libraries.
165
-
166
- ```js
167
- // Paths
168
- const p = nc.createPath("./config/app.json"); // absolute, even if missing
169
- const folder = nc.getFolder("./src");
170
- const file = nc.getFile("package.json");
171
-
172
- // Create
173
- nc.createFolder("./dist");
174
- nc.createFile("./dist/info.txt", false, "hello");
175
-
176
- // JSON I/O
177
- nc.writeJSON("./dist/data.json", { ok: true }); // pretty, 2 spaces
178
- const data = nc.readJSON("./dist/data.json", {}); // fallback on error
179
-
180
- // Append / read
181
- nc.appendFile("./logs/app.log", "started\n");
182
- const content = nc.readFile("./README.md");
183
-
184
- // Inspect
185
- nc.exists("./dist"); // true
186
- nc.fileSize("./dist/data.json"); // bytes
187
- nc.stat("./dist/data.json"); // fs.Stats
188
- nc.hashFile("./dist/data.json"); // sha256 hex
189
-
190
- // List & delete
191
- const files = nc.getFilesIn("./src", true);
192
- nc.deleteFilesIn("./logs", false, (f) => f.endsWith(".log"));
193
- nc.emptyFolder("./tmp"); // keep the folder, clear its contents
194
- ```
195
-
196
- ### Copy & move
197
-
198
- ```js
199
- nc.copyFolder({ path: "./templates", dest: "./dist/templates", recursive: true, withFiles: true, force: true });
200
- nc.copyFilesIn({ path: "./src", dest: "./dist", recursive: true, filter: (f) => f.endsWith(".js") });
201
- nc.copyFile({ path: "./src/index.js", dest: "./dist", force: true });
202
-
203
- nc.moveFolder({ path: "./build", dest: "./dist", recursive: true, withFiles: true, force: true });
204
- nc.moveFile({ path: "./logs/app.log", dest: "./logs/archive/app.log", force: true });
205
- ```
206
-
207
- ### Watch
34
+ const config = nc.env.validate({
35
+ PORT: { type: "port", default: 3000 },
36
+ DATABASE_URL: { type: "url" },
37
+ });
208
38
 
209
- ```js
210
- const watcher = nc.watch({ path: "./src", recursive: true, filter: (event, file) => file.endsWith(".js") });
39
+ const db = new nc.SQLite("./data/app.sqlite");
40
+ const github = nc.http.create({ baseURL: "https://api.github.com/", retry: { attempts: 3 } });
211
41
 
212
- watcher
213
- .on("change", (file) => nc.log(`<% cyan changed %> ${file}`))
214
- .on("rename", (file) => nc.log(`<% yellow renamed %> ${file}`));
42
+ const { data: repo } = await github.get("repos/nodejs/node");
43
+ nc.info(`Node.js has <% yellow ${nc.num.abbreviate(repo.stargazers_count)} %> stars`);
215
44
 
216
- watcher.pause();
217
- watcher.resume();
218
- watcher.stop();
45
+ nc.sys.onShutdown(() => db.close());
219
46
  ```
220
47
 
221
- ---
222
-
223
- ## Checker
48
+ ## Install
224
49
 
225
- 35 runtime type guards. Each is a boolean function that accepts any input.
226
-
227
- ```js
228
- // Primitives & core
229
- nc.isArray([1, 2]); nc.isNumber(42); nc.isInteger(4);
230
- nc.isFloat(4.2); nc.isString("x"); nc.isBoolean(false);
231
- nc.isBigInt(10n); nc.isSymbol(Symbol()); nc.isFunction(() => {});
232
- nc.isAsyncFunction(async () => {});
233
-
234
- // Nullish & primitiveness
235
- nc.isNull(null); nc.isUndefined(undefined); nc.isNil(null); nc.isPrimitive(1);
236
-
237
- // Objects & structures
238
- nc.isObject({}); nc.isPlainObject({}); nc.isPromise(Promise.resolve());
239
- nc.isRegExp(/a/); nc.isDate(new Date()); nc.isValidDate(new Date());
240
- nc.isMap(new Map()); nc.isSet(new Set()); nc.isIterable([1]);
241
- nc.isBuffer(Buffer.from("")); nc.isTypedArray(new Uint8Array()); nc.isError(new Error());
242
-
243
- // Emptiness & formats
244
- nc.isEmpty(""); nc.isEmpty([]); nc.isEmpty({}); nc.isEmpty(new Map());
245
- nc.isEmail("john@example.com"); nc.isURL("https://x.com");
246
- nc.isUUID("123e4567-e89b-12d3-a456-426614174000");
247
- nc.isJSON('{"a":1}'); nc.isNumeric("42");
50
+ ```bash
51
+ npm install @ix-xs/node-comfort
248
52
  ```
249
53
 
250
- ---
251
-
252
- ## Utils
253
-
254
- Async & process helpers.
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 4 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/essentials/logger/) | Colorful logs, levels, JSON output, log files with rotation | `chalk`, `pino`, `winston` |
70
+ | [`fs`](https://ix-xs.github.io/node-comfort/essentials/fs/) | Files and folders without try/catch, atomic writes, glob, watch | `fs-extra`, `glob`, `rimraf` |
71
+ | [`checker`](https://ix-xs.github.io/node-comfort/essentials/checker/) | 70+ type guards and validators, `assert` | `is`, `validator` |
72
+ | [`env`](https://ix-xs.github.io/node-comfort/essentials/env/) | `.env` loading, typed and validated configuration | `dotenv`, `envalid` |
73
+ | [`schema`](https://ix-xs.github.io/node-comfort/data/schema/) | Data validation with TypeScript inference | `zod`, `yup`, `joi` |
74
+ | [`http`](https://ix-xs.github.io/node-comfort/system/http/) | `fetch` with JSON, retries, timeouts, validated responses, hooks, downloads | `axios`, `got`, `ky` |
75
+ | [`SQLite`](https://ix-xs.github.io/node-comfort/system/sqlite/) | CRUD, filters, JSON columns, pagination, transactions, migrations | `better-sqlite3` and helpers |
76
+ | [`time`](https://ix-xs.github.io/node-comfort/data/time/) | Formatting in any language and time zone, durations, date math, cron | `dayjs`, `ms`, `node-cron` |
77
+ | [`str`](https://ix-xs.github.io/node-comfort/data/str/) | Case conversion, slugs, emoji-safe truncation, fuzzy matching, plurals | `lodash`, `slugify`, `leven` |
78
+ | [`num`](https://ix-xs.github.io/node-comfort/data/num/) | Exact rounding, statistics, currencies, bytes | `numeral`, `pretty-bytes` |
79
+ | [`arr`](https://ix-xs.github.io/node-comfort/data/arr/) | Chunk, group, multi-key sort, paginate, set operations | `lodash` |
80
+ | [`obj`](https://ix-xs.github.io/node-comfort/data/obj/) | Deep clone, merge, equal and diff, typed dot paths | `lodash`, `deepmerge`, `dequal` |
81
+ | [`func`](https://ix-xs.github.io/node-comfort/flow/func/) | Debounce, throttle, memoize, retry with backoff, timeout | `p-retry`, `p-timeout` |
82
+ | [`async`](https://ix-xs.github.io/node-comfort/flow/async/) | Concurrency limits, queues, mutex, polling | `p-limit`, `p-map`, `p-queue` |
83
+ | [`Cache`](https://ix-xs.github.io/node-comfort/flow/cache/) | LRU cache with TTL, loaders and statistics | `lru-cache` |
84
+ | [`Emitter`](https://ix-xs.github.io/node-comfort/flow/emitter/) | Typed events, `waitFor`, async iteration | `mitt`, `emittery` |
85
+ | [`id`](https://ix-xs.github.io/node-comfort/data/id/) | UUID v4, v5 and v7, ULID, nanoid, ObjectId, tokens, Snowflakes | `uuid`, `ulid`, `nanoid` |
86
+ | [`crypto`](https://ix-xs.github.io/node-comfort/system/crypto/) | Passwords, encryption, JWT, TOTP two-factor codes | `bcrypt`, `jsonwebtoken`, `otplib` |
87
+ | [`cli`](https://ix-xs.github.io/node-comfort/system/cli/) | Typed arguments and subcommands, prompts, menus, spinners, progress bars, tables, trees | `commander`, `inquirer`, `ora` |
88
+ | [`color`](https://ix-xs.github.io/node-comfort/system/color/) | Chainable terminal colors, truecolor, gradients, links | `chalk`, `picocolors` |
89
+ | [`sys`](https://ix-xs.github.io/node-comfort/system/sys/) | Run commands, `which`, graceful shutdown, platform detection | `execa`, `which` |
90
+ | [`utils`](https://ix-xs.github.io/node-comfort/essentials/utils/) | `wait`, safe JSON, crash guard | |
91
+ | [`errors`](https://ix-xs.github.io/node-comfort/essentials/errors/) | 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");
255
101
 
256
- ```js
257
- // wait
258
- await nc.wait(500);
259
-
260
- // when • poll a predicate and emit events
261
- nc.when(() => Math.random() > 0.8, { label: "Lucky" }, { interval: 200, max: 3, timeout: 5000 })
262
- .on("trigger", (p) => nc.log(`<% green ${p.label} %>`))
263
- .on("timeout", () => nc.log("<% red Timeout %>"))
264
- .start();
265
-
266
- // dontCrash • process-level safety nets
267
- nc.dontCrash()
268
- .on("error", (err) => console.error("Global error:", err))
269
- .on("sig", (signal) => { nc.warn(`Caught ${signal}`); process.exit(0); });
270
-
271
- // JSON
272
- const json = nc.JSONString({ hello: "world" }); // 4-space pretty print
273
- const value = nc.JSONParse(json);
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
274
105
  ```
275
106
 
276
- ---
277
-
278
- ## str • string utilities
279
-
280
- ```js
281
- nc.str.capitalize("hELLO"); // "Hello"
282
- nc.str.titleCase("the quick fox"); // "The Quick Fox"
283
- nc.str.camelCase("foo-bar_baz"); // "fooBarBaz"
284
- nc.str.pascalCase("foo bar"); // "FooBar"
285
- nc.str.snakeCase("helloWorld"); // "hello_world"
286
- nc.str.kebabCase("helloWorld"); // "hello-world"
287
- nc.str.constantCase("helloWorld"); // "HELLO_WORLD"
288
-
289
- nc.str.slugify("Héllo, World!"); // "hello-world"
290
- nc.str.truncate("Hello world", 8); // "Hello w…"
291
- nc.str.center("hi", 6, "*"); // "**hi**"
292
- nc.str.reverse("abc"); // "cba" (emoji-safe)
293
- nc.str.squish(" a b "); // "a b"
294
- nc.str.stripTags("<b>Hi</b>"); // "Hi"
295
- nc.str.escapeHTML('<a>&"'); // "&lt;a&gt;&amp;&quot;"
296
- nc.str.escapeRegExp("a.b"); // "a\\.b"
297
- nc.str.count("banana", "a"); // 3
298
- nc.str.ensurePrefix("a.com", "https://"); // "https://a.com"
299
- nc.str.template("Hi {name} ({user.role})", { name: "Jo", user: { role: "admin" } });
300
- // "Hi Jo (admin)"
301
- nc.str.words("helloWorld-foo"); // ["hello", "World", "foo"]
302
- nc.str.length("😀"); // 1
303
- ```
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/start/performance/).
304
108
 
305
- ---
109
+ TypeScript needs no setup: the types ship with the package.
306
110
 
307
- ## num • number & math utilities
111
+ ```ts
112
+ import { schema as s, type Infer } from "@ix-xs/node-comfort";
308
113
 
309
- ```js
310
- nc.num.clamp(15, 0, 10); // 10
311
- nc.num.round(1.005, 2); // 1.01
312
- nc.num.inRange(5, 0, 10); // true
313
- nc.num.lerp(0, 100, 0.5); // 50
314
- nc.num.mapRange(5, 0, 10, 0, 100); // 50
315
- nc.num.randomInt(1, 6); // dice roll
316
-
317
- nc.num.range(4); // [0, 1, 2, 3]
318
- nc.num.range(0, 10, 2); // [0, 2, 4, 6, 8]
319
- nc.num.range(5, 0); // [5, 4, 3, 2, 1]
320
-
321
- nc.num.sum([1, 2, 3]); // 6
322
- nc.num.average([2, 4]); // 3
323
- nc.num.median([3, 1, 2]); // 2
324
- nc.num.percent(25, 200); // 12.5
325
-
326
- nc.num.formatBytes(1536); // "1.5 KB"
327
- nc.num.formatBytes(1024, { iec: true }); // "1 KiB"
328
- nc.num.abbreviate(2_400_000); // "2.4M"
329
- nc.num.thousands(1234567); // "1,234,567"
330
- nc.num.ordinal(22); // "22nd"
331
- nc.num.parse("42px"); // 42
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 }
332
116
  ```
333
117
 
334
- ---
335
-
336
- ## arr • array utilities
118
+ ## A quick tour
337
119
 
338
- All methods are pure (they never mutate their input). Many accept an
339
- `iteratee`: a property key (string) or a function.
120
+ **Logs** that look good in a terminal and turn into JSON in production:
340
121
 
341
122
  ```js
342
- nc.arr.chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
343
- nc.arr.unique([1, 1, 2]); // [1, 2]
344
- nc.arr.unique(users, "id"); // de-dupe by id
345
- nc.arr.groupBy([1, 2, 3, 4], (n) => (n % 2 ? "odd" : "even"));
346
- nc.arr.keyBy(users, "id");
347
- nc.arr.partition([1, 2, 3, 4], (n) => n % 2 === 0); // [[2, 4], [1, 3]]
348
- nc.arr.countBy(["a", "b", "a"]); // { a: 2, b: 1 }
349
-
350
- nc.arr.sortBy(users, "age");
351
- nc.arr.sortBy(users, ["age", "name"], "desc");
352
- nc.arr.sumBy(items, "price");
353
- nc.arr.maxBy(items, "score");
354
-
355
- nc.arr.difference([1, 2, 3, 4], [2, 4]); // [1, 3]
356
- nc.arr.intersection([1, 2, 3], [2, 3, 4]); // [2, 3]
357
- nc.arr.union([1, 2], [2, 3]); // [1, 2, 3]
358
- nc.arr.zip(["a", "b"], [1, 2]); // [["a", 1], ["b", 2]]
359
-
360
- nc.arr.compact([0, 1, false, 2, ""]); // [1, 2]
361
- nc.arr.flatten([1, [2, [3]]], Infinity); // [1, 2, 3]
362
- nc.arr.shuffle([1, 2, 3]); // random order
363
- nc.arr.sample([1, 2, 3]); // random element
364
- nc.arr.sampleSize([1, 2, 3, 4], 2); // 2 random elements
365
- nc.arr.first([1, 2, 3], 2); // [1, 2]
366
- nc.arr.last([1, 2, 3]); // 3
367
- nc.arr.move([1, 2, 3], 0, 2); // [2, 3, 1]
368
- nc.arr.times(3, (i) => i * 2); // [0, 2, 4]
369
- ```
370
-
371
- ---
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");
372
125
 
373
- ## obj • object utilities
374
-
375
- Pure, dot-path-aware helpers. `get`/`set`/`has` accept `"a.b[0].c"` paths.
376
-
377
- ```js
378
- nc.obj.clone({ a: { b: 1 } }); // deep clone (structuredClone + fallback)
379
- nc.obj.merge({ a: { x: 1 } }, { a: { y: 2 } }); // { a: { x: 1, y: 2 } }
380
- nc.obj.equal({ a: [1, 2] }, { a: [1, 2] }); // true (deep)
381
-
382
- nc.obj.get(data, "a.b[0].c", "default");
383
- nc.obj.set({}, "a.b.c", 1); // { a: { b: { c: 1 } } } (returns a clone)
384
- nc.obj.has({ a: { b: 1 } }, "a.b"); // true
385
-
386
- nc.obj.pick({ a: 1, b: 2, c: 3 }, ["a", "c"]); // { a: 1, c: 3 }
387
- nc.obj.omit({ a: 1, b: 2 }, ["b"]); // { a: 1 }
388
- nc.obj.filter({ a: 1, b: 2 }, (v) => v > 1); // { b: 2 }
389
- nc.obj.mapValues({ a: 1 }, (v) => v * 10); // { a: 10 }
390
- nc.obj.mapKeys({ a: 1 }, (k) => k.toUpperCase()); // { A: 1 }
391
- nc.obj.invert({ a: "x" }); // { x: "a" }
392
- nc.obj.compact({ a: 1, b: null }); // { a: 1 }
393
-
394
- nc.obj.flatten({ a: { b: { c: 1 } } }); // { "a.b.c": 1 }
395
- nc.obj.unflatten({ "a.b.c": 1 }); // { a: { b: { c: 1 } } }
396
- nc.obj.deepFreeze(config); // recursively immutable
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" } });
397
128
  ```
398
129
 
399
- ---
400
-
401
- ## func • function & control-flow utilities
130
+ **Validation** with the type inferred for you:
402
131
 
403
132
  ```js
404
- // Rate limiting
405
- const onResize = nc.func.debounce(() => render(), 200);
406
- const onScroll = nc.func.throttle(() => update(), 100);
407
- onResize.cancel(); onResize.flush();
408
-
409
- // Caching & single-call
410
- const init = nc.func.once(() => setup());
411
- const fib = nc.func.memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
412
-
413
- // Resilience
414
- const data = await nc.func.retry(() => fetchThing(), {
415
- attempts: 5, delay: 200, backoff: 2, // 200, 400, 800…
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"),
416
137
  });
417
- const res = await nc.func.timeout(fetchThing(), 5000, "too slow");
418
-
419
- // Go-style error handling • no try/catch
420
- const [err, user] = await nc.func.attempt(() => getUser(id))();
421
138
 
422
- // Composition
423
- const clean = nc.func.pipe((s) => s.trim(), (s) => s.toUpperCase());
424
- const add = nc.func.curry((a, b, c) => a + b + c);
425
- add(1)(2)(3); // 6
426
-
427
- // Interop
428
- const readFile = nc.func.promisify(require("fs").readFile);
139
+ const result = User.safeParse(req.body);
140
+ if (!result.success) return res.status(400).json(result.error.flatten());
429
141
  ```
430
142
 
431
- ---
432
-
433
- ## time • date & time utilities
434
-
435
- No heavyweight date library required.
143
+ **HTTP** on top of `fetch`, with the parts you always end up writing:
436
144
 
437
145
  ```js
438
- nc.time.parseDuration("1h30m"); // 5400000
439
- nc.time.parseDuration("2 days"); // 172800000
440
- nc.time.formatDuration(5_400_000); // "1h 30m"
441
- nc.time.formatDuration(90_000, { long: true }); // "1 minute 30 seconds"
442
-
443
- nc.time.relative(Date.now() - 3600e3); // "1 hour ago"
444
- nc.time.relative(Date.now() + 86400e3); // "in 1 day"
445
-
446
- nc.time.format(new Date(), "YYYY-MM-DD HH:mm:ss");
447
- nc.time.add(new Date(), "2h");
448
- nc.time.subtract(new Date(), 1, "d");
449
- nc.time.diff("2024-01-03", "2024-01-01", "d"); // 2
450
- nc.time.startOf(new Date(), "day");
451
- nc.time.isSameDay(a, b);
452
- nc.time.unix(); // seconds since epoch
453
-
454
- const sw = nc.time.stopwatch();
455
- doWork();
456
- sw.stop(); // 12.34 (ms, sub-ms precise)
457
- sw.stop(true); // "12.34ms"
458
- ```
459
-
460
- ### Language & time zone
461
-
462
- `relative`, `formatDuration` and `format` are **locale-aware** and built on the
463
- platform's native `Intl` (Node ships with full ICU), so **every language works
464
- out of the box** • no locale files, no dependencies. The default locale is
465
- `"en"` for deterministic output.
466
-
467
- Set a global default (chainable), or override per call:
468
-
469
- ```js
470
- // Global defaults
471
- nc.time.setLocale("fr").setTimezone("Europe/Paris");
472
- nc.time.getConfig(); // { locale: "fr", timeZone: "Europe/Paris" }
473
-
474
- // …or per call via options
475
- nc.time.relative(Date.now() - 3600e3, undefined, { locale: "fr" }); // "il y a 1 heure"
476
- nc.time.relative(Date.now() - 86400e3, undefined, { numeric: "auto" }); // "yesterday"
477
- nc.time.formatDuration(90_000, { long: true, locale: "de" }); // "1 Minute 30 Sekunden"
478
-
479
- // Localized date formatting
480
- nc.time.format(Date.now(), "dddd D MMMM YYYY", { locale: "fr" }); // "lundi 15 janvier 2024"
481
- nc.time.format(Date.now(), "h:mm A"); // "2:30 PM"
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
+ });
482
152
 
483
- // Time-zone aware (IANA names)
484
- nc.time.format(utcDate, "YYYY-MM-DD HH:mm", { timeZone: "Asia/Tokyo" });
485
- nc.time.isSameDay(a, b, { timeZone: "America/New_York" });
153
+ const { data: users } = await api.get("users", { query: { page: 2 } });
154
+ await api.post("users", { name: "Ada" });
486
155
  ```
487
156
 
488
- **`format` tokens** • numeric: `YYYY` `YY` `MM` `M` `DD` `D` `HH` `H` (24h)
489
- `hh` `h` (12h) `mm` `m` `ss` `s` `SSS`; localized: `MMMM` `MMM` (month),
490
- `dddd` `ddd` (weekday), `A` `a` (AM/PM). Wrap literal text in square brackets:
491
- `format(d, "[Updated on] dddd")`.
492
-
493
- ---
494
-
495
- ## id • id, token & hashing utilities
496
-
497
- Cryptographically secure, built on Node's native `node:crypto`.
157
+ **SQLite** without compiling anything, with objects in and objects out:
498
158
 
499
159
  ```js
500
- nc.id.uuid(); // UUID v4
501
- nc.id.ulid(); // sortable, timestamp-prefixed id
502
- nc.id.nano(); // URL-safe nanoid (21 chars)
503
- nc.id.nano(10); // custom length
504
-
505
- nc.id.token(); // 32-byte hex token
506
- nc.id.token(16, "base64url"); // URL-safe base64
507
- nc.id.token(16, "base58"); // Base58 (Bitcoin alphabet)
508
- nc.id.code(); // "K7QF9X" (unambiguous, human-friendly)
509
-
510
- nc.id.hash("hello"); // sha256 hex
511
- nc.id.hash("hello", { algorithm: "md5" });
512
- nc.id.hmac("payload", "secret"); // HMAC-SHA256
513
-
514
- nc.id.safeEqual(provided, expected); // constant-time comparison
515
- nc.id.seq("user"); // "user-1", "user-2", …
516
- ```
160
+ const db = new nc.SQLite("./data/app.sqlite");
517
161
 
518
- > **Tip:** ULIDs sort by creation time, making them a great primary-key
519
- > alternative to UUIDs. Use `nc.id.safeEqual` whenever you compare secrets,
520
- > tokens or signatures to avoid timing attacks.
521
-
522
- ---
523
-
524
- ## SQLite
525
-
526
- Thin wrapper over the built-in `node:sqlite` `DatabaseSync`, with a small model
527
- system and CRUD helpers. Identifiers are validated and values are bound with
528
- `?` placeholders. WAL mode and foreign keys are enabled automatically.
529
-
530
- ```js
531
- const db = new nc.SQLite("data/app.sqlite"); // folder created if needed
532
- // const db = new nc.SQLite(":memory:"); // transient in-memory DB
533
-
534
- await db.createTable({
535
- name: "users",
536
- columns: {
537
- id: { type: "INTEGER", primaryKey: true, autoincrement: true },
538
- name: { type: "TEXT", notNull: true },
539
- role: { type: "TEXT", values: ["admin", "user"], defaultValue: "user" },
540
- createdAt: { type: "INTEGER", notNull: true },
541
- },
542
- indexes: [{ columns: ["role"] }],
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 });
543
167
  });
544
-
545
- await db.insert("users", { name: "John", role: "admin", createdAt: Date.now() });
546
- await db.update("users", { role: "user" }, { name: "John" });
547
- await db.upsert("users", { id: 1, role: "admin" }, ["id"]);
548
-
549
- const user = await db.get("users", { name: "John" });
550
- const admins = await db.getAll("users", { role: "admin" }, { limit: 100, orderBy: "createdAt", direction: "DESC" });
551
-
552
- await db.delete("users", { role: "user" });
553
- await db.count("users", { role: "admin" });
554
- await db.clearTable("users");
555
168
  ```
556
169
 
557
- ### Constraints & indexes
170
+ **Dates** in any language and time zone:
558
171
 
559
172
  ```js
560
- await db.createTable({
561
- name: "UserAuthMethod",
562
- columns: {
563
- id: { type: "INTEGER", primaryKey: true, autoincrement: true },
564
- userId: { type: "INTEGER", notNull: true },
565
- provider: { type: "TEXT", notNull: true },
566
- providerUserId: { type: "TEXT", notNull: true },
567
- },
568
- constraints: [
569
- { type: "unique", columns: ["provider", "providerUserId"] },
570
- { type: "foreignKey", columns: ["userId"], references: { table: "users", columns: ["id"] }, onDelete: "CASCADE" },
571
- ],
572
- });
573
-
574
- await db.createIndex("UserAuthMethod", { columns: ["provider", "providerUserId"], unique: true });
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
575
177
  ```
576
178
 
577
- ### Transactions & raw SQL
179
+ **Command-line tools** with typed flags, prompts and spinners:
578
180
 
579
181
  ```js
580
- await db.transaction(async () => {
581
- await db.insert("users", { name: "A", createdAt: Date.now() });
582
- await db.insert("profiles", { userId: 1, nickname: "A" });
583
- return { ok: true }; // throw to roll back
584
- });
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
585
185
 
586
- await db.exec("CREATE INDEX IF NOT EXISTS idx_name ON users (name)");
587
- const one = await db.queryOne("SELECT * FROM users WHERE id = ?", [1]);
588
- const many = await db.queryAll("SELECT * FROM users WHERE role = ?", ["admin"]);
589
-
590
- db.close();
186
+ const env = await nc.cli.select("Environment?", ["staging", "production"]);
187
+ const spinner = nc.cli.spinner(`Deploying to ${env}`).start();
188
+ spinner.succeed("Deployed");
591
189
  ```
592
190
 
593
- ---
191
+ That's a small part of it. The [guide](https://ix-xs.github.io/node-comfort/start/introduction/) walks through every namespace, and the [recipes](https://ix-xs.github.io/node-comfort/recipes/) show them working together: a small API server, a CLI, a scheduled job.
594
192
 
595
- ## TypeScript
193
+ ## Coming from 1.x
596
194
 
597
- The package ships generated `.d.ts` files, so everything is fully typed out of
598
- the box • no `@types` package needed.
195
+ 2.0 keeps the 1.x API but changes a few behaviors. The main ones:
599
196
 
600
- ```ts
601
- import nc from "@ix-xs/node-comfort";
602
- // or: import * as nc from "@ix-xs/node-comfort";
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`).
603
201
 
604
- const id: string = nc.id.uuid();
605
- const size: string = nc.num.formatBytes(2048);
606
- ```
202
+ The [migration guide](https://ix-xs.github.io/node-comfort/start/migration/) lists every change with before and after examples.
607
203
 
608
- ---
204
+ Coming from 2.0, there is nothing to change: 2.1 adds functions, makes the existing ones faster and corrects edge cases. The [changelog](./CHANGELOG.md) has the details.
609
205
 
610
206
  ## Contributing
611
207
 
612
- Issues and PRs are welcome at
613
- [github.com/ix-xs/node-comfort](https://github.com/ix-xs/node-comfort).
614
-
615
- ```bash
616
- npm run build # regenerate the type definitions (tsc)
617
- ```
618
-
619
- ---
208
+ 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.
620
209
 
621
210
  ## License
622
211