bananass-utils-console 0.8.0 → 0.9.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,163 @@
1
+ /**
2
+ * @fileoverview Console logger.
3
+ * @module bananass-utils-console/logger
4
+ */
5
+ // --------------------------------------------------------------------------------
6
+ // Import
7
+ // --------------------------------------------------------------------------------
8
+ import { styleText } from 'node:util';
9
+ // --------------------------------------------------------------------------------
10
+ // Class
11
+ // --------------------------------------------------------------------------------
12
+ class Logger {
13
+ // ------------------------------------------------------------------------------
14
+ // Private Properties
15
+ // ------------------------------------------------------------------------------
16
+ #debug;
17
+ #quiet;
18
+ #textPrefix;
19
+ #textPrefixDefault = '>';
20
+ #lastMethodCalled = 'log';
21
+ #undeclaredValue = Symbol('undeclared-value');
22
+ constructor(options = {}) {
23
+ this.#debug = options.debug ?? false;
24
+ this.#quiet = options.quiet ?? false;
25
+ this.#textPrefix =
26
+ typeof options.textPrefix === 'boolean'
27
+ ? options.textPrefix
28
+ ? this.#textPrefixDefault
29
+ : this.#undeclaredValue
30
+ : (options.textPrefix ?? this.#textPrefixDefault);
31
+ }
32
+ // ------------------------------------------------------------------------------
33
+ // Public Methods
34
+ // ------------------------------------------------------------------------------
35
+ /**
36
+ * Log a message or executes a callback.
37
+ *
38
+ * The first parameter can be a message or a callback function.
39
+ * - If it's a function, it's invoked with subsequent arguments.
40
+ * - Otherwise, the message (with a configured text prefix) and additional arguments are logged.
41
+ *
42
+ * Behavior based on `quiet` option:
43
+ * - `quiet === true`: output X
44
+ * - `quiet === false`: output O
45
+ *
46
+ * @param params The message or callback function, followed by any additional arguments.
47
+ * @returns The current `Logger` instance for chaining.
48
+ */
49
+ log(...params) {
50
+ const textOrCallback = typeof params[0] !== 'undefined' // Same with default parameter implemetation using `arguments`
51
+ ? params[0]
52
+ : params.length > 0
53
+ ? undefined
54
+ : this.#undeclaredValue;
55
+ const args = params.slice(1);
56
+ this.#lastMethodCalled = 'log';
57
+ if (this.#quiet)
58
+ return this;
59
+ if (typeof textOrCallback === 'function') {
60
+ textOrCallback(...args);
61
+ }
62
+ else {
63
+ // eslint-disable-next-line no-console -- Needed for logging.
64
+ console.log(...[this.#textPrefix, textOrCallback, ...args].filter(arg => arg !== this.#undeclaredValue));
65
+ }
66
+ return this;
67
+ }
68
+ /**
69
+ * Log a message or executes a callback in debug mode.
70
+ *
71
+ * The first parameter can be a message or a callback function.
72
+ * - If it's a function, it's invoked with subsequent arguments.
73
+ * - Otherwise, the message (with a configured text prefix) and additional arguments are logged in debug mode.
74
+ *
75
+ * Behavior based on `debug` and `quiet` options:
76
+ * - `quiet === true`: output X
77
+ * - `quiet === false && debug === true`: output O
78
+ * - `quiet === false && debug === false`: output X
79
+ *
80
+ * @param params The message or callback function, followed by any additional arguments.
81
+ * @returns The current `Logger` instance for chaining.
82
+ */
83
+ debug(...params) {
84
+ const textOrCallback = typeof params[0] !== 'undefined'
85
+ ? params[0]
86
+ : params.length > 0
87
+ ? undefined
88
+ : this.#undeclaredValue;
89
+ const args = params.slice(1);
90
+ this.#lastMethodCalled = 'debug';
91
+ if (this.#quiet)
92
+ return this;
93
+ if (!this.#debug)
94
+ return this;
95
+ if (typeof textOrCallback === 'function') {
96
+ textOrCallback(...args);
97
+ }
98
+ else {
99
+ // eslint-disable-next-line no-console -- Needed for logging.
100
+ console.log(...[this.#textPrefix, textOrCallback, ...args]
101
+ .filter(arg => arg !== this.#undeclaredValue)
102
+ .map(arg => (typeof arg === 'string' ? styleText('gray', arg) : arg)));
103
+ }
104
+ return this;
105
+ }
106
+ /**
107
+ * Ends the current line by logging an empty line using the last method called.
108
+ *
109
+ * This method temporarily sets the text prefix to an undeclared value,
110
+ * then invokes either `log()` or `debug()` depending on the previously method called.
111
+ * After logging, it restores the original text prefix.
112
+ *
113
+ * @returns The current `Logger` instance for chaining.
114
+ */
115
+ eol() {
116
+ const prevTextPrefix = this.#textPrefix;
117
+ this.#textPrefix = this.#undeclaredValue;
118
+ if (this.#lastMethodCalled === 'log') {
119
+ this.log();
120
+ }
121
+ else {
122
+ this.debug();
123
+ }
124
+ this.#textPrefix = prevTextPrefix;
125
+ return this;
126
+ }
127
+ // ------------------------------------------------------------------------------
128
+ // Getters and Setters
129
+ // ------------------------------------------------------------------------------
130
+ /**
131
+ * Get the last method called.
132
+ * @returns {'log' | 'debug'}
133
+ */
134
+ get lastMethodCalled() {
135
+ return this.#lastMethodCalled;
136
+ }
137
+ }
138
+ // --------------------------------------------------------------------------------
139
+ // Export
140
+ // --------------------------------------------------------------------------------
141
+ /**
142
+ * Create a new `Logger` instance.
143
+ *
144
+ * @param options The logger options.
145
+ * @returns A new `Logger` instance.
146
+ *
147
+ * @example
148
+ * import createLogger from 'bananass-utils-console/logger';
149
+ *
150
+ * const logger = createLogger({
151
+ * debug: false,
152
+ * quiet: false,
153
+ * textPrefix: '>',
154
+ * })
155
+ *
156
+ * logger
157
+ * .log('Hello, log!');
158
+ * .debug('Hello, debug!');
159
+ * .eol();
160
+ */
161
+ export default function createLogger(options) {
162
+ return new Logger(options);
163
+ }
@@ -1,101 +1,57 @@
1
1
  /**
2
- * Create a new spinner instance.
3
- *
4
- * @param {Options} [options]
5
- * @returns {Spinner} A new spinner instance.
6
- *
7
- * @example
8
- * import createSpinner from 'bananass-utils-console/spinner';
9
- *
10
- * const spinner = createSpinner({
11
- * text: 'Loading…'
12
- * color: 'yellow',
13
- * stream: process.stderr,
14
- * spinner: {
15
- * frames: ['-', '\\', '|', '/'],
16
- * interval: 100,
17
- * },
18
- * }).start();
19
- *
20
- * setTimeout(() => {
21
- * spinner.success('Success!');
22
- * }, 2000);
2
+ * @fileoverview Console spinner.
3
+ * @module bananass-utils-console/spinner
4
+ * @license MIT Portions of this code were borrowed from [`yocto-spinner`](https://github.com/sindresorhus/yocto-spinner).
23
5
  */
24
- export default function createSpinner(options?: Options): Spinner;
25
- export type ForegroundColors = "black" | "blackBright" | "blue" | "blueBright" | "cyan" | "cyanBright" | "gray" | "green" | "greenBright" | "grey" | "magenta" | "magentaBright" | "red" | "redBright" | "white" | "whiteBright" | "yellow" | "yellowBright";
26
- export type SpinnerStyle = {
27
- frames: string[];
28
- interval: number;
29
- };
6
+ import type { WriteStream } from 'node:tty';
7
+ import { type SpinnerStyle } from './icons.ts';
8
+ type ForegroundColors = 'black' | 'blackBright' | 'blue' | 'blueBright' | 'cyan' | 'cyanBright' | 'gray' | 'green' | 'greenBright' | 'grey' | 'magenta' | 'magentaBright' | 'red' | 'redBright' | 'white' | 'whiteBright' | 'yellow' | 'yellowBright';
30
9
  /**
31
10
  * Spinner options.
32
11
  */
33
- export type Options = {
12
+ interface Options {
34
13
  /**
35
- * Text to display next to the spinner. (default: `''`)
14
+ * Text to display next to the spinner.
15
+ * @default ''
36
16
  */
37
17
  text?: string | undefined;
38
18
  /**
39
- * The color of the spinner. (default: `'yellow'`)
19
+ * The color of the spinner.
20
+ * @default 'yellow'
40
21
  */
41
22
  color?: ForegroundColors | undefined;
42
23
  /**
43
- * The stream to which the spinner is written. (default: `process.stderr`)
24
+ * The stream to which the spinner is written.
25
+ * @default process.stderr
44
26
  */
45
- stream?: NodeJS.WriteStream | undefined;
27
+ stream?: WriteStream | undefined;
46
28
  /**
47
- * Whether the spinner should be interactive. (default: Auto-detected)
29
+ * Whether the spinner should be interactive.
30
+ * @default Auto-detected
48
31
  */
49
32
  isInteractive?: boolean | undefined;
50
33
  /**
51
34
  * Customize the spinner animation with a custom set of frames and interval.
52
35
  *
53
- * ```
36
+ * Pass in any spinner from [`cli-spinners`](https://github.com/sindresorhus/cli-spinners).
37
+ *
38
+ * @example
54
39
  * {
55
- * frames: ['-', '\\', '|', '/'],
56
- * interval: 100,
40
+ * frames: ['-', '\\', '|', '/'],
41
+ * interval: 100,
57
42
  * }
58
- * ```
59
- *
60
- * Pass in any spinner from [`cli-spinners`](https://github.com/sindresorhus/cli-spinners).
61
43
  */
62
44
  spinner?: SpinnerStyle | undefined;
63
- };
64
- /**
65
- * @typedef {"black"|"blackBright"|"blue"|"blueBright"|"cyan"|"cyanBright"|"gray"|"green"|"greenBright"|"grey"|"magenta"|"magentaBright"|"red"|"redBright"|"white"|"whiteBright"|"yellow"|"yellowBright"} ForegroundColors
66
- */
67
- /**
68
- * @typedef {object} SpinnerStyle
69
- * @property {string[]} frames
70
- * @property {number} interval
71
- */
72
- /**
73
- * @typedef {object} Options Spinner options.
74
- * @property {string | undefined} [text] Text to display next to the spinner. (default: `''`)
75
- * @property {ForegroundColors | undefined} [color] The color of the spinner. (default: `'yellow'`)
76
- * @property {NodeJS.WriteStream | undefined} [stream] The stream to which the spinner is written. (default: `process.stderr`)
77
- * @property {boolean | undefined} [isInteractive] Whether the spinner should be interactive. (default: Auto-detected)
78
- * @property {SpinnerStyle | undefined} [spinner]
79
- * Customize the spinner animation with a custom set of frames and interval.
80
- *
81
- * ```
82
- * {
83
- * frames: ['-', '\\', '|', '/'],
84
- * interval: 100,
85
- * }
86
- * ```
87
- *
88
- * Pass in any spinner from [`cli-spinners`](https://github.com/sindresorhus/cli-spinners).
89
- */
45
+ }
90
46
  declare class Spinner {
91
- /** @param {Options} options */
47
+ #private;
92
48
  constructor(options?: Options);
93
49
  /**
94
50
  * Starts the spinner.
95
51
  *
96
52
  * Optionally, updates the text.
97
53
  *
98
- * @param {string} [text] The text to display next to the spinner.
54
+ * @param text The text to display next to the spinner.
99
55
  * @returns The spinner instance.
100
56
  */
101
57
  start(text?: string): this;
@@ -104,35 +60,35 @@ declare class Spinner {
104
60
  *
105
61
  * Optionally displays a final message.
106
62
  *
107
- * @param {string} [finalText] The final text to display after stopping the spinner.
63
+ * @param finalText The final text to display after stopping the spinner.
108
64
  * @returns The spinner instance.
109
65
  */
110
66
  stop(finalText?: string): this;
111
67
  /**
112
68
  * Stops the spinner and displays a success symbol with the message.
113
69
  *
114
- * @param {string} [text] The success message to display.
70
+ * @param text The success message to display.
115
71
  * @returns The spinner instance.
116
72
  */
117
73
  success(text?: string): this;
118
74
  /**
119
75
  * Stops the spinner and displays an error symbol with the message.
120
76
  *
121
- * @param {string} [text] The error message to display.
77
+ * @param text The error message to display.
122
78
  * @returns The spinner instance.
123
79
  */
124
80
  error(text?: string): this;
125
81
  /**
126
82
  * Stops the spinner and displays a warning symbol with the message.
127
83
  *
128
- * @param {string} [text] The warning message to display.
84
+ * @param text The warning message to display.
129
85
  * @returns The spinner instance.
130
86
  */
131
87
  warning(text?: string): this;
132
88
  /**
133
89
  * Stops the spinner and displays an info symbol with the message.
134
90
  *
135
- * @param {string} [text] The info message to display.
91
+ * @param text The info message to display.
136
92
  * @returns The spinner instance.
137
93
  */
138
94
  info(text?: string): this;
@@ -148,26 +104,49 @@ declare class Spinner {
148
104
  * @example
149
105
  * spinner.text = 'New text';
150
106
  */
151
- set text(value: string);
107
+ get text(): string;
152
108
  /**
153
109
  * Change the text displayed next to the spinner.
154
110
  *
155
111
  * @example
156
112
  * spinner.text = 'New text';
157
113
  */
158
- get text(): string;
114
+ set text(value: string);
159
115
  /**
160
116
  * Change the spinner color.
161
117
  */
162
- set color(value: ForegroundColors);
118
+ get color(): ForegroundColors;
163
119
  /**
164
120
  * Change the spinner color.
165
121
  */
166
- get color(): ForegroundColors;
122
+ set color(value: ForegroundColors);
167
123
  /**
168
124
  * Returns whether the spinner is currently spinning.
169
125
  */
170
126
  get isSpinning(): boolean;
171
- #private;
172
127
  }
128
+ /**
129
+ * Create a new spinner instance.
130
+ *
131
+ * @param options The spinner options.
132
+ * @returns A new spinner instance.
133
+ *
134
+ * @example
135
+ * import createSpinner from 'bananass-utils-console/spinner';
136
+ *
137
+ * const spinner = createSpinner({
138
+ * text: 'Loading…'
139
+ * color: 'yellow',
140
+ * stream: process.stderr,
141
+ * spinner: {
142
+ * frames: ['-', '\\', '|', '/'],
143
+ * interval: 100,
144
+ * },
145
+ * }).start();
146
+ *
147
+ * setTimeout(() => {
148
+ * spinner.success('Success!');
149
+ * }, 2000);
150
+ */
151
+ export default function createSpinner(options?: Options): Spinner;
173
152
  export {};
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @fileoverview Console spinner.
3
+ * @module bananass-utils-console/spinner
4
+ * @license MIT Portions of this code were borrowed from [`yocto-spinner`](https://github.com/sindresorhus/yocto-spinner).
5
+ */
6
+ // --------------------------------------------------------------------------------
7
+ // Import
8
+ // --------------------------------------------------------------------------------
9
+ import { styleText } from 'node:util';
10
+ import { successIcon, errorIcon, warningIcon, infoIcon, defaultSpinner, } from "./icons.js";
11
+ import isInteractive from "./is-interactive.js";
12
+ // --------------------------------------------------------------------------------
13
+ // Class
14
+ // --------------------------------------------------------------------------------
15
+ class Spinner {
16
+ // ------------------------------------------------------------------------------
17
+ // Private Properties
18
+ // ------------------------------------------------------------------------------
19
+ #frames;
20
+ #interval;
21
+ #currentFrame = -1;
22
+ #timer = undefined;
23
+ #text;
24
+ #stream;
25
+ #color;
26
+ #lines = 0;
27
+ #isInteractive;
28
+ #exitHandlerBound;
29
+ #lastSpinnerFrameTime = 0;
30
+ constructor(options = {}) {
31
+ const spinner = options.spinner ?? defaultSpinner;
32
+ this.#frames = spinner.frames;
33
+ this.#interval = spinner.interval;
34
+ this.#text = options.text ?? '';
35
+ this.#stream = options.stream ?? process.stderr;
36
+ this.#color = options.color ?? 'yellow';
37
+ this.#isInteractive = options.isInteractive ?? isInteractive(this.#stream);
38
+ this.#exitHandlerBound = this.#exitHandler.bind(this);
39
+ }
40
+ // ------------------------------------------------------------------------------
41
+ // Private Methods
42
+ // ------------------------------------------------------------------------------
43
+ #symbolStop(symbol, text = this.#text) {
44
+ return this.stop(`${symbol} ${text}`);
45
+ }
46
+ #render() {
47
+ const currentTime = Date.now();
48
+ // Ensure we only update the spinner frame at the wanted interval, even if the render method is called more often.
49
+ if (this.#currentFrame === -1 ||
50
+ currentTime - this.#lastSpinnerFrameTime >= this.#interval) {
51
+ this.#currentFrame = (this.#currentFrame + 1) % this.#frames.length;
52
+ this.#lastSpinnerFrameTime = currentTime;
53
+ }
54
+ const color = this.#color ?? 'yellow';
55
+ const frame = this.#frames[this.#currentFrame];
56
+ let string = `${styleText(color, frame)} ${this.#text}`;
57
+ if (!this.#isInteractive) {
58
+ string += '\n';
59
+ }
60
+ this.clear();
61
+ this.#write(string);
62
+ if (this.#isInteractive) {
63
+ this.#lines = this.#lineCount(string);
64
+ }
65
+ }
66
+ #write(text) {
67
+ this.#stream.write(text);
68
+ }
69
+ #lineCount(text) {
70
+ const width = this.#stream.columns ?? 80;
71
+ const lines = text.split('\n');
72
+ let lineCount = 0;
73
+ for (const line of lines) {
74
+ lineCount += Math.max(1, Math.ceil(line.length / width));
75
+ }
76
+ return lineCount;
77
+ }
78
+ #hideCursor() {
79
+ if (this.#isInteractive) {
80
+ this.#write('\u001B[?25l');
81
+ }
82
+ }
83
+ #showCursor() {
84
+ if (this.#isInteractive) {
85
+ this.#write('\u001B[?25h');
86
+ }
87
+ }
88
+ #subscribeToProcessEvents() {
89
+ process.once('SIGINT', this.#exitHandlerBound);
90
+ process.once('SIGTERM', this.#exitHandlerBound);
91
+ }
92
+ #unsubscribeFromProcessEvents() {
93
+ process.off('SIGINT', this.#exitHandlerBound);
94
+ process.off('SIGTERM', this.#exitHandlerBound);
95
+ }
96
+ #exitHandler(signal) {
97
+ if (this.isSpinning) {
98
+ this.stop();
99
+ }
100
+ // SIGINT: 128 + 2, SIGTERM: 128 + 15
101
+ const exitCode = signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1;
102
+ process.exit(exitCode);
103
+ }
104
+ // ------------------------------------------------------------------------------
105
+ // Public Methods
106
+ // ------------------------------------------------------------------------------
107
+ /**
108
+ * Starts the spinner.
109
+ *
110
+ * Optionally, updates the text.
111
+ *
112
+ * @param text The text to display next to the spinner.
113
+ * @returns The spinner instance.
114
+ */
115
+ start(text) {
116
+ if (text) {
117
+ this.#text = text;
118
+ }
119
+ if (this.isSpinning) {
120
+ return this;
121
+ }
122
+ this.#hideCursor();
123
+ this.#render();
124
+ this.#subscribeToProcessEvents();
125
+ this.#timer = setInterval(() => {
126
+ this.#render();
127
+ }, this.#interval);
128
+ return this;
129
+ }
130
+ /**
131
+ * Stops the spinner.
132
+ *
133
+ * Optionally displays a final message.
134
+ *
135
+ * @param finalText The final text to display after stopping the spinner.
136
+ * @returns The spinner instance.
137
+ */
138
+ stop(finalText) {
139
+ if (!this.isSpinning) {
140
+ return this;
141
+ }
142
+ clearInterval(this.#timer);
143
+ this.#timer = undefined;
144
+ this.#showCursor();
145
+ this.clear();
146
+ this.#unsubscribeFromProcessEvents();
147
+ if (finalText) {
148
+ this.#stream.write(`${finalText}\n`);
149
+ }
150
+ return this;
151
+ }
152
+ /**
153
+ * Stops the spinner and displays a success symbol with the message.
154
+ *
155
+ * @param text The success message to display.
156
+ * @returns The spinner instance.
157
+ */
158
+ success(text) {
159
+ return this.#symbolStop(successIcon, text);
160
+ }
161
+ /**
162
+ * Stops the spinner and displays an error symbol with the message.
163
+ *
164
+ * @param text The error message to display.
165
+ * @returns The spinner instance.
166
+ */
167
+ error(text) {
168
+ return this.#symbolStop(errorIcon, text);
169
+ }
170
+ /**
171
+ * Stops the spinner and displays a warning symbol with the message.
172
+ *
173
+ * @param text The warning message to display.
174
+ * @returns The spinner instance.
175
+ */
176
+ warning(text) {
177
+ return this.#symbolStop(warningIcon, text);
178
+ }
179
+ /**
180
+ * Stops the spinner and displays an info symbol with the message.
181
+ *
182
+ * @param text The info message to display.
183
+ * @returns The spinner instance.
184
+ */
185
+ info(text) {
186
+ return this.#symbolStop(infoIcon, text);
187
+ }
188
+ /**
189
+ * Clears the spinner.
190
+ *
191
+ * @returns The spinner instance.
192
+ */
193
+ clear() {
194
+ if (!this.#isInteractive) {
195
+ return this;
196
+ }
197
+ this.#stream.cursorTo(0);
198
+ for (let index = 0; index < this.#lines; index++) {
199
+ if (index > 0) {
200
+ this.#stream.moveCursor(0, -1);
201
+ }
202
+ this.#stream.clearLine(1);
203
+ }
204
+ this.#lines = 0;
205
+ return this;
206
+ }
207
+ // ------------------------------------------------------------------------------
208
+ // Getters and Setters
209
+ // ------------------------------------------------------------------------------
210
+ /**
211
+ * Change the text displayed next to the spinner.
212
+ *
213
+ * @example
214
+ * spinner.text = 'New text';
215
+ */
216
+ get text() {
217
+ return this.#text;
218
+ }
219
+ /**
220
+ * Change the text displayed next to the spinner.
221
+ *
222
+ * @example
223
+ * spinner.text = 'New text';
224
+ */
225
+ set text(value) {
226
+ this.#text = value ?? '';
227
+ this.#render();
228
+ }
229
+ /**
230
+ * Change the spinner color.
231
+ */
232
+ get color() {
233
+ return this.#color;
234
+ }
235
+ /**
236
+ * Change the spinner color.
237
+ */
238
+ set color(value) {
239
+ this.#color = value;
240
+ this.#render();
241
+ }
242
+ /**
243
+ * Returns whether the spinner is currently spinning.
244
+ */
245
+ get isSpinning() {
246
+ return this.#timer !== undefined;
247
+ }
248
+ }
249
+ // --------------------------------------------------------------------------------
250
+ // Export
251
+ // --------------------------------------------------------------------------------
252
+ /**
253
+ * Create a new spinner instance.
254
+ *
255
+ * @param options The spinner options.
256
+ * @returns A new spinner instance.
257
+ *
258
+ * @example
259
+ * import createSpinner from 'bananass-utils-console/spinner';
260
+ *
261
+ * const spinner = createSpinner({
262
+ * text: 'Loading…'
263
+ * color: 'yellow',
264
+ * stream: process.stderr,
265
+ * spinner: {
266
+ * frames: ['-', '\\', '|', '/'],
267
+ * interval: 100,
268
+ * },
269
+ * }).start();
270
+ *
271
+ * setTimeout(() => {
272
+ * spinner.success('Success!');
273
+ * }, 2000);
274
+ */
275
+ export default function createSpinner(options) {
276
+ return new Spinner(options);
277
+ }