@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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Logger.js
CHANGED
|
@@ -1,378 +1,963 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
if (typeof
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A console logger that works with zero setup: levels, colors, a small
|
|
5
|
+
* markup for styling, groups, timers and tables. In production, switch to
|
|
6
|
+
* one JSON object per line with `LOG_FORMAT=json`, and write to rotating
|
|
7
|
+
* files if you need to.
|
|
8
|
+
*
|
|
9
|
+
* Markup: `<% red bold Error:%> disk is full`. Any style name works, plus
|
|
10
|
+
* `#ff8800`, `bg#1e1e2e`, `rgb(255, 128, 0)` and `bgRgb(...)`.
|
|
11
|
+
*
|
|
12
|
+
* Levels, from chatty to severe: `trace`, `debug`, `info`, `warn`, `error`,
|
|
13
|
+
* `fatal`. The default is `info`; `LOG_LEVEL` changes it. `DEBUG=1` shows
|
|
14
|
+
* debug messages, and `DEBUG=db,http:*` only the debug and trace messages of
|
|
15
|
+
* the loggers with these scopes. Warnings and errors go to stderr.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* nc.info("Server listening on port 3000");
|
|
19
|
+
* nc.warn("Cache is almost full");
|
|
20
|
+
* nc.error(new Error("Payment failed", { cause: err }));
|
|
21
|
+
* nc.log("<% cyan bold Tip:%> press <% bgWhite black q %> to quit");
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require("node:fs");
|
|
25
|
+
const path = require("node:path");
|
|
26
|
+
const util = require("node:util");
|
|
27
|
+
const color = require("./Color.js");
|
|
28
|
+
const { timeConfig } = require("./internal/shared.js");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Log levels, from the most verbose to the most severe. `silent` hides everything.
|
|
32
|
+
* @typedef {"trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent"} LogLevel
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where logs go: `process.stdout`, a file stream, or anything with a
|
|
37
|
+
* `write(text)` method.
|
|
38
|
+
* @typedef {{ write(text: string): unknown }} LogStream
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Writing logs to a file.
|
|
43
|
+
* @typedef {object} LogFileOptions
|
|
44
|
+
* @property {string} path The file to append to. Folders are created, colors removed.
|
|
45
|
+
* @property {string | number} [maxSize] Rotate past this size, like `"10MB"`.
|
|
46
|
+
* @property {number} [maxFiles] How many rotated files to keep. Defaults to `5`.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A message as `onLog` receives it.
|
|
51
|
+
* @typedef {object} LogEntry
|
|
52
|
+
* @property {Date} time
|
|
53
|
+
* @property {LogLevel} level
|
|
54
|
+
* @property {"log" | "trace" | "debug" | "info" | "success" | "warn" | "error" | "fatal"} kind The method that was called.
|
|
55
|
+
* @property {string | undefined} scope
|
|
56
|
+
* @property {string} message The line as written, without colors or timestamp.
|
|
57
|
+
* @property {unknown[]} args What was passed to the method, untouched.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Options for `createLogger()` and `configure()`.
|
|
62
|
+
* @typedef {object} LoggerOptions
|
|
63
|
+
* @property {LogLevel} [level] Hide messages below this level. Defaults to `LOG_LEVEL`, else `"info"`.
|
|
64
|
+
* @property {string} [scope] Shown before each message, like `[db]`. Child loggers extend it (`db:pool`).
|
|
65
|
+
* @property {boolean | string} [timestamp] `true` shows `[HH:mm:ss]`, `false` hides it, a string is a `time.format` pattern. Defaults to `true`.
|
|
66
|
+
* @property {"pretty" | "json"} [format] `"json"` writes one object per line, for log collectors. Defaults to `LOG_FORMAT`, else `"pretty"`.
|
|
67
|
+
* @property {{ open?: string, close?: string }} [delimiter] Markup delimiters. Defaults to `<%` and `%>`.
|
|
68
|
+
* @property {boolean} [colors] Force colors on or off. By default, it depends on the terminal.
|
|
69
|
+
* @property {LogStream} [stdout] Where `trace` to `info` go. Defaults to `process.stdout`.
|
|
70
|
+
* @property {LogStream} [stderr] Where `warn`, `error` and `fatal` go. Defaults to `process.stderr`.
|
|
71
|
+
* @property {string | LogFileOptions} [file] Also write every message to a file.
|
|
72
|
+
* @property {Record<string, unknown>} [fields] Added to every JSON record, like a service name.
|
|
73
|
+
* @property {string[]} [redact] Keys whose values are replaced by `"[redacted]"` wherever they appear in logged objects, like `["password", "authorization"]`. Case doesn't matter.
|
|
74
|
+
* @property {(entry: LogEntry) => void | Promise<void>} [onLog] Called for every message that passes the level, to send logs somewhere else. It isn't awaited, and an error it throws (or a promise it rejects) is ignored: logging never breaks the program.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A logger from `createLogger()`. Every method returns the logger, so calls chain.
|
|
79
|
+
*
|
|
80
|
+
* @typedef {object} Logger
|
|
81
|
+
* @property {(...args: unknown[]) => Logger} log A plain message, no badge. Supports markup.
|
|
82
|
+
* @property {(...args: unknown[]) => Logger} trace Very detailed diagnostics, hidden unless the level is `trace`.
|
|
83
|
+
* @property {(...args: unknown[]) => Logger} debug Diagnostics, hidden unless the level is `debug` or lower.
|
|
84
|
+
* @property {(...args: unknown[]) => Logger} info An `ℹ INFO` message.
|
|
85
|
+
* @property {(...args: unknown[]) => Logger} success A `✔ OK` message.
|
|
86
|
+
* @property {(...args: unknown[]) => Logger} warn A `⚠ WARN` message, on stderr.
|
|
87
|
+
* @property {(...args: unknown[]) => Logger} error An `✖ ERROR` message, on stderr. Errors show their stack and cause.
|
|
88
|
+
* @property {(...args: unknown[]) => Logger} fatal An `✖ FATAL` message, on stderr.
|
|
89
|
+
* @property {(label?: string) => Logger} group Prints a label and indents what follows. Groups nest.
|
|
90
|
+
* @property {() => Logger} groupEnd Closes the current group.
|
|
91
|
+
* @property {(label?: string) => Logger} timeStart Starts a named timer.
|
|
92
|
+
* @property {(label?: string) => Logger} timeEnd Prints how long since `timeStart(label)`.
|
|
93
|
+
* @property {(rows: Array<Record<string, unknown>> | unknown[][], options?: import("./Cli").TableOptions) => Logger} table Prints rows as a table.
|
|
94
|
+
* @property {(text: string, options?: import("./Cli").BoxOptions) => Logger} box Prints text in a box.
|
|
95
|
+
* @property {(title?: string) => Logger} divider Prints a horizontal line.
|
|
96
|
+
* @property {(scope: string, options?: LoggerOptions) => Logger} child A logger with the same settings and a nested scope. Its `fields` are added to the parent's.
|
|
97
|
+
* @property {(level: LogLevel) => Logger} setLevel Changes the minimum level.
|
|
98
|
+
* @property {() => LogLevel} getLevel The current minimum level.
|
|
99
|
+
* @property {(level: LogLevel) => boolean} isLevelEnabled Would this level be printed? Skip expensive debug output when it wouldn't.
|
|
100
|
+
* @property {(options: LoggerOptions) => Logger} configure Changes several settings at once.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
/** @type {Record<LogLevel, number>} */
|
|
104
|
+
const LEVELS = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, silent: Infinity };
|
|
105
|
+
|
|
106
|
+
/** @type {Record<string, { label: string, style: string, stream: "stdout" | "stderr", level: LogLevel }>} */
|
|
107
|
+
const BADGES = {
|
|
108
|
+
log: { label: "", style: "", stream: "stdout", level: "info" },
|
|
109
|
+
trace: { label: "… TRACE", style: "gray dim", stream: "stdout", level: "trace" },
|
|
110
|
+
debug: { label: "● DEBUG", style: "gray bold", stream: "stdout", level: "debug" },
|
|
111
|
+
info: { label: "ℹ INFO", style: "cyan bold", stream: "stdout", level: "info" },
|
|
112
|
+
success: { label: "✔ OK", style: "greenBright bold", stream: "stdout", level: "info" },
|
|
113
|
+
warn: { label: "⚠ WARN", style: "yellowBright bold", stream: "stderr", level: "warn" },
|
|
114
|
+
error: { label: "✖ ERROR", style: "redBright bold", stream: "stderr", level: "error" },
|
|
115
|
+
fatal: { label: "✖ FATAL", style: "bgRed whiteBright bold", stream: "stderr", level: "fatal" },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const STYLE_NAMES = new Set([
|
|
119
|
+
"reset", "bold", "dim", "italic", "underline", "overline", "inverse", "hidden", "strikethrough",
|
|
120
|
+
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray", "grey",
|
|
121
|
+
"blackBright", "redBright", "greenBright", "yellowBright", "blueBright", "magentaBright", "cyanBright", "whiteBright",
|
|
122
|
+
"bgBlack", "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", "bgCyan", "bgWhite", "bgGray", "bgGrey",
|
|
123
|
+
"bgBlackBright", "bgRedBright", "bgGreenBright", "bgYellowBright", "bgBlueBright", "bgMagentaBright", "bgCyanBright", "bgWhiteBright",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const REDACTED = "[redacted]";
|
|
127
|
+
const TRIM_LINES = /^(\s*\r?\n)+|(\r?\n\s*)+$/g;
|
|
128
|
+
const RGB = /(bg)?rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi;
|
|
129
|
+
const TOKEN = /^(\S+)(\s+|$)/;
|
|
130
|
+
|
|
131
|
+
/** @returns {LogLevel} */
|
|
132
|
+
const _defaultLevel = () => {
|
|
133
|
+
const fromEnv = String(process.env.LOG_LEVEL ?? "").toLowerCase();
|
|
134
|
+
if (Object.hasOwn(LEVELS, fromEnv)) return /** @type {LogLevel} */ (fromEnv);
|
|
135
|
+
return _debugAll(process.env.DEBUG) || process.env.NODE_DEBUG ? "debug" : "info";
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Does `DEBUG` ask for everything? `DEBUG=1`, `DEBUG=true` and `DEBUG=*` do.
|
|
140
|
+
* @param {string | undefined} value
|
|
141
|
+
*/
|
|
142
|
+
const _debugAll = (value) => value !== undefined && /^(?:1|true|yes|on|\*)$/i.test(value.trim());
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Does `DEBUG` name this scope? It's a list of scopes, where `*` stands for
|
|
146
|
+
* anything and a leading `-` excludes: `DEBUG=db,http:*,-http:static`.
|
|
147
|
+
* @param {string | undefined} value
|
|
148
|
+
* @param {string | undefined} scope
|
|
149
|
+
*/
|
|
150
|
+
const _debugScope = (value, scope) => {
|
|
151
|
+
if (!value || !scope || _debugAll(value)) return false;
|
|
152
|
+
let wanted = false;
|
|
153
|
+
for (const part of value.split(/[\s,]+/)) {
|
|
154
|
+
if (!part) continue;
|
|
155
|
+
const exclude = part[0] === "-";
|
|
156
|
+
const pattern = new RegExp(`^${(exclude ? part.slice(1) : part).split("*").map(_escape).join(".*")}$`);
|
|
157
|
+
if (!pattern.test(scope)) continue;
|
|
158
|
+
if (exclude) return false;
|
|
159
|
+
wanted = true;
|
|
160
|
+
}
|
|
161
|
+
return wanted;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** @param {string} value */
|
|
165
|
+
const _escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {import("./Color").ColorModule} palette
|
|
169
|
+
* @param {string[]} tokens
|
|
170
|
+
* @param {string} text
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
const _style = (palette, tokens, text) => {
|
|
174
|
+
/** @type {any} */
|
|
175
|
+
let chain = palette;
|
|
176
|
+
for (const token of tokens) {
|
|
177
|
+
const name = token === "normal" ? "reset" : token;
|
|
178
|
+
let m;
|
|
179
|
+
if (STYLE_NAMES.has(name)) chain = chain[name];
|
|
180
|
+
else if ((m = /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i.exec(name))) chain = chain.rgb(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
181
|
+
else if ((m = /^bgrgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/i.exec(name))) chain = chain.bgRgb(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
182
|
+
else if (/^#[0-9a-f]{3,8}$/i.test(name)) chain = chain.hex(name);
|
|
183
|
+
else if (/^bg#[0-9a-f]{3,8}$/i.test(name)) chain = chain.bgHex(name.slice(2));
|
|
184
|
+
}
|
|
185
|
+
return chain === palette ? text : chain(text);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {string} token
|
|
190
|
+
*/
|
|
191
|
+
const _isStyleToken = (token) =>
|
|
192
|
+
STYLE_NAMES.has(token) ||
|
|
193
|
+
token === "normal" ||
|
|
194
|
+
/^(bg)?rgb\(\d{1,3},\d{1,3},\d{1,3}\)$/i.test(token) ||
|
|
195
|
+
/^(bg)?#[0-9a-f]{3,8}$/i.test(token);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Renders `<% styles text %>` markup.
|
|
199
|
+
* @param {string} input
|
|
200
|
+
* @param {{ open: string, close: string, regex: RegExp }} delimiter
|
|
201
|
+
* @param {import("./Color").ColorModule} palette
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
const _markup = (input, delimiter, palette) => {
|
|
205
|
+
if (!input.includes(delimiter.open)) return input;
|
|
206
|
+
return input.replace(delimiter.regex, (_, content) => {
|
|
207
|
+
const normalized = String(content).replace(RGB, (m0, bg, r, g, b) => `${bg ?? ""}rgb(${r},${g},${b})`);
|
|
208
|
+
/** @type {string[]} */
|
|
209
|
+
const tokens = [];
|
|
210
|
+
let rest = normalized.replace(/^\s+/, "");
|
|
211
|
+
for (;;) {
|
|
212
|
+
const match = TOKEN.exec(rest);
|
|
213
|
+
if (!match || !_isStyleToken(match[1])) break;
|
|
214
|
+
tokens.push(match[1]);
|
|
215
|
+
rest = rest.slice(match[0].length);
|
|
216
|
+
}
|
|
217
|
+
return _style(palette, tokens, rest);
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A copy of a value with the secret keys masked, for values `JSON.stringify`
|
|
223
|
+
* can't handle on its own: class instances, circular references.
|
|
224
|
+
* @param {unknown} value
|
|
225
|
+
* @param {Set<string>} secrets Lower-cased key names.
|
|
226
|
+
* @param {Map<object, unknown>} seen
|
|
227
|
+
* @returns {unknown}
|
|
228
|
+
*/
|
|
229
|
+
const _redact = (value, secrets, seen) => {
|
|
230
|
+
if (value === null || typeof value !== "object" || value instanceof Date || value instanceof RegExp || value instanceof Error || ArrayBuffer.isView(value)) return value;
|
|
231
|
+
const known = seen.get(value);
|
|
232
|
+
if (known !== undefined) return known;
|
|
233
|
+
if (Array.isArray(value)) {
|
|
234
|
+
/** @type {unknown[]} */
|
|
235
|
+
const list = [];
|
|
236
|
+
seen.set(value, list);
|
|
237
|
+
for (const item of value) list.push(_redact(item, secrets, seen));
|
|
238
|
+
return list;
|
|
239
|
+
}
|
|
240
|
+
if (value instanceof Map || value instanceof Set) return value;
|
|
241
|
+
/** @type {Record<string, unknown>} */
|
|
242
|
+
const copy = Object.create(Object.getPrototypeOf(value));
|
|
243
|
+
seen.set(value, copy);
|
|
244
|
+
for (const key of Object.keys(value)) {
|
|
245
|
+
copy[key] = secrets.has(key.toLowerCase()) ? REDACTED : _redact(/** @type {Record<string, unknown>} */ (value)[key], secrets, seen);
|
|
246
|
+
}
|
|
247
|
+
return copy;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {unknown} value
|
|
252
|
+
* @param {boolean} colors
|
|
253
|
+
* @param {Set<string> | undefined} secrets
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
const _stringify = (value, colors, secrets) => {
|
|
257
|
+
if (typeof value === "string") return value;
|
|
258
|
+
if (value instanceof Error) {
|
|
259
|
+
let text = value.stack ?? `${value.name}: ${value.message}`;
|
|
260
|
+
let cause = /** @type {any} */ (value).cause;
|
|
261
|
+
let depth = 0;
|
|
262
|
+
while (cause !== undefined && depth++ < 5) {
|
|
263
|
+
text += `\nCaused by: ${cause instanceof Error ? cause.stack ?? cause.message : util.inspect(cause, { colors })}`;
|
|
264
|
+
cause = cause instanceof Error ? /** @type {any} */ (cause).cause : undefined;
|
|
265
|
+
}
|
|
266
|
+
return text;
|
|
267
|
+
}
|
|
268
|
+
if (value !== null && typeof value === "object") {
|
|
269
|
+
const proto = Object.getPrototypeOf(value);
|
|
270
|
+
if (Array.isArray(value) || proto === Object.prototype || proto === null) {
|
|
271
|
+
try {
|
|
272
|
+
const json = JSON.stringify(value, (key, v) => (secrets?.has(key.toLowerCase()) ? REDACTED : typeof v === "bigint" ? `${v}n` : v), 2);
|
|
273
|
+
if (json !== undefined) return json;
|
|
274
|
+
} catch {
|
|
275
|
+
// circular or exotic: fall back to util.inspect
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return util.inspect(secrets ? _redact(value, secrets, new Map()) : value, { depth: 6, colors, breakLength: 100 });
|
|
279
|
+
}
|
|
280
|
+
return typeof value === "symbol" || typeof value === "function" ? util.inspect(value, { colors }) : String(value);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @param {string} level
|
|
285
|
+
* @param {unknown[]} args
|
|
286
|
+
* @param {string | undefined} scope
|
|
287
|
+
* @param {Record<string, unknown>} fields
|
|
288
|
+
* @param {Set<string> | undefined} secrets
|
|
289
|
+
* @param {Date} now
|
|
290
|
+
* @returns {string}
|
|
291
|
+
*/
|
|
292
|
+
const _json = (level, args, scope, fields, secrets, now) => {
|
|
293
|
+
/** @type {Record<string, unknown>} */
|
|
294
|
+
const record = { time: now.toISOString(), level, ...fields };
|
|
295
|
+
if (scope) record.scope = scope;
|
|
296
|
+
const messages = [];
|
|
297
|
+
for (const arg of args) {
|
|
298
|
+
if (arg instanceof Error) {
|
|
299
|
+
record.err = { name: arg.name, message: arg.message, stack: arg.stack, code: /** @type {any} */ (arg).code, cause: /** @type {any} */ (arg).cause instanceof Error ? /** @type {any} */ (arg).cause.message : /** @type {any} */ (arg).cause };
|
|
300
|
+
} else if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) {
|
|
301
|
+
Object.assign(record, arg);
|
|
302
|
+
} else {
|
|
303
|
+
messages.push(typeof arg === "string" ? color.strip(arg) : _stringify(arg, false, secrets));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
record.msg = messages.join(" ");
|
|
307
|
+
try {
|
|
308
|
+
return JSON.stringify(record, (key, v) => (secrets?.has(key.toLowerCase()) ? REDACTED : typeof v === "bigint" ? v.toString() : v));
|
|
309
|
+
} catch {
|
|
310
|
+
return JSON.stringify({ time: record.time, level, msg: `${record.msg} [unserializable fields]` });
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* A log file: where it is, how big it may get, how big it is now.
|
|
316
|
+
* @typedef {{ target: string, limit: number | undefined, keep: number, size: number, ready: boolean }} _LogFile
|
|
317
|
+
*/
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param {LogFileOptions} file
|
|
321
|
+
* @returns {_LogFile}
|
|
322
|
+
*/
|
|
323
|
+
const _openFile = (file) => ({
|
|
324
|
+
target: path.resolve(file.path),
|
|
325
|
+
limit: file.maxSize === undefined ? undefined : typeof file.maxSize === "number" ? file.maxSize : require("./Num.js").parseBytes(file.maxSize),
|
|
326
|
+
keep: Math.max(1, file.maxFiles ?? 5),
|
|
327
|
+
size: 0,
|
|
328
|
+
ready: false,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/** @param {string} target */
|
|
332
|
+
const _sizeOf = (target) => {
|
|
333
|
+
try {
|
|
334
|
+
return fs.statSync(target).size;
|
|
335
|
+
} catch {
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Appends a line, rotating the file when it gets too big. The folder is
|
|
342
|
+
* made once and the size is counted here, so a line costs one write.
|
|
343
|
+
* @param {_LogFile} file
|
|
344
|
+
* @param {string} line
|
|
345
|
+
*/
|
|
346
|
+
const _writeFile = (file, line) => {
|
|
347
|
+
const text = `${line}\n`;
|
|
348
|
+
try {
|
|
349
|
+
if (!file.ready) {
|
|
350
|
+
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
351
|
+
file.size = _sizeOf(file.target);
|
|
352
|
+
file.ready = true;
|
|
353
|
+
}
|
|
354
|
+
const bytes = Buffer.byteLength(text);
|
|
355
|
+
if (file.limit !== undefined && file.size > 0 && file.size + bytes - 1 > file.limit) {
|
|
356
|
+
// Another process may have rotated or removed the file: look before moving anything.
|
|
357
|
+
file.size = _sizeOf(file.target);
|
|
358
|
+
if (file.size > 0 && file.size + bytes - 1 > file.limit) {
|
|
359
|
+
for (let i = file.keep - 1; i >= 1; i--) {
|
|
360
|
+
if (fs.existsSync(`${file.target}.${i}`)) fs.renameSync(`${file.target}.${i}`, `${file.target}.${i + 1}`);
|
|
361
|
+
}
|
|
362
|
+
if (fs.existsSync(`${file.target}.${file.keep + 1}`)) fs.rmSync(`${file.target}.${file.keep + 1}`);
|
|
363
|
+
fs.renameSync(file.target, `${file.target}.1`);
|
|
364
|
+
file.size = 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
fs.appendFileSync(file.target, text);
|
|
369
|
+
} catch {
|
|
370
|
+
// The folder was removed since: make it again, once.
|
|
371
|
+
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
372
|
+
fs.appendFileSync(file.target, text);
|
|
373
|
+
file.size = 0;
|
|
374
|
+
}
|
|
375
|
+
file.size += bytes;
|
|
376
|
+
} catch {
|
|
377
|
+
// Logging must never crash the application.
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/** @param {number} n */
|
|
382
|
+
const _two = (n) => (n < 10 ? `0${n}` : String(n));
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* What a message looks like on one output: its colors, and the parts that
|
|
386
|
+
* are the same on every line, styled once.
|
|
387
|
+
* @typedef {{ palette: import("./Color").ColorModule, colors: boolean, labels: Record<string, string>, scope: string, stamp: string, stampAt: number, stampWidth: number }} _Face
|
|
388
|
+
*/
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {LoggerOptions} options
|
|
392
|
+
* @returns {Logger}
|
|
393
|
+
*/
|
|
394
|
+
const _create = (options) => {
|
|
395
|
+
const settings = {
|
|
396
|
+
level: options.level ?? _defaultLevel(),
|
|
397
|
+
scope: options.scope,
|
|
398
|
+
timestamp: options.timestamp ?? true,
|
|
399
|
+
format: options.format ?? (process.env.LOG_FORMAT === "json" ? "json" : "pretty"),
|
|
400
|
+
delimiter: { open: "<%", close: "%>", ...options.delimiter },
|
|
401
|
+
colors: options.colors,
|
|
402
|
+
stdout: options.stdout,
|
|
403
|
+
stderr: options.stderr,
|
|
404
|
+
file: typeof options.file === "string" ? { path: options.file } : options.file,
|
|
405
|
+
fields: options.fields ?? {},
|
|
406
|
+
redact: options.redact,
|
|
407
|
+
onLog: options.onLog,
|
|
408
|
+
};
|
|
409
|
+
/** @type {string[]} */
|
|
410
|
+
const groups = [];
|
|
411
|
+
/** @type {Map<string, bigint>} */
|
|
412
|
+
const timers = new Map();
|
|
413
|
+
|
|
414
|
+
// Everything below is worked out from the settings, and again when they change.
|
|
415
|
+
let threshold = 0;
|
|
416
|
+
let debugScope = false;
|
|
417
|
+
// What each method does with a message, decided once: a call that prints
|
|
418
|
+
// nothing must cost next to nothing.
|
|
419
|
+
/** @type {Record<keyof typeof BADGES, boolean>} */
|
|
420
|
+
const on = { log: true, trace: false, debug: false, info: true, success: true, warn: true, error: true, fatal: true };
|
|
421
|
+
/** @type {{ open: string, close: string, regex: RegExp }} */
|
|
422
|
+
let delimiter;
|
|
423
|
+
/** @type {Set<string> | undefined} */
|
|
424
|
+
let secrets;
|
|
425
|
+
/** @type {_LogFile | undefined} */
|
|
426
|
+
let file;
|
|
427
|
+
/** @type {{ stdout?: _Face, stderr?: _Face, file?: _Face }} */
|
|
428
|
+
let faces = {};
|
|
429
|
+
|
|
430
|
+
const gate = () => {
|
|
431
|
+
threshold = LEVELS[settings.level];
|
|
432
|
+
for (const kind of /** @type {Array<keyof typeof BADGES>} */ (Object.keys(BADGES))) {
|
|
433
|
+
on[kind] = LEVELS[BADGES[kind].level] >= threshold || (debugScope && threshold !== Infinity && (kind === "debug" || kind === "trace"));
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const refresh = () => {
|
|
438
|
+
// DEBUG=db,http:* turns debug on for those scopes only, whatever the level.
|
|
439
|
+
debugScope = _debugScope(process.env.DEBUG, settings.scope);
|
|
440
|
+
gate();
|
|
441
|
+
const { open, close } = settings.delimiter;
|
|
442
|
+
delimiter = { open, close, regex: new RegExp(`${_escape(open)}([\\s\\S]*?)${_escape(close)}`, "g") };
|
|
443
|
+
secrets = settings.redact?.length ? new Set(settings.redact.map((key) => String(key).toLowerCase())) : undefined;
|
|
444
|
+
file = settings.file ? _openFile(settings.file) : undefined;
|
|
445
|
+
faces = {};
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
/** @param {"stdout" | "stderr"} which */
|
|
449
|
+
const streamOf = (which) => (which === "stderr" ? settings.stderr ?? process.stderr : settings.stdout ?? process.stdout);
|
|
450
|
+
|
|
451
|
+
/** @param {"stdout" | "stderr" | "file"} which */
|
|
452
|
+
const paletteOf = (which) => {
|
|
453
|
+
if (which === "file" || settings.colors === false) return color.create(0);
|
|
454
|
+
if (settings.colors === true) return color.create(color.level > 0 ? color.level : 3);
|
|
455
|
+
const stream = streamOf(which);
|
|
456
|
+
return stream === process.stdout || stream === process.stderr ? color.create(color.detect(/** @type {any} */ (stream))) : color.create(0);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* @param {"stdout" | "stderr" | "file"} which
|
|
461
|
+
* @returns {_Face}
|
|
462
|
+
*/
|
|
463
|
+
const faceOf = (which) => {
|
|
464
|
+
let face = faces[which];
|
|
465
|
+
if (face) return face;
|
|
466
|
+
const palette = paletteOf(which);
|
|
467
|
+
face = { palette, colors: palette.level > 0, labels: {}, scope: settings.scope ? palette.magenta(`[${settings.scope}]`) : "", stamp: "", stampAt: -1, stampWidth: 0 };
|
|
468
|
+
for (const kind of Object.keys(BADGES)) face.labels[kind] = BADGES[kind].label ? _style(palette, BADGES[kind].style.split(" "), BADGES[kind].label) : "";
|
|
469
|
+
faces[which] = face;
|
|
470
|
+
return face;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The timestamp, written again only when the second changes.
|
|
475
|
+
* @param {_Face} face
|
|
476
|
+
* @param {Date} now
|
|
477
|
+
*/
|
|
478
|
+
const stampOf = (face, now) => {
|
|
479
|
+
const second = Math.floor(now.getTime() / 1000);
|
|
480
|
+
if (second === face.stampAt) return;
|
|
481
|
+
face.stampAt = second;
|
|
482
|
+
// A pattern, or a time zone set with time.setTimezone(), needs the real formatter.
|
|
483
|
+
const text = typeof settings.timestamp === "string" || timeConfig.timeZone !== undefined
|
|
484
|
+
? require("./Time.js").format(now, typeof settings.timestamp === "string" ? settings.timestamp : "HH:mm:ss")
|
|
485
|
+
: `${_two(now.getHours())}:${_two(now.getMinutes())}:${_two(now.getSeconds())}`;
|
|
486
|
+
face.stamp = `${face.palette.gray(`[${text}]`)} `;
|
|
487
|
+
face.stampWidth = color.width(text) + 3;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* @param {_Face} face
|
|
492
|
+
* @param {keyof typeof BADGES} kind
|
|
493
|
+
* @param {unknown[]} args
|
|
494
|
+
* @param {Date} now
|
|
495
|
+
* @param {boolean} stamped
|
|
496
|
+
* @returns {string}
|
|
497
|
+
*/
|
|
498
|
+
const render = (face, kind, args, now, stamped) => {
|
|
499
|
+
const { palette } = face;
|
|
500
|
+
let body = "";
|
|
501
|
+
for (let i = 0; i < args.length; i++) {
|
|
502
|
+
const text = _markup(_stringify(args[i], face.colors, secrets), delimiter, palette);
|
|
503
|
+
body += (i ? " " : "") + (face.colors && (kind === "debug" || kind === "trace") ? palette.gray(text) : text);
|
|
504
|
+
}
|
|
505
|
+
let prefix = "";
|
|
506
|
+
let padWidth = 0;
|
|
507
|
+
if (stamped) {
|
|
508
|
+
stampOf(face, now);
|
|
509
|
+
prefix = face.stamp;
|
|
510
|
+
padWidth = face.stampWidth;
|
|
511
|
+
}
|
|
512
|
+
if (groups.length) {
|
|
513
|
+
prefix += " ".repeat(groups.length);
|
|
514
|
+
padWidth += groups.length * 2;
|
|
515
|
+
}
|
|
516
|
+
if (face.scope) prefix += `${face.scope} `;
|
|
517
|
+
if (face.labels[kind]) prefix += `${face.labels[kind]} `;
|
|
518
|
+
if (body.indexOf("\n") === -1) return (prefix + body).trimEnd();
|
|
519
|
+
const lines = body.replace(TRIM_LINES, "").split(/\r?\n/);
|
|
520
|
+
const pad = " ".repeat(padWidth);
|
|
521
|
+
let out = (prefix + lines[0]).trimEnd();
|
|
522
|
+
for (let i = 1; i < lines.length; i++) out += `\n${pad}${lines[i]}`;
|
|
523
|
+
return out;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @param {keyof typeof BADGES} kind
|
|
528
|
+
* @param {unknown[]} args
|
|
529
|
+
*/
|
|
530
|
+
const emit = (kind, args) => {
|
|
531
|
+
if (!on[kind]) return;
|
|
532
|
+
const badge = BADGES[kind];
|
|
533
|
+
const now = new Date();
|
|
534
|
+
const stream = streamOf(badge.stream);
|
|
535
|
+
if (settings.format === "json") {
|
|
536
|
+
const line = _json(kind === "log" ? "info" : kind, args, settings.scope, settings.fields, secrets, now);
|
|
537
|
+
stream.write(`${line}\n`);
|
|
538
|
+
if (file) _writeFile(file, line);
|
|
539
|
+
} else {
|
|
540
|
+
const face = faceOf(badge.stream);
|
|
541
|
+
const line = render(face, kind, args, now, Boolean(settings.timestamp));
|
|
542
|
+
stream.write(`${line}\n`);
|
|
543
|
+
// The file gets the same line without colors: the one just made, when it has none.
|
|
544
|
+
if (file) _writeFile(file, face.colors ? render(faceOf("file"), kind, args, now, Boolean(settings.timestamp)) : line);
|
|
545
|
+
}
|
|
546
|
+
if (settings.onLog) {
|
|
547
|
+
try {
|
|
548
|
+
/** @type {unknown} */
|
|
549
|
+
const pending = settings.onLog({ time: now, level: badge.level, kind: /** @type {LogEntry["kind"]} */ (kind), scope: settings.scope, message: render(faceOf("file"), kind, args, now, false), args });
|
|
550
|
+
// An async hook that fails would otherwise be an unhandled rejection.
|
|
551
|
+
if (pending && typeof (/** @type {any} */ (pending).then) === "function") /** @type {PromiseLike<unknown>} */ (pending).then(undefined, () => {});
|
|
552
|
+
} catch {
|
|
553
|
+
// A broken hook must not take the program down with it.
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
refresh();
|
|
559
|
+
|
|
560
|
+
/** @type {Logger} */
|
|
561
|
+
const logger = {
|
|
562
|
+
log: (...args) => (on.log && emit("log", args), logger),
|
|
563
|
+
trace: (...args) => (on.trace && emit("trace", args), logger),
|
|
564
|
+
debug: (...args) => (on.debug && emit("debug", args), logger),
|
|
565
|
+
info: (...args) => (on.info && emit("info", args), logger),
|
|
566
|
+
success: (...args) => (on.success && emit("success", args), logger),
|
|
567
|
+
warn: (...args) => (on.warn && emit("warn", args), logger),
|
|
568
|
+
error: (...args) => (on.error && emit("error", args), logger),
|
|
569
|
+
fatal: (...args) => (on.fatal && emit("fatal", args), logger),
|
|
570
|
+
group: (label = "") => {
|
|
571
|
+
emit("log", [`▼ ${label}`.trimEnd()]);
|
|
572
|
+
groups.push(label);
|
|
573
|
+
return logger;
|
|
574
|
+
},
|
|
575
|
+
groupEnd: () => {
|
|
576
|
+
groups.pop();
|
|
577
|
+
return logger;
|
|
578
|
+
},
|
|
579
|
+
timeStart: (label = "default") => {
|
|
580
|
+
timers.set(label, process.hrtime.bigint());
|
|
581
|
+
return logger;
|
|
582
|
+
},
|
|
583
|
+
timeEnd: (label = "default") => {
|
|
584
|
+
const start = timers.get(label);
|
|
585
|
+
if (start === undefined) return logger.warn(`Timer "${label}" does not exist`);
|
|
586
|
+
timers.delete(label);
|
|
587
|
+
const ms = Number(process.hrtime.bigint() - start) / 1e6;
|
|
588
|
+
return logger.log(`${label}: ${ms < 1000 ? `${Math.round(ms * 100) / 100}ms` : require("./Time.js").formatDuration(ms, { units: 2 })}`);
|
|
589
|
+
},
|
|
590
|
+
table: (rows, tableOptions) => logger.log(require("./Cli.js").table(rows, { ...tableOptions, colors: faceOf("stdout").colors })),
|
|
591
|
+
box: (text, boxOptions) => logger.log(require("./Cli.js").box(text, { ...boxOptions, colors: faceOf("stdout").colors })),
|
|
592
|
+
divider: (title) => {
|
|
593
|
+
const columns = /** @type {any} */ (streamOf("stdout")).columns ?? 80;
|
|
594
|
+
const width = Math.max(10, Math.min(columns, 100) - (settings.timestamp ? 11 : 0) - groups.length * 2);
|
|
595
|
+
const text = title ? `── ${title} ${"─".repeat(Math.max(0, width - color.width(title) - 4))}` : "─".repeat(width);
|
|
596
|
+
return logger.log(`${settings.delimiter.open}gray ${text}${settings.delimiter.close}`);
|
|
597
|
+
},
|
|
598
|
+
child: (scope, childOptions = {}) =>
|
|
599
|
+
_create({
|
|
600
|
+
...settings,
|
|
601
|
+
...childOptions,
|
|
602
|
+
fields: { ...settings.fields, ...childOptions.fields },
|
|
603
|
+
scope: settings.scope ? `${settings.scope}:${scope}` : scope,
|
|
604
|
+
}),
|
|
605
|
+
setLevel: (level) => {
|
|
606
|
+
if (!Object.hasOwn(LEVELS, level)) throw new RangeError(`Unknown log level "${level}"`);
|
|
607
|
+
settings.level = level;
|
|
608
|
+
gate();
|
|
609
|
+
return logger;
|
|
610
|
+
},
|
|
611
|
+
getLevel: () => settings.level,
|
|
612
|
+
isLevelEnabled: (level) => threshold !== Infinity && (LEVELS[level] >= threshold || (debugScope && (level === "debug" || level === "trace"))),
|
|
613
|
+
configure: (next) => {
|
|
614
|
+
if (next.level !== undefined) logger.setLevel(next.level);
|
|
615
|
+
if (next.scope !== undefined) settings.scope = next.scope;
|
|
616
|
+
if (next.timestamp !== undefined) settings.timestamp = next.timestamp;
|
|
617
|
+
if (next.format !== undefined) settings.format = next.format;
|
|
618
|
+
if (next.delimiter !== undefined) settings.delimiter = { ...settings.delimiter, ...next.delimiter };
|
|
619
|
+
if (next.colors !== undefined) settings.colors = next.colors;
|
|
620
|
+
if (next.stdout !== undefined) settings.stdout = next.stdout;
|
|
621
|
+
if (next.stderr !== undefined) settings.stderr = next.stderr;
|
|
622
|
+
if (next.file !== undefined) settings.file = typeof next.file === "string" ? { path: next.file } : next.file;
|
|
623
|
+
if (next.fields !== undefined) settings.fields = next.fields;
|
|
624
|
+
if (next.redact !== undefined) settings.redact = next.redact;
|
|
625
|
+
if (next.onLog !== undefined) settings.onLog = next.onLog;
|
|
626
|
+
refresh();
|
|
627
|
+
return logger;
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
return logger;
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const _default = _create({});
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Creates a logger with its own settings.
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* const log = nc.createLogger({ scope: "api", timestamp: "HH:mm:ss.SSS" });
|
|
640
|
+
* log.info("GET /users 200"); // [14:30:05.120] [api] ℹ INFO GET /users 200
|
|
641
|
+
* log.child("db").debug("query took 12ms");
|
|
642
|
+
*
|
|
643
|
+
* const prod = nc.createLogger({ format: "json", fields: { service: "billing" } });
|
|
644
|
+
* prod.error("charge failed", { orderId: 42 }, error);
|
|
645
|
+
*
|
|
646
|
+
* @param {LoggerOptions} [options]
|
|
647
|
+
* @returns {Logger}
|
|
648
|
+
*/
|
|
649
|
+
function createLogger(options = {}) {
|
|
650
|
+
return _create(options);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Prints a message without a badge. Strings support markup, objects are
|
|
655
|
+
* pretty-printed, and several arguments are joined with spaces.
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* nc.log("<% green ✔ Saved%> in <% bold 12ms%>");
|
|
659
|
+
* nc.log("user:", { id: 1, roles: ["admin"] });
|
|
660
|
+
*
|
|
661
|
+
* @param {...unknown} args
|
|
662
|
+
* @returns {typeof import("./Logger")}
|
|
663
|
+
*/
|
|
664
|
+
function log(...args) {
|
|
665
|
+
_default.log(...args);
|
|
666
|
+
return module.exports;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Very detailed diagnostics, shown only at the `trace` level.
|
|
671
|
+
*
|
|
672
|
+
* @param {...unknown} args
|
|
673
|
+
* @returns {typeof import("./Logger")}
|
|
674
|
+
*/
|
|
675
|
+
function trace(...args) {
|
|
676
|
+
_default.trace(...args);
|
|
677
|
+
return module.exports;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Diagnostics for developers, shown at the `debug` level (the default when
|
|
682
|
+
* `DEBUG` is set).
|
|
683
|
+
*
|
|
684
|
+
* @example
|
|
685
|
+
* nc.debug("cache miss", { key });
|
|
686
|
+
*
|
|
687
|
+
* @param {...unknown} args
|
|
688
|
+
* @returns {typeof import("./Logger")}
|
|
689
|
+
*/
|
|
690
|
+
function debug(...args) {
|
|
691
|
+
_default.debug(...args);
|
|
692
|
+
return module.exports;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* An informational message.
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* nc.info("Server listening on http://localhost:3000");
|
|
700
|
+
*
|
|
701
|
+
* @param {...unknown} args
|
|
702
|
+
* @returns {typeof import("./Logger")}
|
|
703
|
+
*/
|
|
704
|
+
function info(...args) {
|
|
705
|
+
_default.info(...args);
|
|
706
|
+
return module.exports;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* A success message.
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* nc.success("Database connected");
|
|
714
|
+
*
|
|
715
|
+
* @param {...unknown} args
|
|
716
|
+
* @returns {typeof import("./Logger")}
|
|
717
|
+
*/
|
|
718
|
+
function success(...args) {
|
|
719
|
+
_default.success(...args);
|
|
720
|
+
return module.exports;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* A warning, written to stderr.
|
|
725
|
+
*
|
|
726
|
+
* @param {...unknown} args
|
|
727
|
+
* @returns {typeof import("./Logger")}
|
|
728
|
+
*/
|
|
729
|
+
function warn(...args) {
|
|
730
|
+
_default.warn(...args);
|
|
731
|
+
return module.exports;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* An error, written to stderr. Error objects show their stack trace and
|
|
736
|
+
* their chain of causes.
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* nc.error(new Error("Cannot save order", { cause: dbError }));
|
|
740
|
+
*
|
|
741
|
+
* @param {...unknown} args
|
|
742
|
+
* @returns {typeof import("./Logger")}
|
|
743
|
+
*/
|
|
744
|
+
function error(...args) {
|
|
745
|
+
_default.error(...args);
|
|
746
|
+
return module.exports;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* A fatal error, written to stderr. It doesn't stop the process; call
|
|
751
|
+
* `process.exit(1)` yourself if you mean to.
|
|
752
|
+
*
|
|
753
|
+
* @param {...unknown} args
|
|
754
|
+
* @returns {typeof import("./Logger")}
|
|
755
|
+
*/
|
|
756
|
+
function fatal(...args) {
|
|
757
|
+
_default.fatal(...args);
|
|
758
|
+
return module.exports;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Prints a label and indents what follows, until `groupEnd()`. Groups nest.
|
|
763
|
+
*
|
|
764
|
+
* @example
|
|
765
|
+
* nc.group("Startup").info("Loading config");
|
|
766
|
+
* nc.group("Database").success("Connected").groupEnd();
|
|
767
|
+
* nc.groupEnd();
|
|
768
|
+
*
|
|
769
|
+
* @param {string} [label]
|
|
770
|
+
* @returns {typeof import("./Logger")}
|
|
771
|
+
*/
|
|
772
|
+
function group(label) {
|
|
773
|
+
_default.group(label);
|
|
774
|
+
return module.exports;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Closes the current group.
|
|
779
|
+
*
|
|
780
|
+
* @returns {typeof import("./Logger")}
|
|
781
|
+
*/
|
|
782
|
+
function groupEnd() {
|
|
783
|
+
_default.groupEnd();
|
|
784
|
+
return module.exports;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Starts a named timer. `timeEnd()` prints the elapsed time.
|
|
789
|
+
*
|
|
790
|
+
* @example
|
|
791
|
+
* nc.timeStart("import");
|
|
792
|
+
* await importProducts();
|
|
793
|
+
* nc.timeEnd("import"); // "import: 1s 240ms"
|
|
794
|
+
*
|
|
795
|
+
* @param {string} [label="default"]
|
|
796
|
+
* @returns {typeof import("./Logger")}
|
|
797
|
+
*/
|
|
798
|
+
function timeStart(label) {
|
|
799
|
+
_default.timeStart(label);
|
|
800
|
+
return module.exports;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Prints the time since `timeStart()` with the same label.
|
|
805
|
+
*
|
|
806
|
+
* @param {string} [label="default"]
|
|
807
|
+
* @returns {typeof import("./Logger")}
|
|
808
|
+
*/
|
|
809
|
+
function timeEnd(label) {
|
|
810
|
+
_default.timeEnd(label);
|
|
811
|
+
return module.exports;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Prints rows as a table.
|
|
816
|
+
*
|
|
817
|
+
* @example
|
|
818
|
+
* nc.table([{ name: "Ada", role: "admin" }, { name: "Bob", role: "user" }]);
|
|
819
|
+
*
|
|
820
|
+
* @param {Array<Record<string, unknown>> | unknown[][]} rows
|
|
821
|
+
* @param {import("./Cli").TableOptions} [options]
|
|
822
|
+
* @returns {typeof import("./Logger")}
|
|
823
|
+
*/
|
|
824
|
+
function table(rows, options) {
|
|
825
|
+
_default.table(rows, options);
|
|
826
|
+
return module.exports;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Prints text in a box. Nice for startup banners.
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* nc.box("Server ready\nhttp://localhost:3000", { title: "my-app", borderColor: "green" });
|
|
834
|
+
*
|
|
835
|
+
* @param {string} text
|
|
836
|
+
* @param {import("./Cli").BoxOptions} [options]
|
|
837
|
+
* @returns {typeof import("./Logger")}
|
|
838
|
+
*/
|
|
839
|
+
function box(text, options) {
|
|
840
|
+
_default.box(text, options);
|
|
841
|
+
return module.exports;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Prints a horizontal line, with an optional title in it.
|
|
846
|
+
*
|
|
847
|
+
* @param {string} [title]
|
|
848
|
+
* @returns {typeof import("./Logger")}
|
|
849
|
+
*/
|
|
850
|
+
function divider(title) {
|
|
851
|
+
_default.divider(title);
|
|
852
|
+
return module.exports;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Sets the minimum level.
|
|
857
|
+
*
|
|
858
|
+
* @example
|
|
859
|
+
* nc.setLevel("warn"); // warnings and errors only
|
|
860
|
+
* nc.setLevel("silent"); // nothing
|
|
861
|
+
*
|
|
862
|
+
* @param {LogLevel} level
|
|
863
|
+
* @returns {typeof import("./Logger")}
|
|
864
|
+
* @throws {RangeError} For an unknown level.
|
|
865
|
+
*/
|
|
866
|
+
function setLevel(level) {
|
|
867
|
+
_default.setLevel(level);
|
|
868
|
+
return module.exports;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* The current minimum level.
|
|
873
|
+
*
|
|
874
|
+
* @returns {LogLevel}
|
|
875
|
+
*/
|
|
876
|
+
function getLevel() {
|
|
877
|
+
return _default.getLevel();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Would this level be printed? Use it to skip building expensive debug
|
|
882
|
+
* output.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* if (nc.isLevelEnabled("debug")) nc.debug(buildHugeReport());
|
|
886
|
+
*
|
|
887
|
+
* @param {LogLevel} level
|
|
888
|
+
* @returns {boolean}
|
|
889
|
+
*/
|
|
890
|
+
function isLevelEnabled(level) {
|
|
891
|
+
return _default.isLevelEnabled(level);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Changes several settings at once.
|
|
896
|
+
*
|
|
897
|
+
* @example
|
|
898
|
+
* nc.configure({ level: "debug", file: "logs/app.log" });
|
|
899
|
+
* nc.configure({ format: "json", fields: { service: "api" } });
|
|
900
|
+
*
|
|
901
|
+
* @param {LoggerOptions} options
|
|
902
|
+
* @returns {typeof import("./Logger")}
|
|
903
|
+
*/
|
|
904
|
+
function configure(options) {
|
|
905
|
+
_default.configure(options);
|
|
906
|
+
return module.exports;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Shows or hides timestamps, or sets their pattern.
|
|
911
|
+
*
|
|
912
|
+
* @example
|
|
913
|
+
* nc.setTimestamp(false);
|
|
914
|
+
* nc.setTimestamp("YYYY-MM-DD HH:mm:ss.SSS");
|
|
915
|
+
*
|
|
916
|
+
* @param {boolean | string} value
|
|
917
|
+
* @returns {typeof import("./Logger")}
|
|
918
|
+
*/
|
|
919
|
+
function setTimestamp(value) {
|
|
920
|
+
_default.configure({ timestamp: value });
|
|
921
|
+
return module.exports;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Changes the markup delimiters, for when `<%` clashes with a template
|
|
926
|
+
* engine.
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* nc.setDelimiter({ open: "{{", close: "}}" });
|
|
930
|
+
* nc.log("{{red Hello}} world");
|
|
931
|
+
*
|
|
932
|
+
* @param {{ open?: string, close?: string }} [options]
|
|
933
|
+
* @returns {typeof import("./Logger")}
|
|
934
|
+
*/
|
|
935
|
+
function setDelimiter(options = {}) {
|
|
936
|
+
_default.configure({ delimiter: options });
|
|
937
|
+
return module.exports;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
module.exports = {
|
|
941
|
+
log,
|
|
942
|
+
trace,
|
|
943
|
+
debug,
|
|
944
|
+
info,
|
|
945
|
+
success,
|
|
946
|
+
warn,
|
|
947
|
+
error,
|
|
948
|
+
fatal,
|
|
949
|
+
group,
|
|
950
|
+
groupEnd,
|
|
951
|
+
timeStart,
|
|
952
|
+
timeEnd,
|
|
953
|
+
table,
|
|
954
|
+
box,
|
|
955
|
+
divider,
|
|
956
|
+
setLevel,
|
|
957
|
+
getLevel,
|
|
958
|
+
isLevelEnabled,
|
|
959
|
+
configure,
|
|
960
|
+
setTimestamp,
|
|
961
|
+
setDelimiter,
|
|
962
|
+
createLogger,
|
|
963
|
+
};
|