@nejs/basic-extensions 2.19.0 → 2.21.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 (68) hide show
  1. package/bin/repl.basics.js +36 -6
  2. package/bin/repl.signature.js +63 -0
  3. package/dist/@nejs/basic-extensions.bundle.2.21.0.js +25 -0
  4. package/dist/@nejs/basic-extensions.bundle.2.21.0.js.map +7 -0
  5. package/dist/cjs/classes/descriptor.d.ts +1 -1
  6. package/dist/cjs/classes/enum.d.ts +37 -19
  7. package/dist/cjs/classes/enum.js +192 -64
  8. package/dist/cjs/classes/enum.js.map +1 -1
  9. package/dist/cjs/classes/iterable.d.ts +3 -3
  10. package/dist/cjs/classes/param.parser.d.ts +1 -7
  11. package/dist/cjs/classes/pluggable.proxy.d.ts +2 -1
  12. package/dist/cjs/classes/property.d.ts +2 -9
  13. package/dist/cjs/classes/refset.d.ts +2 -2
  14. package/dist/cjs/classes/symkeys.d.ts +1 -1
  15. package/dist/cjs/global.this.js +17 -7
  16. package/dist/cjs/global.this.js.map +1 -1
  17. package/dist/cjs/index.d.ts +8 -5
  18. package/dist/cjs/index.js +13 -8
  19. package/dist/cjs/index.js.map +1 -1
  20. package/dist/cjs/string.extensions.js +38 -0
  21. package/dist/cjs/string.extensions.js.map +1 -1
  22. package/dist/cjs/utils/copy.object.d.ts +2 -2
  23. package/dist/cjs/utils/descriptor.utils.d.ts +152 -0
  24. package/dist/cjs/utils/descriptor.utils.js +274 -13
  25. package/dist/cjs/utils/descriptor.utils.js.map +1 -1
  26. package/dist/cjs/utils/index.d.ts +15 -0
  27. package/dist/cjs/utils/index.js +9 -0
  28. package/dist/cjs/utils/index.js.map +1 -1
  29. package/dist/cjs/utils/stdout.d.ts +742 -0
  30. package/dist/cjs/utils/stdout.js +1042 -0
  31. package/dist/cjs/utils/stdout.js.map +1 -0
  32. package/dist/mjs/classes/descriptor.d.ts +1 -1
  33. package/dist/mjs/classes/enum.d.ts +37 -19
  34. package/dist/mjs/classes/enum.js +192 -65
  35. package/dist/mjs/classes/enum.js.map +1 -1
  36. package/dist/mjs/classes/iterable.d.ts +3 -3
  37. package/dist/mjs/classes/param.parser.d.ts +1 -7
  38. package/dist/mjs/classes/pluggable.proxy.d.ts +2 -1
  39. package/dist/mjs/classes/property.d.ts +2 -9
  40. package/dist/mjs/classes/refset.d.ts +2 -2
  41. package/dist/mjs/classes/symkeys.d.ts +1 -1
  42. package/dist/mjs/index.d.ts +8 -5
  43. package/dist/mjs/index.js +13 -8
  44. package/dist/mjs/index.js.map +1 -1
  45. package/dist/mjs/string.extensions.js +38 -0
  46. package/dist/mjs/string.extensions.js.map +1 -1
  47. package/dist/mjs/utils/copy.object.d.ts +2 -2
  48. package/dist/mjs/utils/descriptor.utils.d.ts +152 -0
  49. package/dist/mjs/utils/descriptor.utils.js +268 -13
  50. package/dist/mjs/utils/descriptor.utils.js.map +1 -1
  51. package/dist/mjs/utils/index.d.ts +15 -0
  52. package/dist/mjs/utils/index.js +10 -1
  53. package/dist/mjs/utils/index.js.map +1 -1
  54. package/dist/mjs/utils/stdout.d.ts +742 -0
  55. package/dist/mjs/utils/stdout.js +1037 -0
  56. package/dist/mjs/utils/stdout.js.map +1 -0
  57. package/package.json +6 -20
  58. package/repl.bootstrap.js +24 -16
  59. package/repl.history +30 -15
  60. package/src/classes/enum.js +278 -133
  61. package/src/index.js +22 -8
  62. package/src/string.extensions.js +41 -0
  63. package/src/utils/descriptor.utils.js +332 -14
  64. package/src/utils/index.js +20 -0
  65. package/src/utils/stdout.js +1151 -0
  66. package/tests/utils/descriptor.utils.test.js +130 -0
  67. package/dist/@nejs/basic-extensions.bundle.2.19.0.js +0 -19
  68. package/dist/@nejs/basic-extensions.bundle.2.19.0.js.map +0 -7
