@nejs/basic-extensions 2.16.0 → 2.17.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.
@@ -0,0 +1,554 @@
1
+ const repl = await import('node:repl');
2
+ const fs = await import('node:fs');
3
+ const project = JSON.parse(String(fs.readFileSync('./package.json')));
4
+
5
+ /**
6
+ * Default prompt character used in the REPL.
7
+ *
8
+ * @constant {string} kDefaultPrompt
9
+ * @default
10
+ * @example
11
+ * console.log(kDefaultPrompt) // Output: 'λ'
12
+ */
13
+ const kDefaultPrompt = 'λ'
14
+
15
+ /**
16
+ * Formats the given text in red color for terminal output.
17
+ *
18
+ * This function uses ANSI escape codes to colorize text. The default
19
+ * additional character appended to the text is a space.
20
+ *
21
+ * @function
22
+ * @param {string} text - The text to be colorized.
23
+ * @param {string} [plus=' '] - An optional string to append after the text.
24
+ * @returns {string} The colorized text with the appended string.
25
+ * @example
26
+ * console.log(red('Error')) // Output: '\x1b[31mError\x1b[1;39m\x1b[22m '
27
+ */
28
+ const red = (text, plus = ' ') =>
29
+ `\x1b[31m${text}\x1b[1;39m\x1b[22m${plus}`
30
+
31
+ /**
32
+ * Formats the given text in green color for terminal output.
33
+ *
34
+ * This function uses ANSI escape codes to colorize text. The default
35
+ * additional character appended to the text is a space.
36
+ *
37
+ * @function
38
+ * @param {string} text - The text to be colorized.
39
+ * @param {string} [plus=' '] - An optional string to append after the text.
40
+ * @returns {string} The colorized text with the appended string.
41
+ * @example
42
+ * console.log(green('Success')) // Output: '\x1b[32mSuccess\x1b[1;39m\x1b[22m '
43
+ */
44
+ const green = (text, plus = ' ') =>
45
+ `\x1b[32m${text}\x1b[1;39m\x1b[22m${plus}`
46
+
47
+ /**
48
+ * Default options for configuring the REPL (Read-Eval-Print Loop) environment.
49
+ *
50
+ * This object provides a set of default configurations that can be used to
51
+ * initialize a REPL session. Each property in this object can be overridden
52
+ * by providing a custom options object when creating a REPL instance.
53
+ *
54
+ * @constant {Object} replOpts
55
+ * @property {Function|undefined} about - A function to display information
56
+ * about the REPL or project. Defaults to undefined.
57
+ * @property {boolean} allowInvocation - Determines if invocation of commands
58
+ * is allowed. Defaults to true.
59
+ * @property {boolean} allowDefaultCommands - Indicates if default commands
60
+ * like 'clear' and 'about' should be available. Defaults to true.
61
+ * @property {Array} commands - An array of custom command definitions to be
62
+ * added to the REPL. Defaults to an empty array.
63
+ * @property {Object} exports - An object containing variables or functions
64
+ * to be exported to the REPL context. Defaults to an empty object.
65
+ * @property {Function} onReady - A callback function that is executed when
66
+ * the REPL is ready. Defaults to an empty function.
67
+ * @property {string} prompt - The prompt string displayed in the REPL.
68
+ * Defaults to the value of `kDefaultPrompt`.
69
+ * @property {boolean} useGlobal - Specifies whether the REPL should use the
70
+ * global context. Defaults to true.
71
+ * @property {Object} replOpts - Additional options to be passed to the REPL
72
+ * server. Defaults to an empty object.
73
+ *
74
+ * @example
75
+ * // Creating a REPL with default options
76
+ * const replInstance = createRepl()
77
+ *
78
+ * @example
79
+ * // Overriding default options
80
+ * const customRepl = createRepl({
81
+ * prompt: '> ',
82
+ * allowDefaultCommands: false
83
+ * })
84
+ */
85
+ const replOpts = {
86
+ about: undefined,
87
+ allowDefaultCommands: true,
88
+ commands: [],
89
+ exports: {},
90
+ onReady: () => { },
91
+ prompt: kDefaultPrompt,
92
+ useGlobal: true,
93
+ replOpts: {},
94
+ }
95
+
96
+ /**
97
+ * Creates a REPL (Read-Eval-Print Loop) instance with customizable options.
98
+ *
99
+ * This function initializes a REPL environment using the provided options,
100
+ * allowing for the execution of commands and scripts in a dynamic context.
101
+ * It supports custom commands, prompt customization, and context exports.
102
+ *
103
+ * @function createRepl
104
+ * @param {Object} [options] - Configuration options for the REPL instance.
105
+ * @param {Function} [options.about] - Function to display information about
106
+ * the REPL or project.
107
+ * @param {boolean} [options.allowDefaultCommands=true] - Indicates if default
108
+ * commands like 'clear' and 'about' should be available.
109
+ * @param {Array} [options.commands] - Custom command definitions to be added
110
+ * to the REPL.
111
+ * @param {Object} [options.exports] - Variables or functions to be exported
112
+ * to the REPL context.
113
+ * @param {Function} [options.onReady] - Callback executed when the REPL is
114
+ * ready.
115
+ * @param {string} [options.prompt=kDefaultPrompt] - The prompt string
116
+ * displayed in the REPL.
117
+ * @param {boolean} [options.useGlobal=true] - Specifies whether the REPL
118
+ * should use the global context.
119
+ * @param {Object} [options.replOpts] - Additional options for the REPL server.
120
+ *
121
+ * @example
122
+ * // Creating a REPL with default options
123
+ * const replInstance = createRepl()
124
+ *
125
+ * @example
126
+ * // Overriding default options
127
+ * const customRepl = createRepl({
128
+ * prompt: '> ',
129
+ * allowDefaultCommands: false
130
+ * })
131
+ */
132
+ export function createRepl(options) {
133
+ options = {
134
+ ...replOpts,
135
+ ...((options && typeof options === 'object' && options) || {})
136
+ }
137
+
138
+ const prompt = green(options?.prompt ?? kDefaultPrompt)
139
+ const replServer = new repl.REPLServer({
140
+ useGlobal: options?.useGlobal ?? false,
141
+ prompt: options?.prompt ?? kDefaultPrompt,
142
+ ...(options?.replOpts ?? {})
143
+ })
144
+
145
+ const aboutFn = options?.about ?? defaultAbout.bind(replServer)
146
+ const state = {
147
+ allowInvocation: true
148
+ }
149
+
150
+ const clearFn = (displayPrompt = true) => {
151
+ clear(state)
152
+
153
+ if (displayPrompt)
154
+ replServer.displayPrompt()
155
+ }
156
+
157
+ let commands = [
158
+ ...(options?.allowDefaultCommands === false ? [] : [
159
+ ['cls', { action: () => clearFn(false), help: 'Clears the screen' }],
160
+ ['clear', { action: () => clearFn(false), help: 'Clears the screen' }],
161
+ ['about', { action: aboutFn, help: 'Shows info about this project' }],
162
+ ['state', {
163
+ action() {
164
+ printStateString(options?.exports ?? replServer.context, state)
165
+ },
166
+ help: 'Generates state about this REPL context'
167
+ }]
168
+ ]),
169
+ ...(Array.isArray(options?.commands) ? options.commands : [])
170
+ ]
171
+
172
+ for (const [command, options] of commands) {
173
+ const { action, help, overridable } = options
174
+
175
+ replServer.defineCommand(command, { action, help })
176
+
177
+ if (overridable !== false) {
178
+ overridableGlobal(replServer, command, action)
179
+ }
180
+ }
181
+
182
+ Object.assign(replServer.context,
183
+ options?.exports ?? {},
184
+ {
185
+ [Symbol.for('repl.prompt')]: prompt,
186
+ replServer
187
+ },
188
+ )
189
+
190
+ Object.defineProperty(replServer, '_initialPrompt', {
191
+ get() {
192
+ const _prompt = replServer.context[Symbol.for('repl.prompt')]
193
+ const isRed = !globalThis?._
194
+
195
+ return isRed ? red(_prompt) : green(_prompt)
196
+ }
197
+ })
198
+
199
+ replServer.setupHistory('repl.history', function(err, repl) {
200
+ clearFn(false)
201
+ aboutFn(false)
202
+ options?.onReady?.call(replServer)
203
+ replServer.displayPrompt()
204
+ })
205
+
206
+ return replServer
207
+ }
208
+
209
+ /**
210
+ * Displays information about the current project in the REPL.
211
+ *
212
+ * This function outputs the project's name, version, description, and author
213
+ * to the console using ANSI escape codes for color formatting. It attempts to
214
+ * display the REPL prompt after printing the information.
215
+ *
216
+ * The function uses optional chaining to check if `this` context has a
217
+ * `displayPrompt` method. If not, it defaults to using the `replServer`
218
+ * instance to display the prompt.
219
+ *
220
+ * @function defaultAbout
221
+ * @example
222
+ * // Outputs project information in the REPL
223
+ * defaultAbout()
224
+ */
225
+ function defaultAbout(displayPrompt = true) {
226
+ console.log(`\x1b[32m${project.name}\x1b[39m v\x1b[1m${project.version}\x1b[22m`);
227
+ console.log(`\x1b[3m${project.description}\x1b[23m`);
228
+ console.log(`Written by \x1b[34m${project.author ?? 'Jane Doe'}\x1b[39m.`);
229
+
230
+ if (displayPrompt) {
231
+ console.log('')
232
+ return this?.displayPrompt() ?? replServer.displayPrompt();
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Clears the terminal screen if invocation is allowed.
238
+ *
239
+ * This function uses ANSI escape codes to reset the cursor position and
240
+ * clear the terminal screen. It is typically used to refresh the display
241
+ * in a REPL environment.
242
+ *
243
+ * @function clear
244
+ * @param {boolean} [replState.allowInvocation] - A flag indicating whether the
245
+ * screen clearing is permitted. Defaults to true.
246
+ * @example
247
+ * // Clears the screen if invocation is allowed
248
+ * clear()
249
+ */
250
+ function clear(replState) {
251
+ if (replState.allowInvocation) {
252
+ process.stdout.write('\x1b[3;0f\x1b[2J')
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Creates an overridable global property within a given context, allowing
258
+ * dynamic reassignment and restoration of its default behavior.
259
+ *
260
+ * This function defines a property on the specified context that can be
261
+ * overridden by an expression assignment. It also registers a REPL command
262
+ * to restore the property to its default state. The property is initially
263
+ * set to execute a provided action function, and upon reassignment, it
264
+ * stores the new value and logs a message indicating the change.
265
+ *
266
+ * @function overridableGlobal
267
+ * @param {Object} replServer - The REPL server instance to define commands on.
268
+ * @param {string} property - The name of the property to be made overridable.
269
+ * @param {Function} action - The default function to execute when the property
270
+ * is accessed before being overridden.
271
+ * @param {string} [changeText='Expression assignment to "@X", previous function now disabled.'] -
272
+ * The message to log when the property is overridden. The placeholder "@X" is
273
+ * replaced with the property name.
274
+ * @param {Object} [context=globalThis] - The context in which to define the
275
+ * property. Defaults to the global object.
276
+ *
277
+ * @example
278
+ * // Define an overridable global property 'myProp' in the REPL
279
+ * overridableGlobal(replServer, 'myProp', () => 'default value')
280
+ */
281
+ function overridableGlobal(
282
+ replServer,
283
+ property,
284
+ action,
285
+ changeText = 'Expression assignment to "@X", previous function now disabled.',
286
+ context = globalThis,
287
+ ) {
288
+ const message = changeText.replaceAll(/\@X/g, property)
289
+
290
+ let changed = false
291
+ let storage = undefined
292
+
293
+ const makeDescriptor = () => ({
294
+ get() {
295
+ if (changed === false) {
296
+ return action()
297
+ }
298
+
299
+ return storage
300
+ },
301
+ set(value) {
302
+ if (changed === false) {
303
+ console.log(message)
304
+ changed = true
305
+ }
306
+
307
+ storage = value
308
+ },
309
+ configurable: true,
310
+ get enumerable() { return changed }
311
+ })
312
+
313
+ replServer.defineCommand(
314
+ `restore${property.charAt(0).toUpperCase()}${property.substring(1,property.length)}`,
315
+ {
316
+ action() {
317
+ changed = false
318
+ storage = undefined
319
+
320
+ Object.defineProperty(context, property, makeDescriptor())
321
+ console.log(this.help)
322
+ },
323
+ help: `Restores ${property} to default REPL custom state.`
324
+ }
325
+ )
326
+
327
+ Object.defineProperty(context, property, makeDescriptor())
328
+ }
329
+
330
+ /**
331
+ * Generates a snapshot of the current REPL state, categorizing global objects
332
+ * into classes, functions, properties, symbols, and descriptors. This function
333
+ * is designed to capture and organize the current state for inspection or
334
+ * modification purposes. It temporarily disables invocation to safely enumerate
335
+ * global objects, capturing their descriptors and categorizing them accordingly.
336
+ * If invocation is already disabled, it returns the current state without
337
+ * modification. Skipped properties during enumeration are tracked but not
338
+ * processed further.
339
+ *
340
+ * @returns {Object} An object representing the current REPL state, with
341
+ * properties for classes, functions, properties, symbols, and descriptors
342
+ * (further divided into accessors and data descriptors). Each category is an
343
+ * object with keys as the global identifiers and values containing the key,
344
+ * value, and descriptor of the item.
345
+ */
346
+ function generateState(forObject = globalThis, _state) {
347
+ const replState = {
348
+ classes: {},
349
+ functions: {},
350
+ properties: {},
351
+ symbols: {},
352
+ descriptors: {
353
+ accessors: {},
354
+ data: {},
355
+ },
356
+ };
357
+
358
+ if (!_state.allowInvocation) {
359
+ return replState;
360
+ }
361
+
362
+ let skipped = [];
363
+
364
+ _state.allowInvocation = false;
365
+ Reflect.ownKeys(forObject).forEach(key => {
366
+ try {
367
+ const value = forObject[key];
368
+ const descriptor = Object.getOwnPropertyDescriptor(forObject, key);
369
+
370
+ if (String(value).startsWith('class')) {
371
+ replState.classes[key] = {key, value, descriptor};
372
+ }
373
+ else if (typeof value === 'function') {
374
+ replState.functions[key] = {key, value, descriptor};
375
+ }
376
+ else {
377
+ replState.properties[key] = {key, value, descriptor};
378
+ }
379
+
380
+ if (typeof key === 'symbol') {
381
+ replState.symbols[key] = { key, value, descriptor };
382
+ }
383
+
384
+ if (Reflect.has(descriptor, 'get') || Reflect.has(descriptor, 'set')) {
385
+ replState.descriptors.accessors[key] = { key, descriptor };
386
+ }
387
+ else if (Reflect.has(descriptor, 'value')) {
388
+ replState.descriptors.data[key] = { key, descriptor };
389
+ }
390
+ }
391
+ catch (ignored) {
392
+ skipped.push(String(key));
393
+ }
394
+ });
395
+ _state.allowInvocation = true;
396
+
397
+ return replState;
398
+ }
399
+
400
+ /**
401
+ * Prints a formatted string representation of the state of an object.
402
+ *
403
+ * This function generates a state object for the given `forObject` and
404
+ * prints its classes, functions, properties, and descriptors in a
405
+ * human-readable format. The output is styled using ANSI escape codes
406
+ * for terminal display.
407
+ *
408
+ * @param {Object} [forObject=globalThis] - The object to generate the
409
+ * state from. Defaults to the global object.
410
+ *
411
+ * @example
412
+ * // Prints the state of the global object
413
+ * printStateString()
414
+ *
415
+ * @example
416
+ * // Prints the state of a custom object without allowing invocation
417
+ * const myObject = { a: 1, b: function() {}, c: class {} }
418
+ * printStateString(myObject, false)
419
+ */
420
+ function printStateString(forObject = globalThis, _state) {
421
+ const state = generateState(forObject, _state);
422
+ const b = (s) => `\x1b[1m${s}\x1b[22m`;
423
+ const i = (s) => `\x1b[3m${s}\x1b[23m`;
424
+ const j = ', ';
425
+
426
+ state.classes = [...Object.keys(state.classes)].map(k => String(k));
427
+ state.functions = [...Object.keys(state.functions)].map(k => String(k));
428
+ state.properties = [...Object.keys(state.properties)].map(k => String(k));
429
+
430
+ state.descriptors.accessors = [...Object.keys(state.descriptors.accessors)]
431
+ .map(k => String(k));
432
+
433
+ state.descriptors.data = [...Object.keys(state.descriptors.data)]
434
+ .map(k => String(k));
435
+
436
+ if (state.classes.length)
437
+ console.log(`${b('Classes')}\n${wrapContent(state.classes, i, j)}`);
438
+
439
+ if (state.functions.length)
440
+ console.log(`${b('Functions')}\n${wrapContent(state.functions, i, j)}`);
441
+
442
+ if (state.properties.length)
443
+ console.log(`${b('Properties')}\n${wrapContent(state.properties, i, j)}`);
444
+
445
+ if (state.descriptors.accessors.length)
446
+ console.log(`${b('Accessors')}\n${wrapContent(state.descriptors.accessors, i, j)}`);
447
+
448
+ console.log('')
449
+ }
450
+
451
+ /**
452
+ * Formats a string or array of values into lines with specified indentation and line width.
453
+ * @param {string|array} input - The input string or array of strings to be formatted.
454
+ * @param {number} nCols - The maximum number of columns per line (default 80).
455
+ * @param {number} nSpaceIndents - The number of spaces for indentation (default 2).
456
+ * @returns {string} The formatted string.
457
+ */
458
+ function formatValues(input, transform, nCols = 80, nSpaceIndents = 2) {
459
+ // Split the string into an array if input is a string
460
+ const values = typeof input === 'string' ? input.split(', ') : input;
461
+ let line = ''.padStart(nSpaceIndents, ' ');
462
+ let result = [];
463
+
464
+ values.forEach((value, index) => {
465
+ // Transform value if a transform function is supplied.
466
+ if (transform && typeof transform === 'function') {
467
+ value = transform(value);
468
+ }
469
+
470
+ // Check if adding the next value exceeds the column limit
471
+ if (line.length + value.length + 2 > nCols && line.trim().length > 0) {
472
+ // If it does, push the line to the result and start a new line
473
+ result.push(line);
474
+ line = ''.padStart(nSpaceIndents, ' ');
475
+ }
476
+
477
+ // Add the value to the line, followed by ", " if it's not the last value
478
+ line += value + (index < values.length - 1 ? ', ' : '');
479
+ });
480
+
481
+ // Add the last line if it's not empty
482
+ if (line.trim().length > 0) {
483
+ result.push(line);
484
+ }
485
+
486
+ return result.join('\n');
487
+ }
488
+
489
+ /**
490
+ * Wraps a long string or array of strings into lines with specified
491
+ * indentation and line width.
492
+ *
493
+ * This function processes the input by splitting it into lines, applying
494
+ * optional transformations, and wrapping the content to fit within a
495
+ * specified width. It handles ANSI escape codes to ensure accurate
496
+ * length calculations for terminal output.
497
+ *
498
+ * @function wrapContent
499
+ * @param {string|Array} longString - The input string or array of strings
500
+ * to be wrapped.
501
+ * @param {Function} [transform] - An optional function to transform each
502
+ * element before wrapping.
503
+ * @param {string} [joinOn=' '] - The string used to join elements in a line.
504
+ * @param {number} [indent=2] - The number of spaces for indentation.
505
+ * @param {number} [wrapAt=80] - The maximum line width for wrapping.
506
+ * @returns {string} The wrapped content as a single string with lines
507
+ * separated by newlines.
508
+ * @example
509
+ * // Wraps a long string with default settings
510
+ * const wrapped = wrapContent('This is a very long string that needs to be wrapped.')
511
+ * console.log(wrapped)
512
+ */
513
+ function wrapContent(
514
+ longString,
515
+ transform,
516
+ joinOn = ' ',
517
+ indent = 2,
518
+ wrapAt = 80
519
+ ) {
520
+ let asArray = Array.isArray(longString)
521
+ ? longString
522
+ : String(longString).replaceAll(/\r\n/g, '\n').split('\n')
523
+
524
+ asArray = asArray.map(element => String(element).trim())
525
+
526
+ let lines = []
527
+ let maxLen = wrapAt - indent
528
+ let curLine = []
529
+ let sgrLength = (s) => s.replaceAll(/\x1b\[?\d+(;\d+)*[a-zA-Z]/g, '').length
530
+
531
+ for (let element of asArray) {
532
+ if (typeof transform === 'function') {
533
+ element = String(transform(element)).trim()
534
+ }
535
+
536
+ let curLength = sgrLength(curLine.join(joinOn))
537
+ let elementLength = sgrLength(String(element) + joinOn)
538
+
539
+ if (curLength + elementLength > maxLen) {
540
+ let leading = indent > 0 ? ' '.repeat(indent) : ''
541
+ lines.push(`${leading}${curLine.join(joinOn)}`)
542
+ curLine = []
543
+ }
544
+
545
+ curLine.push(String(element))
546
+ }
547
+
548
+ if (curLine.length) {
549
+ let leading = indent > 0 ? ' '.repeat(indent) : ''
550
+ lines.push(`${leading}${curLine.join(joinOn)}`)
551
+ }
552
+
553
+ return lines.join('\n')
554
+ }