@ix-xs/node-comfort 1.2.0 → 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 (75) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +136 -549
  3. package/index.js +80 -35
  4. package/package.json +160 -33
  5. package/src/Arr.js +887 -419
  6. package/src/Async.js +585 -0
  7. package/src/Cache.js +329 -0
  8. package/src/Checker.js +971 -539
  9. package/src/Cli.js +1034 -0
  10. package/src/Color.js +415 -0
  11. package/src/Crypto.js +707 -0
  12. package/src/Emitter.js +289 -0
  13. package/src/Env.js +555 -0
  14. package/src/FS.js +1483 -894
  15. package/src/Func.js +670 -353
  16. package/src/Http.js +538 -0
  17. package/src/Id.js +384 -201
  18. package/src/Logger.js +739 -378
  19. package/src/Num.js +844 -361
  20. package/src/Obj.js +855 -372
  21. package/src/SQLite.js +1308 -1005
  22. package/src/Schema.js +1586 -0
  23. package/src/Str.js +983 -436
  24. package/src/Sys.js +511 -0
  25. package/src/Time.js +1260 -422
  26. package/src/Utils.js +321 -325
  27. package/src/errors.js +292 -0
  28. package/types/index.d.ts +693 -526
  29. package/types/src/Arr.d.ts +455 -145
  30. package/types/src/Arr.d.ts.map +1 -1
  31. package/types/src/Async.d.ts +321 -0
  32. package/types/src/Async.d.ts.map +1 -0
  33. package/types/src/Cache.d.ts +195 -0
  34. package/types/src/Cache.d.ts.map +1 -0
  35. package/types/src/Checker.d.ts +501 -235
  36. package/types/src/Checker.d.ts.map +1 -1
  37. package/types/src/Cli.d.ts +582 -0
  38. package/types/src/Cli.d.ts.map +1 -0
  39. package/types/src/Color.d.ts +294 -0
  40. package/types/src/Color.d.ts.map +1 -0
  41. package/types/src/Crypto.d.ts +534 -0
  42. package/types/src/Crypto.d.ts.map +1 -0
  43. package/types/src/Emitter.d.ts +170 -0
  44. package/types/src/Emitter.d.ts.map +1 -0
  45. package/types/src/Env.d.ts +382 -0
  46. package/types/src/Env.d.ts.map +1 -0
  47. package/types/src/FS.d.ts +686 -238
  48. package/types/src/FS.d.ts.map +1 -1
  49. package/types/src/Func.d.ts +380 -135
  50. package/types/src/Func.d.ts.map +1 -1
  51. package/types/src/Http.d.ts +374 -0
  52. package/types/src/Http.d.ts.map +1 -0
  53. package/types/src/Id.d.ts +149 -68
  54. package/types/src/Id.d.ts.map +1 -1
  55. package/types/src/Logger.d.ts +363 -46
  56. package/types/src/Logger.d.ts.map +1 -1
  57. package/types/src/Num.d.ts +473 -137
  58. package/types/src/Num.d.ts.map +1 -1
  59. package/types/src/Obj.d.ts +390 -104
  60. package/types/src/Obj.d.ts.map +1 -1
  61. package/types/src/SQLite.d.ts +646 -309
  62. package/types/src/SQLite.d.ts.map +1 -1
  63. package/types/src/Schema.d.ts +940 -0
  64. package/types/src/Schema.d.ts.map +1 -0
  65. package/types/src/Str.d.ts +569 -166
  66. package/types/src/Str.d.ts.map +1 -1
  67. package/types/src/Sys.d.ts +269 -0
  68. package/types/src/Sys.d.ts.map +1 -0
  69. package/types/src/Time.d.ts +663 -204
  70. package/types/src/Time.d.ts.map +1 -1
  71. package/types/src/Utils.d.ts +154 -102
  72. package/types/src/Utils.d.ts.map +1 -1
  73. package/types/src/errors.d.ts +292 -0
  74. package/types/src/errors.d.ts.map +1 -0
  75. package/types/index.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,622 +1,209 @@
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/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)
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
- ```
34
+ const config = nc.env.validate({
35
+ PORT: { type: "port", default: 3000 },
36
+ DATABASE_URL: { type: "url" },
37
+ });
195
38
 
196
- ### Copy & move
39
+ const db = new nc.SQLite("./data/app.sqlite");
40
+ const github = nc.http.create({ baseURL: "https://api.github.com/", retry: { attempts: 3 } });
197
41
 
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 });
42
+ const { data: repo } = await github.get("repos/nodejs/node");
43
+ nc.info(`Node.js has <% yellow ${nc.num.abbreviate(repo.stargazers_count)} %> stars`);
202
44
 
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 });
45
+ nc.sys.onShutdown(() => db.close());
205
46
  ```
206
47
 
207
- ### Watch
208
-
209
- ```js
210
- const watcher = nc.watch({ path: "./src", recursive: true, filter: (event, file) => file.endsWith(".js") });
211
-
212
- watcher
213
- .on("change", (file) => nc.log(`<% cyan changed %> ${file}`))
214
- .on("rename", (file) => nc.log(`<% yellow renamed %> ${file}`));
215
-
216
- watcher.pause();
217
- watcher.resume();
218
- watcher.stop();
219
- ```
48
+ ## Install
220
49
 
221
- ---
222
-
223
- ## Checker
224
-
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 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");
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
- ---
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).
277
108
 
278
- ## str • string utilities
109
+ TypeScript needs no setup: the types ship with the package.
279
110
 
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
- ```
304
-
305
- ---
306
-
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
- ---
372
-
373
- ## obj • object utilities
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");
374
125
 
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
138
 
419
- // Go-style error handling • no try/catch
420
- const [err, user] = await nc.func.attempt(() => getUser(id))();
421
-
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
- ```
517
-
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
- ---
160
+ const db = new nc.SQLite("./data/app.sqlite");
523
161
 
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
- });
585
-
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"]);
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
589
185
 
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/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.
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
- ```
607
-
608
- ---
202
+ The [migration guide](https://ix-xs.github.io/node-comfort/guide/migration.html) lists every change with before and after examples.
609
203
 
610
204
  ## Contributing
611
205
 
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
- ---
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.
620
207
 
621
208
  ## License
622
209