@@ -0,0 +1,1151 @@
1
+ import { Extension, Patch } from '@nejs/extension'
2
+
3
+ /**
4
+ * Captures the output written to `process.stdout` during the execution of
5
+ * a callback function. This function temporarily overrides the standard
6
+ * output stream to capture any data written to it, allowing for inspection
7
+ * or testing of console output.
8
+ *
9
+ * @param {Function|*} callback - The function to execute, during which
10
+ * `process.stdout` is captured. If not a function, it will be treated
11
+ * as the first argument to a console log.
12
+ * @param {Array} [args=[]] - Arguments to pass to the callback function.
13
+ * @param {Object} [thisArg=console] - The value of `this` provided for
14
+ * the call to `callback`.
15
+ * @returns {string} The captured output from `process.stdout`.
16
+ *
17
+ * @example
18
+ * const output = captureStdout(() => {
19
+ * console.log('Hello, World!')
20
+ * })
21
+ * console.log(output) // Outputs: 'Hello, World!'
22
+ *
23
+ * @description
24
+ * This function is useful for testing or capturing console output without
25
+ * displaying it in the terminal. It works by temporarily replacing
26
+ * `process.stdout.write` with a custom function that appends output to a
27
+ * string. After the callback is executed, the original `process.stdout.write`
28
+ * is restored.
29
+ */
30
+ export function captureStdout(callback, args = [], thisArg = console) {
31
+ let captured = ''
32
+ const originalWrite = process.stdout.write
33
+
34
+ if (typeof callback !== 'function') {
35
+ let newArgs = [callback]
36
+
37
+ if (thisArg) {
38
+ newArgs.push(thisArg)
39
+ }
40
+
41
+ newArgs = newArgs.concat(args)
42
+
43
+ callback = function () {
44
+ console.log(...newArgs)
45
+ }
46
+
47
+ thisArg = console
48
+ args = []
49
+ }
50
+
51
+ process.stdout.write = (chunk, encoding, callback) => {
52
+ captured += chunk
53
+ }
54
+
55
+ try {
56
+ callback.apply(thisArg, args)
57
+ } finally {
58
+ process.stdout.write = originalWrite
59
+ }
60
+
61
+ return captured.substring(0, captured.length - 1)
62
+ }
63
+
64
+ /**
65
+ * A class that simulates a console for capturing and manipulating console
66
+ * output as strings. This class provides methods to log messages, format
67
+ * them with colors, and store them in a buffer for later inspection or
68
+ * manipulation.
69
+ *
70
+ * @example
71
+ * const stringConsole = new StringConsole()
72
+ * stringConsole.log('Hello, World!')
73
+ * stringConsole.buffer // ['Hello, World!']
74
+ *
75
+ * @description
76
+ * The StringConsole class is designed to capture console output without
77
+ * displaying it in the terminal. It stores the output in a buffer, allowing
78
+ * for easy retrieval and manipulation. This is particularly useful for
79
+ * testing or when console output needs to be processed programmatically.
80
+ */
81
+ export class StringConsole {
82
+ /**
83
+ * @type {Array}
84
+ * @description
85
+ * The buffer array is used to store captured console output. It is
86
+ * initialized as an empty array and can be populated with strings
87
+ * representing console messages. This buffer serves as a temporary
88
+ * storage for output that can be manipulated or inspected later.
89
+ *
90
+ * @example
91
+ * const console = new StringConsole()
92
+ * console.buffer.push('Hello, World!')
93
+ * console.buffer // ['Hello, World!']
94
+ */
95
+ buffer = [];
96
+
97
+ /**
98
+ * The last index of the buffer when capture began. This number should be
99
+ * set to `NaN` when not in use.
100
+ *
101
+ * @type {number|NaN}
102
+ */
103
+ capturedAt = NaN
104
+
105
+ /**
106
+ * If this is `true`, all "logged" output will be captured in an ever
107
+ * growing buffer.
108
+ *
109
+ * @type {boolean}
110
+ * @see {@link StringConsole.buffer}
111
+ */
112
+ captureOutput = true;
113
+
114
+ /**
115
+ * @typedef {
116
+ * Int8Array|Int16Array|Int32Array|Float32Array|Float64Array
117
+ * } TypedArray
118
+ */
119
+
120
+ /**
121
+ * @typedef {(
122
+ * chunk: string|Buffer|TypedArray|DataView,
123
+ * encoding: string|null,
124
+ * callback: ()=>{}
125
+ * )=>boolean} StringConsoleRecorder
126
+ * @property {boolean} [Symbol.for('StringConsole.recorder')]
127
+ */
128
+
129
+ /**
130
+ * The recorder function is what is subsituted for the `process.stdout.write`
131
+ * function whenever we need to temporarily capture the output of data bound
132
+ * for the bidirectional read-write stream, `stdout`.
133
+ *
134
+ * @type {StringConsoleRecorder}
135
+ * @param {string|Buffer|TypedArray|DataView|any} chunk Optional data to
136
+ * write. For streams not operating in object mode, chunk must be a
137
+ * {@link String}, {@link Buffer}, {@link Int8Array}, {@link Int16Array},
138
+ * {@link Int32Array}, {@link Float32Array}, {@link Float64Array} or
139
+ * {@link DataView}. For object mode streams, chunk may be any JavaScript
140
+ * value other than `null`.
141
+ * @param {string|null} encoding the encoding, if chunk is a string.
142
+ * Default: `'utf8'`
143
+ * @param {Function} callback callback for when this chunk of data is
144
+ * flushed.
145
+ *
146
+ * @returns {boolean} false if the stream wishes for the calling code to
147
+ * wait for the 'drain' event to be emitted before continuing to write
148
+ * additional data; otherwise true.
149
+ */
150
+ recorder = Object.defineProperty(
151
+ function recorder(chunk, encoding, callback) { this.buffer.push(chunk) },
152
+ Symbol.for(`StringConsole.recorder`),
153
+ { value: true, configurable: true }
154
+ );
155
+
156
+ /**
157
+ * Initializes a new instance of the StringConsole class.
158
+ *
159
+ * @param {string|string[]} [initialContents] - The initial contents to
160
+ * populate the buffer. If an array is provided, it will be used directly
161
+ * as the buffer. If a single string is provided, it will be converted
162
+ * to a string and added to the buffer.
163
+ *
164
+ * @example
165
+ * const console1 = new StringConsole('Hello')
166
+ * console1.buffer // ['Hello']
167
+ *
168
+ * const console2 = new StringConsole(['Hello', 'World'])
169
+ * console2.buffer // ['Hello', 'World']
170
+ */
171
+ constructor(captureOutput = true, initialContents = undefined) {
172
+ this.recorder = this.recorder.bind(this)
173
+
174
+ if (Array.isArray(initialContents))
175
+ this.buffer = initialContents
176
+
177
+ else if (initialContents)
178
+ this.buffer.push(String(initialContents))
179
+ }
180
+
181
+ /**
182
+ * Clears the buffer by removing all elements.
183
+ *
184
+ * This method utilizes the `splice` function to remove all elements
185
+ * from the buffer array, effectively resetting it to an empty state.
186
+ * This is useful when you want to discard all previously captured
187
+ * console output and start fresh.
188
+ *
189
+ * @returns {StringConsole} `this` to allow for calling `clear()`
190
+ * before immediately invoking a console method.
191
+ *
192
+ * @example
193
+ * const console = new StringConsole(['Hello', 'World'])
194
+ * console.clear()
195
+ * console.buffer // []
196
+ */
197
+ clear() {
198
+ this.buffer.splice(0, this.buffer.length)
199
+
200
+ return this
201
+ }
202
+
203
+
204
+ /**
205
+ * Checks if the console output is currently being captured.
206
+ *
207
+ * This method determines if the `process.stdout.write` function has been
208
+ * overridden to capture console output by checking for the presence of
209
+ * a specific symbol.
210
+ *
211
+ * @returns {boolean} True if capturing is active, false otherwise.
212
+ *
213
+ * @example
214
+ * const stringConsole = new StringConsole()
215
+ * stringConsole.startCapture()
216
+ * console.log(stringConsole.isCapturing()) // Stores 'true' in the buffer
217
+ */
218
+ isCapturing() {
219
+ return Reflect.has(
220
+ process.stdout.write,
221
+ Symbol.for('StringConsole.recorder')
222
+ )
223
+ }
224
+
225
+ /**
226
+ * Starts capturing console output.
227
+ *
228
+ * This method overrides the `process.stdout.write` function with a custom
229
+ * recorder function to capture all console output.
230
+ *
231
+ * @returns {number} the last index of the buffer in its current state or
232
+ * 0 if it is empty
233
+ *
234
+ * @example
235
+ * const stringConsole = new StringConsole()
236
+ * stringConsole.startCapture()
237
+ * console.log('This will be stored in stringConsole.buffer')
238
+ */
239
+ startCapture() {
240
+ if (this.captureOutput === false)
241
+ this.buffer = []
242
+
243
+ process.stdout.write = this.recorder
244
+ process.stderr.write = this.recorder
245
+ this.capturedAt = this.buffer.length ? this.buffer.length : 0
246
+
247
+ return this.capturedAt
248
+ }
249
+
250
+ /**
251
+ * An object containing two properties covering the captured content
252
+ * while `process.stdout.write` was swapped. It should contain the
253
+ * range of line indicies as well as the content as an array of strings
254
+ *
255
+ * @typedef {object} StringConsoleCapturedOutput
256
+ * @property {number[]} range an array of two numbers, a starting index
257
+ * and an ending index. This value will be [NaN,NaN] if this instance
258
+ * has indicated that storing captured output is disabled.
259
+ * @property {string[]} lines an array of strings of captured output
260
+ * that occurred in between calls to {@link ~startCapture} and then
261
+ * ending call to {@link ~stopCapture}
262
+ */
263
+
264
+ /**
265
+ * Stops capturing console output.
266
+ *
267
+ * This method restores the original `process.stdout.write` function,
268
+ * ceasing the capture of console output.
269
+ *
270
+ * @returns {StringConsoleCapturedOutput} the range of indices capturing
271
+ * the lines of the buffer that have been added since capturing was
272
+ * started.
273
+ *
274
+ * @example
275
+ * const stringConsole = new StringConsole()
276
+ * stringConsole.startCapture()
277
+ * console.log('This will be stored in stringConsole.buffer')
278
+ * stringConsole.stopCapture()
279
+ * console.log('This will not be captured')
280
+ */
281
+ stopCapture() {
282
+ const range = [this.capturedAt || 0, this.buffer.length - 1]
283
+ const lines = this.buffer.slice(range[0], range[1] + 1)
284
+
285
+ if (this.captureOutput === false)
286
+ this.buffer = []
287
+
288
+ process.stdout.write = StringConsole[Symbol.for('process.stdout.write')]
289
+ process.stderr.write = StringConsole[Symbol.for('process.stderr.write')]
290
+ this.capturedAt = NaN
291
+
292
+ return { range, lines }
293
+ }
294
+
295
+ /**
296
+ * Joins the StringConsole output as a single string. By default, each entry
297
+ * captured so far is joined on a new line. Pass a different joiner such as
298
+ * an empty string or a whitespace character, as examples, to change the
299
+ * output string.
300
+ *
301
+ * @param {string} joinOn the string to join the output buffer on, defaults
302
+ * to a new line character
303
+ * @returns a single string of contatenated entries so far to this buffer.
304
+ */
305
+ toString(joinOn = '') {
306
+ return this.buffer.join(joinOn)
307
+ }
308
+
309
+ /**
310
+ * Captures formatted debug messages as though they'd been printed. The
311
+ * resulting output that would have been printed is stored in the buffer
312
+ * as well as being returned.
313
+ *
314
+ * This method formats the provided arguments with color coding specific
315
+ * to the 'debug' level as though `console.debug` were used. The output
316
+ * is captured and stored in the buffer for later inspection, but not
317
+ * actually printed to the standard output.
318
+ *
319
+ * @param {any[]} args - The arguments to be log captured. These can be
320
+ * of any type and will be formatted with color coding without being logged.
321
+ *
322
+ * @returns {string} The captured console output as a string.
323
+ *
324
+ * @example
325
+ * const stringConsole = new StringConsole()
326
+ * stringConsole.debug('[debug]', 'message')
327
+ * stringConsole.buffer // Contains the captured messages so far as an array
328
+ */
329
+ debug(...args) {
330
+ args = this.constructor.colorArgs('debug', args)
331
+
332
+ this.startCapture()
333
+ console.debug(...args)
334
+
335
+ return this.stopCapture().lines.join('\n')
336
+ }
337
+
338
+ /**
339
+ * Captures formatted error messages as though they'd been printed. The
340
+ * resulting output that would have been printed is stored in the buffer
341
+ * as well as being returned.
342
+ *
343
+ * This method formats the provided arguments with color coding specific
344
+ * to the 'error' level as though `console.error` were used. The output
345
+ * is captured and stored in the buffer for later inspection, but not
346
+ * actually printed to the standard output.
347
+ *
348
+ * @param {any[]} args - The arguments to be log captured. These can be
349
+ * of any type and will be formatted with color coding without being logged.
350
+ *
351
+ * @returns {string} The captured console output as a string.
352
+ *
353
+ * @example
354
+ * const stringConsole = new StringConsole()
355
+ * stringConsole.error('[error]', 'message')
356
+ * stringConsole.buffer // Contains the captured messages so far as an array
357
+ */
358
+ error(...args) {
359
+ args = this.constructor.colorArgs('error', args)
360
+
361
+ this.startCapture()
362
+ console.error(...args)
363
+
364
+ return this.stopCapture().lines.join('\n')
365
+ }
366
+
367
+ /**
368
+ * Groups console output under a specified group name and captures the
369
+ * output. No content will actually be logged to the console, just
370
+ * the output that normally would be is formatted in a string and returned
371
+ * instead.
372
+ *
373
+ * This method allows you to format multiple messages under a single
374
+ * group name. It captures the output of each invocation and stores it in
375
+ * a buffer. The captured output is returned as a single string.
376
+ *
377
+ * @param {string} groupName - The name of the group under which the
378
+ * messages will be logged.
379
+ * @param {...Array} invocations - An array of invocations where each
380
+ * invocation is an array. The first element is the log level (e.g.,
381
+ * 'log', 'info'), and the remaining elements are the arguments to be
382
+ * logged.
383
+ *
384
+ * @returns {string} The captured console output as a string.
385
+ *
386
+ * @example
387
+ * const console = new StringConsole()
388
+ * const output = console.group('MyGroup',
389
+ * ['log', 'Hello'],
390
+ * ['warn', 'Warning!']
391
+ * )
392
+ *
393
+ * console.buffer // Contains the captured group output
394
+ */
395
+ group(groupName, ...invocations) {
396
+ const commands = ['log', 'info', 'warn', 'error', 'debug', 'trace']
397
+ const buffer = []
398
+
399
+ invocations = invocations.filter(i => commands.includes(i?.[0]))
400
+
401
+ if (groupName)
402
+ groupName = this.constructor.style(groupName, ['underline', 'bold'])
403
+ else
404
+ groupName = this.constructor.style('grouped', ['underline', 'bold'])
405
+
406
+ this.startCapture()
407
+ console.group(groupName)
408
+
409
+ for (const invocation of invocations) {
410
+ if (!Array.isArray(invocation) || invocation.length < 2)
411
+ continue
412
+
413
+ const [level, ...args] = invocation
414
+ console[level](...this.constructor.colorArgs(level, args))
415
+ }
416
+
417
+ console.groupEnd(groupName)
418
+
419
+ return this.stopCapture().lines.join('')
420
+ }
421
+
422
+ /**
423
+ * Captures formatted info messages as though they'd been printed. The
424
+ * resulting output that would have been printed is stored in the buffer
425
+ * as well as being returned.
426
+ *
427
+ * This method formats the provided arguments with color coding specific
428
+ * to the 'info' level as though `console.info` were used. The output
429
+ * is captured and stored in the buffer for later inspection, but not
430
+ * actually printed to the standard output.
431
+ *
432
+ * @param {any[]} args - The arguments to be log captured. These can be
433
+ * of any type and will be formatted with color coding without being logged.
434
+ *
435
+ * @returns {string} The captured console output as a string.
436
+ *
437
+ * @example
438
+ * const stringConsole = new StringConsole()
439
+ * stringConsole.info('[info]', 'message')
440
+ * stringConsole.buffer // Contains the captured messages so far as an array
441
+ */
442
+ info(...args) {
443
+ args = this.constructor.colorArgs('info', args)
444
+
445
+ this.startCapture()
446
+ console.info(...args)
447
+
448
+ return this.stopCapture().lines.join('\n')
449
+ }
450
+
451
+ /**
452
+ * Captures formatted log messages as though they'd been printed. The
453
+ * resulting output that would have been printed is stored in the buffer
454
+ * as well as being returned.
455
+ *
456
+ * This method formats the provided arguments with color coding specific
457
+ * to the 'log' level as though `console.log` were used. The output
458
+ * is captured and stored in the buffer for later inspection, but not
459
+ * actually printed to the standard output.
460
+ *
461
+ * @param {any[]} args - The arguments to be log captured. These can be
462
+ * of any type and will be formatted with color coding without being logged.
463
+ *
464
+ * @returns {string} The captured console output as a string.
465
+ *
466
+ * @example
467
+ * const stringConsole = new StringConsole()
468
+ * stringConsole.log('[log]', 'message')
469
+ * stringConsole.buffer // Contains the captured messages so far as an array
470
+ */
471
+ log(...args) {
472
+ args = this.constructor.colorArgs('log', args)
473
+
474
+ this.startCapture()
475
+ console.log(...args)
476
+
477
+ return this.stopCapture().lines.join('\n')
478
+ }
479
+
480
+ /**
481
+ * Captures formatted trace messages as though they'd been printed. The
482
+ * resulting output that would have been printed is stored in the buffer
483
+ * as well as being returned.
484
+ *
485
+ * This method formats the provided arguments with color coding specific
486
+ * to the 'trace' level as though `console.trace` were used. The output
487
+ * is captured and stored in the buffer for later inspection, but not
488
+ * actually printed to the standard output.
489
+ *
490
+ * @param {any[]} args - The arguments to be log captured. These can be
491
+ * of any type and will be formatted with color coding without being logged.
492
+ *
493
+ * @returns {string} The captured console output as a string.
494
+ *
495
+ * @example
496
+ * const stringConsole = new StringConsole()
497
+ * stringConsole.trace('[trace]', 'message')
498
+ * stringConsole.buffer // Contains the captured messages so far as an array
499
+ */
500
+ trace(...args) {
501
+ args = this.constructor.colorArgs('trace', args)
502
+
503
+ this.startCapture()
504
+ console.trace(...args)
505
+
506
+ return this.stopCapture().lines.join('\n')
507
+ }
508
+
509
+ /**
510
+ * Captures formatted warn messages as though they'd been printed. The
511
+ * resulting output that would have been printed is stored in the buffer
512
+ * as well as being returned.
513
+ *
514
+ * This method formats the provided arguments with color coding specific
515
+ * to the 'warn' level as though `console.warn` were used. The output
516
+ * is captured and stored in the buffer for later inspection, but not
517
+ * actually printed to the standard output.
518
+ *
519
+ * @param {any[]} args - The arguments to be log captured. These can be
520
+ * of any type and will be formatted with color coding without being logged.
521
+ *
522
+ * @returns {string} The captured console output as a string.
523
+ *
524
+ * @example
525
+ * const stringConsole = new StringConsole()
526
+ * stringConsole.warn('[warn]', 'message')
527
+ * stringConsole.buffer // Contains the captured messages so far as an array
528
+ */
529
+ warn(...args) {
530
+ args = this.constructor.colorArgs('warn', args)
531
+
532
+ this.startCapture()
533
+ console.warn(...args)
534
+
535
+ return this.stopCapture().lines.join('\n')
536
+ }
537
+
538
+ /**
539
+ * Captures a single line of text that would be logged to the console if
540
+ * the console function of the same name were to be invoked. The string
541
+ * is formatted according to the log colors, or any pre-existing colors as
542
+ * those are untouched. After formatting, the string is returned.
543
+ *
544
+ * @param {...*} args the arguments to be logged. These can be of any
545
+ * type and will be passed to the underlying console's method of the same
546
+ * name.
547
+ *
548
+ * @returns {string}
549
+ *
550
+ * @example
551
+ * const string = StringConsole.debug('[debug]: %o', someVariable)
552
+ */
553
+ static debug(...args) {
554
+ return this.#console.clear().debug(...args)
555
+ }
556
+
557
+ /**
558
+ * Captures a single line of text that would be logged to the console if
559
+ * the console function of the same name were to be invoked. The string
560
+ * is formatted according to the log colors, or any pre-existing colors as
561
+ * those are untouched. After formatting, the string is returned.
562
+ *
563
+ * @param {...*} args the arguments to be logged. These can be of any
564
+ * type and will be passed to the underlying console's method of the same
565
+ * name.
566
+ *
567
+ * @returns {string}
568
+ *
569
+ * @example
570
+ * const string = StringConsole.error('[error]: %o', someVariable)
571
+ */
572
+ static error(...args) {
573
+ return this.#console.clear().error(...args)
574
+ }
575
+
576
+ /**
577
+ * Groups console output under a specified group name and captures the
578
+ * output. No content will actually be logged to the console, just
579
+ * the output that normally would be is formatted in a string and returned
580
+ * instead.
581
+ *
582
+ * This method allows you to format multiple messages under a single
583
+ * group name. It captures the output of each invocation and stores it in
584
+ * a buffer. The captured output is returned as a single string.
585
+ *
586
+ * @param {string} groupName - The name of the group under which the
587
+ * messages will be logged.
588
+ * @param {...Array} invocations - An array of invocations where each
589
+ * invocation is an array. The first element is the log level (e.g.,
590
+ * 'log', 'info'), and the remaining elements are the arguments to be
591
+ * logged.
592
+ *
593
+ * @returns {string} The captured console output as a string.
594
+ *
595
+ * @example
596
+ * const console = new StringConsole()
597
+ * const output = console.group('MyGroup',
598
+ * ['log', 'Hello'],
599
+ * ['warn', 'Warning!']
600
+ * )
601
+ *
602
+ * console.buffer // Contains the captured group output
603
+ */
604
+ static group(groupName, ...invocations) {
605
+ return this.#console.clear().group(groupName, ...invocations)
606
+ }
607
+
608
+ /**
609
+ * Captures a single line of text that would be logged to the console if
610
+ * the console function of the same name were to be invoked. The string
611
+ * is formatted according to the log colors, or any pre-existing colors as
612
+ * those are untouched. After formatting, the string is returned.
613
+ *
614
+ * @param {...*} args the arguments to be logged. These can be of any
615
+ * type and will be passed to the underlying console's method of the same
616
+ * name.
617
+ *
618
+ * @returns {string}
619
+ *
620
+ * @example
621
+ * const string = StringConsole.info('[info]: %o', someVariable)
622
+ */
623
+ static info(...args) {
624
+ return this.#console.clear().info(...args)
625
+ }
626
+
627
+ /**
628
+ * Captures a single line of text that would be logged to the console if
629
+ * the console function of the same name were to be invoked. The string
630
+ * is formatted according to the log colors, or any pre-existing colors as
631
+ * those are untouched. After formatting, the string is returned.
632
+ *
633
+ * @param {...*} args the arguments to be logged. These can be of any
634
+ * type and will be passed to the underlying console's method of the same
635
+ * name.
636
+ *
637
+ * @returns {string}
638
+ *
639
+ * @example
640
+ * const string = StringConsole.log('[log]: %o', someVariable)
641
+ */
642
+ static log(...args) {
643
+ return this.#console.clear().log(...args)
644
+ }
645
+
646
+ /**
647
+ * Captures a single line of text that would be logged to the console if
648
+ * the console function of the same name were to be invoked. The string
649
+ * is formatted according to the log colors, or any pre-existing colors as
650
+ * those are untouched. After formatting, the string is returned.
651
+ *
652
+ * @param {...*} args the arguments to be logged. These can be of any
653
+ * type and will be passed to the underlying console's method of the same
654
+ * name.
655
+ *
656
+ * @returns {string}
657
+ *
658
+ * @example
659
+ * const string = StringConsole.trace('[trace]: %o', someVariable)
660
+ */
661
+ static trace(...args) {
662
+ return this.#console.clear().trace(...args)
663
+ }
664
+
665
+ /**
666
+ * Captures a single line of text that would be logged to the console if
667
+ * the console function of the same name were to be invoked. The string
668
+ * is formatted according to the log colors, or any pre-existing colors as
669
+ * those are untouched. After formatting, the string is returned.
670
+ *
671
+ * @param {...*} args the arguments to be logged. These can be of any
672
+ * type and will be passed to the underlying console's method of the same
673
+ * name.
674
+ *
675
+ * @returns {string}
676
+ *
677
+ * @example
678
+ * const string = StringConsole.warn('[warn]: %o', someVariable)
679
+ */
680
+ static warn(...args) {
681
+ return this.#console.clear().warn(...args)
682
+ }
683
+
684
+ /**
685
+ * Internal instance of {@link StringConsole} used for static logging
686
+ * methods.
687
+ *
688
+ * @type {StringConsole}
689
+ */
690
+ static #console = new StringConsole(false);
691
+
692
+ /**
693
+ * A static map defining color codes for console output. Each color is
694
+ * associated with an array containing two numbers, which represent
695
+ * the ANSI escape codes for styling text in the terminal.
696
+ *
697
+ * The first number in the array is the suffix code for the standard
698
+ * color, while the second number suffix code to undo the color. These
699
+ * codes are useless without the pen prefix code.
700
+ *
701
+ * @type {Map<string, number[]>}
702
+ * @see {@link StringConsole.pens}
703
+ *
704
+ * @example
705
+ * // Accessing the color codes for 'red'
706
+ * const redCodes = StringConsole.colors.get('red')
707
+ * const fgCodes = StringConsole.pens.get('foreground')
708
+ * const prefix = `\x1b[${fgCodes[0]}${redCodes[0]}m`
709
+ * const suffix = `\x1b[${fgCodes[1]}${redCodes[1]}m`
710
+ * // Outputs: "Text" in red but "!!" in the default color
711
+ * console.log(`${prefix}Text!!${suffix}`)
712
+ *
713
+ * @description
714
+ * This map is used to apply color coding to console messages, enhancing
715
+ * readability and providing visual cues for different log levels.
716
+ */
717
+ static colors = new Map([
718
+ ['black', [0, 9]],
719
+ ['red', [1, 9]],
720
+ ['green', [2, 9]],
721
+ ['yellow', [3, 9]],
722
+ ['blue', [4, 9]],
723
+ ['magenta', [5, 9]],
724
+ ['cyan', [6, 9]],
725
+ ['white', [7, 9]]
726
+ ])
727
+
728
+ /**
729
+ * A static map defining the color schemes for different logging levels.
730
+ * Each log level is associated with an array of color styles that are
731
+ * applied to the console output for that level.
732
+ *
733
+ * The available log levels and their corresponding color styles are:
734
+ * - 'log': White
735
+ * - 'info': Cyan
736
+ * - 'warn': Yellow
737
+ * - 'error': Red
738
+ * - 'trace': Magenta
739
+ * - 'debug': Bold Yellow
740
+ *
741
+ * @type {Map<string, string[]>}
742
+ *
743
+ * @example
744
+ * const logColor = StringConsole.levels.get('log') // ['white']
745
+ * const errorColor = StringConsole.levels.get('error') // ['red']
746
+ */
747
+ static levels = Object.defineProperties(new Map([
748
+ ['log', ['white']],
749
+ ['info', ['cyan']],
750
+ ['warn', ['yellow']],
751
+ ['error', ['red']],
752
+ ['trace', ['magenta']],
753
+ ['debug', ['bold', 'yellow']]
754
+ ]), {
755
+ color: {
756
+ value: function color(key) {
757
+ for (const value of this.get(key)) {
758
+ if (StringConsole.colors.has(value)) {
759
+ return StringConsole.colors.get(value)
760
+ }
761
+ }
762
+
763
+ return StringConsole.colors.get('white')
764
+ },
765
+ configurable: true,
766
+ },
767
+
768
+ styles: {
769
+ value: function styles(key) {
770
+ const styles = []
771
+
772
+ for (const value of this.get(key)) {
773
+ if (StringConsole.styles.has(value)) {
774
+ styles.push(StringConsole.styles.get(value))
775
+ }
776
+ }
777
+
778
+ return styles
779
+ },
780
+ configurable: true,
781
+ },
782
+ });
783
+
784
+ /**
785
+ * A static map defining the ANSI escape codes for different pen styles
786
+ * used in console output. Each pen style is associated with an array
787
+ * containing two numbers: the first for setting the style and the second
788
+ * for resetting it.
789
+ *
790
+ * The available pen styles and their corresponding ANSI codes are:
791
+ * - 'foreground': [3, 3] - Standard foreground color
792
+ * - 'background': [4, 4] - Standard background color
793
+ * - 'bright.foreground': [9, 3] - Bright foreground color
794
+ * - 'bright.background': [10, 4] - Bright background color
795
+ *
796
+ * These are prefixes for both enabling and disabling. Normally a red color
797
+ * is represented using SGR (Select Graphic Rendition) codes like \x1b[31m
798
+ * for the foreground and \x1b[39m to return to normal color. So the 3
799
+ * determines a foreground prefix for starting and stopping (the 3's in 31
800
+ * and 39). Background prefixes are usually 4. These change for bright
801
+ * colors which use 9 and 3, and 10 and 4, respectively.
802
+ *
803
+ * @type {Map<string, number[]>}
804
+ *
805
+ * @example
806
+ * // [3, 3]
807
+ * const foregroundPen = StringConsole.pens.get('foreground')
808
+ *
809
+ * // [10, 4]
810
+ * const brightBackgroundPen = StringConsole.pens.get('bright.background')
811
+ */
812
+ static pens = new Map([
813
+ ['foreground', [3, 3]],
814
+ ['background', [4, 4]],
815
+ ['bright.foreground', [9, 3]],
816
+ ['bright.background', [10, 4]]
817
+ ]);
818
+
819
+ /**
820
+ * A static map defining ANSI escape codes for various text styles used
821
+ * in console output. Each style is associated with an array containing
822
+ * two escape codes: one for enabling the style and one for disabling it.
823
+ *
824
+ * The available styles and their corresponding ANSI codes are:
825
+ * - 'reset': Resets all styles to default.
826
+ * - 'blink': Enables blinking text.
827
+ * - 'bold': Makes text bold.
828
+ * - 'conceal': Conceals text.
829
+ * - 'dim': Dims the text.
830
+ * - 'italics': Italicizes the text.
831
+ * - 'negative': Inverts the foreground and background colors.
832
+ * - 'strike': Strikes through the text.
833
+ * - 'underline': Underlines the text.
834
+ *
835
+ * @type {Map<string, string[]>}
836
+ *
837
+ * @example
838
+ * const boldStyle = StringConsole.styles.get('bold')
839
+ * // ['\x1b[1m', '\x1b[22m']
840
+ */
841
+ static styles = new Map([
842
+ ['reset', ['\x1b[0m']],
843
+ ['blink', ['\x1b[5m', '\x1b[25m']],
844
+ ['bold', ['\x1b[1m', '\x1b[22m']],
845
+ ['conceal', ['\x1b[8m', '\x1b[28m']],
846
+ ['dim', ['\x1b[2m', '\x1b[22m']],
847
+ ['italics', ['\x1b[3m', '\x1b[23m']],
848
+ ['negative', ['\x1b[7m', '\x1b[27m']],
849
+ ['strike', ['\x1b[9m', '\x1b[29m']],
850
+ ['underline', ['\x1b[4m', '\x1b[24m']]
851
+ ]);
852
+
853
+ /**
854
+ * Applies ANSI color codes to a given string based on specified options.
855
+ * This method checks if the string already contains color codes or if
856
+ * the input is not a string, in which case it returns the original input.
857
+ * Otherwise, it formats the string with the specified color and pen
858
+ * options.
859
+ *
860
+ * @param {string} string - The string to be colorized.
861
+ * @param {Object} [options] - Configuration options for colorization.
862
+ * @param {string} [options.level] - The log level determining
863
+ * which colors to apply.
864
+ * @param {number[]} [options.rgb8] a single color code where 0 - 7, for
865
+ * the 'standard' colors specified by the SGR sequences 30 to 37; 8-15 are
866
+ * high intensity or bright colors,
867
+ * @param {number[]} [options.rgb24] An array of three values, ordered, red,
868
+ * green and then blue. The values should range from 0 to 255.
869
+ * @param {string|string[]} [options.styles] defaulting to an empty array, if
870
+ * supplied with a single known style {@link ~styles}, or an array of them.
871
+ * @param {string} [options.pen='foreground'] - The pen type for color
872
+ * application, either 'foreground' or 'background'.
873
+ * @param {Array} [options.buffer=[]] - An array to prepend to the
874
+ * formatted string.
875
+ * @param {Array} [options.before=[]] - An array of strings to prepend
876
+ * before the main string.
877
+ * @param {Array} [options.after=[]] - An array of strings to append
878
+ * after the main string. 16 - 231, for the colors in the 6 × 6 × 6 cube
879
+ * defined by 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5); 232-255:
880
+ * grayscale from dark to light in 24 steps.
881
+ *
882
+ * @returns {string} The colorized string with ANSI codes applied.
883
+ *
884
+ * @example
885
+ * const coloredString = StringConsole.color('Hello', {
886
+ * level: 'info',
887
+ * pen: 'bright.foreground'
888
+ * })
889
+ * console.log(coloredString)
890
+ */
891
+ static color(string, options = {
892
+ level: undefined,
893
+ color: undefined,
894
+ pen: 'foreground',
895
+ rgb8: undefined,
896
+ rgb24: [186, 186, 186],
897
+ styles: [],
898
+ buffer: [],
899
+ before: [],
900
+ after: [],
901
+ }) {
902
+ const { colors: Colors, styles: Styles, pens: Pens, levels: Levels } = this
903
+ let useColors = undefined
904
+ let useRGB = false
905
+ let styles = []
906
+
907
+ const pens = this.pens.get(options?.pen ?? 'foreground')
908
+ const [p0, p1] = pens
909
+
910
+ if (options?.styles) {
911
+ if (Array.isArray(options.styles))
912
+ styles = options.styles
913
+ .filter(s => Styles.has(s))
914
+ .map(s => Styles.get(s))
915
+ else if (typeof options.styles === 'string' && Styles.has(options.styles))
916
+ styles = Styles.get(options.styles)
917
+ }
918
+
919
+ if (options?.level && Levels.has(options.level)) {
920
+ useColors = Levels.color(options.level)
921
+
922
+ const addlStyles = Levels.styles(options.level)
923
+ if (addlStyles.length)
924
+ styles = styles.concat(addlStyles)
925
+ }
926
+
927
+ else if (options?.color && Colors.has(options.color))
928
+ useColors = Colors.get(options.color)
929
+
930
+ else if (options?.rgb24 && Array.isArray(options.rgb24)) {
931
+ useColors = [`\x1b[${p0}8;2;${options.rgb24.join(';')};m`, `\x1b[${p1}9m`]
932
+ useRGB = true
933
+ }
934
+
935
+ else if (options?.rgb8 && typeof options.rgb8 === 'number' ) {
936
+ useColors = [`\x1b[${p0}8;5;${options.rgb8}m`, `\x1b[${p1}9m`]
937
+ useRGB = true
938
+ }
939
+
940
+ else useColors = [9,9]
941
+
942
+ const [c0, c1] = (useRGB
943
+ ? useColors
944
+ : useColors?.map((c,i) => `\x1b[${pens[i]}${c}m`) ?? [`\x1b[39;49m`]
945
+ )
946
+
947
+ if (this.hasColor(string)) return string
948
+ if (typeof string !== 'string') return string
949
+ if (string instanceof String) (string = String(string))
950
+ if (!useColors) return string
951
+
952
+ if (options?.buffer && !Array.isArray(options.buffer))
953
+ options.buffer = [String(options.buffer)]
954
+
955
+ if (options?.before && !Array.isArray(options.before))
956
+ options.before = [String(options.before)]
957
+
958
+ if (options?.after && !Array.isArray(options.after))
959
+ options.after = [String(options.after)]
960
+
961
+ const buffer = [].concat(options?.buffer ?? [])
962
+ const before = [].concat(options?.before ?? [])
963
+ const after = [].concat(options?.after ?? [])
964
+
965
+ if (c0)
966
+ before.push(c0)
967
+
968
+ if (c1)
969
+ after.push(c1)
970
+
971
+ for (const style of styles) {
972
+ if (style?.[0])
973
+ before.push(style[0])
974
+
975
+ if (style?.[1])
976
+ after.push(style[1])
977
+ }
978
+
979
+ return [...buffer, before.join(''), string, after.join('')].join('')
980
+ }
981
+
982
+ /**
983
+ * Applies color formatting to each argument based on the specified log level.
984
+ *
985
+ * This method processes an array of arguments, applying color formatting
986
+ * to each one according to the provided log level. The color formatting
987
+ * is determined by the `color` method, which uses the log level to
988
+ * select the appropriate color scheme.
989
+ *
990
+ * @param {string} level - The log level that determines the color scheme
991
+ * to be applied. Common levels include 'log', 'info', 'warn', 'error',
992
+ * etc.
993
+ * @param {Array} args - An array of arguments to be formatted. Each
994
+ * argument will be processed individually to apply the color formatting.
995
+ *
996
+ * @returns {Array} A new array containing the formatted arguments with
997
+ * color applied.
998
+ *
999
+ * @example
1000
+ * const formattedArgs = StringConsole.colorArgs(
1001
+ * 'info',
1002
+ * ['Message 1', 'Message 2']
1003
+ * )
1004
+ * // formattedArgs will contain the messages with 'info' level
1005
+ * // color formatting
1006
+ */
1007
+ static colorArgs(level, args) {
1008
+ const newArgs = []
1009
+
1010
+ if (args === null || args === undefined || !Array.isArray(args))
1011
+ return args
1012
+
1013
+ for (const arg of args) {
1014
+ newArgs.push(this.color(arg, { level }))
1015
+ }
1016
+
1017
+ return newArgs
1018
+ }
1019
+
1020
+ /**
1021
+ * Determines if a given string contains ANSI color codes.
1022
+ *
1023
+ * This method checks for the presence of ANSI escape codes in the
1024
+ * provided string, which are used for color formatting in terminal
1025
+ * outputs. The presence of these codes indicates that the string
1026
+ * has color formatting applied.
1027
+ *
1028
+ * @param {string} string - The string to be checked for ANSI color codes.
1029
+ *
1030
+ * @returns {boolean} Returns true if the string contains ANSI color codes,
1031
+ * otherwise false.
1032
+ *
1033
+ * @example
1034
+ * const hasColor = StringConsole.hasColor('\x1b[31mRed Text\x1b[0m')
1035
+ * // hasColor will be true
1036
+ *
1037
+ * const noColor = StringConsole.hasColor('Plain Text')
1038
+ * // noColor will be false
1039
+ */
1040
+ static hasColor(string) {
1041
+ return string.includes('\x1b[')
1042
+ }
1043
+
1044
+ /**
1045
+ * Applies a series of styles to a given string using ANSI escape codes.
1046
+ *
1047
+ * This method takes a string and an array of style names or style arrays,
1048
+ * and applies the corresponding ANSI escape codes to the string. The
1049
+ * styles are defined in the `styles` map, which associates style names
1050
+ * with their respective ANSI codes.
1051
+ *
1052
+ * @param {string} string - The string to which styles will be applied.
1053
+ * @param {string|string[]} styles - A style name or an array of style
1054
+ * names/arrays to be applied. Each style can be a string that matches
1055
+ * a key in the `styles` map or an array containing ANSI codes.
1056
+ *
1057
+ * @returns {string} The styled string with ANSI escape codes applied.
1058
+ *
1059
+ * @example
1060
+ * const styledText = StringConsole.style('Hello', ['bold', 'underline'])
1061
+ * // styledText will have 'Hello' with bold and underline styles
1062
+ */
1063
+ static style(string, styles) {
1064
+ const before = []
1065
+ const after = []
1066
+ const buffer = []
1067
+
1068
+ if (typeof styles === 'string' && this.styles.has(styles)) {
1069
+ styles = [styles]
1070
+ }
1071
+
1072
+ for (const style of styles) {
1073
+ let group = []
1074
+
1075
+ if (this.styles.has(style))
1076
+ group = this.styles.get(style)
1077
+
1078
+ else if (Array.isArray(style) && style.length >= 1)
1079
+ group = style
1080
+
1081
+ if (group?.[0])
1082
+ before.push(group?.[0])
1083
+
1084
+ if (group?.[1])
1085
+ after.push(group?.[1])
1086
+ }
1087
+
1088
+ return [before.join(''), string, after.join('')].join('')
1089
+ }
1090
+
1091
+ /* Since this class captures the swaps the process.stdout.write function,
1092
+ * in to and out of place repeatedly, we want to avoid any possible issues
1093
+ * where this conflict could be problematic. To this end, we capture the
1094
+ * global process.stdout.write function as a copy when this class is defined
1095
+ * but before it is used.
1096
+ *
1097
+ * If no other code modifies the `writer` property, this is created as a
1098
+ * non-enumerable alias to the [Symbol.for('process.stdout.write')] symbol
1099
+ * the actual function is stored in.
1100
+ */
1101
+ static {
1102
+ Object.defineProperties(StringConsole, {
1103
+ [Symbol.for('process.stdout.write')]: {
1104
+ value: Object.defineProperties(process.stdout.write, {
1105
+ [Symbol.for('original')]: {value: true, configurable: true },
1106
+ isOriginal: { get() { return true }, configurable: true },
1107
+ }),
1108
+ configurable: true,
1109
+ },
1110
+
1111
+ [Symbol.for('process.stderr.write')]: {
1112
+ value: Object.defineProperties(process.stderr.write, {
1113
+ [Symbol.for('original')]: {value: true, configurable: true },
1114
+ isOriginal: { get() { return true }, configurable: true },
1115
+ }),
1116
+ configurable: true,
1117
+ },
1118
+ })
1119
+
1120
+ if (!Reflect.has(StringConsole, 'writer')) {
1121
+ Object.defineProperties(StringConsole, {
1122
+ writer: {
1123
+ value: StringConsole[Symbol.for('process.stdout.write')],
1124
+ configurable: true,
1125
+ },
1126
+
1127
+ errorWriter: {
1128
+ value: StringConsole[Symbol.for('process.stderr.write')],
1129
+ configurable: true,
1130
+ },
1131
+ })
1132
+ }
1133
+ }
1134
+ }
1135
+
1136
+ export const SC = StringConsole
1137
+ export const StringConsoleExtension = new Extension(StringConsole)
1138
+ export const StdoutGlobalPatches = new Patch(globalThis, {
1139
+ [Patch.kMutablyHidden]: {
1140
+ captureStdout,
1141
+ }
1142
+ })
1143
+
1144
+ export default {
1145
+ SC: StringConsole,
1146
+ StringConsole,
1147
+ StringConsoleExtension,
1148
+ StdoutGlobalPatches,
1149
+
1150
+ captureStdout,
1151
+ }