[6a3a178] | 1 | 'use strict';
|
---|
| 2 | const readline = require('readline');
|
---|
| 3 | const chalk = require('chalk');
|
---|
| 4 | const cliCursor = require('cli-cursor');
|
---|
| 5 | const cliSpinners = require('cli-spinners');
|
---|
| 6 | const logSymbols = require('log-symbols');
|
---|
| 7 | const stripAnsi = require('strip-ansi');
|
---|
| 8 | const wcwidth = require('wcwidth');
|
---|
| 9 | const isInteractive = require('is-interactive');
|
---|
| 10 | const isUnicodeSupported = require('is-unicode-supported');
|
---|
| 11 | const {BufferListStream} = require('bl');
|
---|
| 12 |
|
---|
| 13 | const TEXT = Symbol('text');
|
---|
| 14 | const PREFIX_TEXT = Symbol('prefixText');
|
---|
| 15 | const ASCII_ETX_CODE = 0x03; // Ctrl+C emits this code
|
---|
| 16 |
|
---|
| 17 | class StdinDiscarder {
|
---|
| 18 | constructor() {
|
---|
| 19 | this.requests = 0;
|
---|
| 20 |
|
---|
| 21 | this.mutedStream = new BufferListStream();
|
---|
| 22 | this.mutedStream.pipe(process.stdout);
|
---|
| 23 |
|
---|
| 24 | const self = this; // eslint-disable-line unicorn/no-this-assignment
|
---|
| 25 | this.ourEmit = function (event, data, ...args) {
|
---|
| 26 | const {stdin} = process;
|
---|
| 27 | if (self.requests > 0 || stdin.emit === self.ourEmit) {
|
---|
| 28 | if (event === 'keypress') { // Fixes readline behavior
|
---|
| 29 | return;
|
---|
| 30 | }
|
---|
| 31 |
|
---|
| 32 | if (event === 'data' && data.includes(ASCII_ETX_CODE)) {
|
---|
| 33 | process.emit('SIGINT');
|
---|
| 34 | }
|
---|
| 35 |
|
---|
| 36 | Reflect.apply(self.oldEmit, this, [event, data, ...args]);
|
---|
| 37 | } else {
|
---|
| 38 | Reflect.apply(process.stdin.emit, this, [event, data, ...args]);
|
---|
| 39 | }
|
---|
| 40 | };
|
---|
| 41 | }
|
---|
| 42 |
|
---|
| 43 | start() {
|
---|
| 44 | this.requests++;
|
---|
| 45 |
|
---|
| 46 | if (this.requests === 1) {
|
---|
| 47 | this.realStart();
|
---|
| 48 | }
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | stop() {
|
---|
| 52 | if (this.requests <= 0) {
|
---|
| 53 | throw new Error('`stop` called more times than `start`');
|
---|
| 54 | }
|
---|
| 55 |
|
---|
| 56 | this.requests--;
|
---|
| 57 |
|
---|
| 58 | if (this.requests === 0) {
|
---|
| 59 | this.realStop();
|
---|
| 60 | }
|
---|
| 61 | }
|
---|
| 62 |
|
---|
| 63 | realStart() {
|
---|
| 64 | // No known way to make it work reliably on Windows
|
---|
| 65 | if (process.platform === 'win32') {
|
---|
| 66 | return;
|
---|
| 67 | }
|
---|
| 68 |
|
---|
| 69 | this.rl = readline.createInterface({
|
---|
| 70 | input: process.stdin,
|
---|
| 71 | output: this.mutedStream
|
---|
| 72 | });
|
---|
| 73 |
|
---|
| 74 | this.rl.on('SIGINT', () => {
|
---|
| 75 | if (process.listenerCount('SIGINT') === 0) {
|
---|
| 76 | process.emit('SIGINT');
|
---|
| 77 | } else {
|
---|
| 78 | this.rl.close();
|
---|
| 79 | process.kill(process.pid, 'SIGINT');
|
---|
| 80 | }
|
---|
| 81 | });
|
---|
| 82 | }
|
---|
| 83 |
|
---|
| 84 | realStop() {
|
---|
| 85 | if (process.platform === 'win32') {
|
---|
| 86 | return;
|
---|
| 87 | }
|
---|
| 88 |
|
---|
| 89 | this.rl.close();
|
---|
| 90 | this.rl = undefined;
|
---|
| 91 | }
|
---|
| 92 | }
|
---|
| 93 |
|
---|
| 94 | let stdinDiscarder;
|
---|
| 95 |
|
---|
| 96 | class Ora {
|
---|
| 97 | constructor(options) {
|
---|
| 98 | if (!stdinDiscarder) {
|
---|
| 99 | stdinDiscarder = new StdinDiscarder();
|
---|
| 100 | }
|
---|
| 101 |
|
---|
| 102 | if (typeof options === 'string') {
|
---|
| 103 | options = {
|
---|
| 104 | text: options
|
---|
| 105 | };
|
---|
| 106 | }
|
---|
| 107 |
|
---|
| 108 | this.options = {
|
---|
| 109 | text: '',
|
---|
| 110 | color: 'cyan',
|
---|
| 111 | stream: process.stderr,
|
---|
| 112 | discardStdin: true,
|
---|
| 113 | ...options
|
---|
| 114 | };
|
---|
| 115 |
|
---|
| 116 | this.spinner = this.options.spinner;
|
---|
| 117 |
|
---|
| 118 | this.color = this.options.color;
|
---|
| 119 | this.hideCursor = this.options.hideCursor !== false;
|
---|
| 120 | this.interval = this.options.interval || this.spinner.interval || 100;
|
---|
| 121 | this.stream = this.options.stream;
|
---|
| 122 | this.id = undefined;
|
---|
| 123 | this.isEnabled = typeof this.options.isEnabled === 'boolean' ? this.options.isEnabled : isInteractive({stream: this.stream});
|
---|
| 124 | this.isSilent = typeof this.options.isSilent === 'boolean' ? this.options.isSilent : false;
|
---|
| 125 |
|
---|
| 126 | // Set *after* `this.stream`
|
---|
| 127 | this.text = this.options.text;
|
---|
| 128 | this.prefixText = this.options.prefixText;
|
---|
| 129 | this.linesToClear = 0;
|
---|
| 130 | this.indent = this.options.indent;
|
---|
| 131 | this.discardStdin = this.options.discardStdin;
|
---|
| 132 | this.isDiscardingStdin = false;
|
---|
| 133 | }
|
---|
| 134 |
|
---|
| 135 | get indent() {
|
---|
| 136 | return this._indent;
|
---|
| 137 | }
|
---|
| 138 |
|
---|
| 139 | set indent(indent = 0) {
|
---|
| 140 | if (!(indent >= 0 && Number.isInteger(indent))) {
|
---|
| 141 | throw new Error('The `indent` option must be an integer from 0 and up');
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | this._indent = indent;
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | _updateInterval(interval) {
|
---|
| 148 | if (interval !== undefined) {
|
---|
| 149 | this.interval = interval;
|
---|
| 150 | }
|
---|
| 151 | }
|
---|
| 152 |
|
---|
| 153 | get spinner() {
|
---|
| 154 | return this._spinner;
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | set spinner(spinner) {
|
---|
| 158 | this.frameIndex = 0;
|
---|
| 159 |
|
---|
| 160 | if (typeof spinner === 'object') {
|
---|
| 161 | if (spinner.frames === undefined) {
|
---|
| 162 | throw new Error('The given spinner must have a `frames` property');
|
---|
| 163 | }
|
---|
| 164 |
|
---|
| 165 | this._spinner = spinner;
|
---|
| 166 | } else if (!isUnicodeSupported()) {
|
---|
| 167 | this._spinner = cliSpinners.line;
|
---|
| 168 | } else if (spinner === undefined) {
|
---|
| 169 | // Set default spinner
|
---|
| 170 | this._spinner = cliSpinners.dots;
|
---|
| 171 | } else if (spinner !== 'default' && cliSpinners[spinner]) {
|
---|
| 172 | this._spinner = cliSpinners[spinner];
|
---|
| 173 | } else {
|
---|
| 174 | throw new Error(`There is no built-in spinner named '${spinner}'. See https://github.com/sindresorhus/cli-spinners/blob/main/spinners.json for a full list.`);
|
---|
| 175 | }
|
---|
| 176 |
|
---|
| 177 | this._updateInterval(this._spinner.interval);
|
---|
| 178 | }
|
---|
| 179 |
|
---|
| 180 | get text() {
|
---|
| 181 | return this[TEXT];
|
---|
| 182 | }
|
---|
| 183 |
|
---|
| 184 | set text(value) {
|
---|
| 185 | this[TEXT] = value;
|
---|
| 186 | this.updateLineCount();
|
---|
| 187 | }
|
---|
| 188 |
|
---|
| 189 | get prefixText() {
|
---|
| 190 | return this[PREFIX_TEXT];
|
---|
| 191 | }
|
---|
| 192 |
|
---|
| 193 | set prefixText(value) {
|
---|
| 194 | this[PREFIX_TEXT] = value;
|
---|
| 195 | this.updateLineCount();
|
---|
| 196 | }
|
---|
| 197 |
|
---|
| 198 | get isSpinning() {
|
---|
| 199 | return this.id !== undefined;
|
---|
| 200 | }
|
---|
| 201 |
|
---|
| 202 | getFullPrefixText(prefixText = this[PREFIX_TEXT], postfix = ' ') {
|
---|
| 203 | if (typeof prefixText === 'string') {
|
---|
| 204 | return prefixText + postfix;
|
---|
| 205 | }
|
---|
| 206 |
|
---|
| 207 | if (typeof prefixText === 'function') {
|
---|
| 208 | return prefixText() + postfix;
|
---|
| 209 | }
|
---|
| 210 |
|
---|
| 211 | return '';
|
---|
| 212 | }
|
---|
| 213 |
|
---|
| 214 | updateLineCount() {
|
---|
| 215 | const columns = this.stream.columns || 80;
|
---|
| 216 | const fullPrefixText = this.getFullPrefixText(this.prefixText, '-');
|
---|
| 217 | this.lineCount = 0;
|
---|
| 218 | for (const line of stripAnsi(fullPrefixText + '--' + this[TEXT]).split('\n')) {
|
---|
| 219 | this.lineCount += Math.max(1, Math.ceil(wcwidth(line) / columns));
|
---|
| 220 | }
|
---|
| 221 | }
|
---|
| 222 |
|
---|
| 223 | get isEnabled() {
|
---|
| 224 | return this._isEnabled && !this.isSilent;
|
---|
| 225 | }
|
---|
| 226 |
|
---|
| 227 | set isEnabled(value) {
|
---|
| 228 | if (typeof value !== 'boolean') {
|
---|
| 229 | throw new TypeError('The `isEnabled` option must be a boolean');
|
---|
| 230 | }
|
---|
| 231 |
|
---|
| 232 | this._isEnabled = value;
|
---|
| 233 | }
|
---|
| 234 |
|
---|
| 235 | get isSilent() {
|
---|
| 236 | return this._isSilent;
|
---|
| 237 | }
|
---|
| 238 |
|
---|
| 239 | set isSilent(value) {
|
---|
| 240 | if (typeof value !== 'boolean') {
|
---|
| 241 | throw new TypeError('The `isSilent` option must be a boolean');
|
---|
| 242 | }
|
---|
| 243 |
|
---|
| 244 | this._isSilent = value;
|
---|
| 245 | }
|
---|
| 246 |
|
---|
| 247 | frame() {
|
---|
| 248 | const {frames} = this.spinner;
|
---|
| 249 | let frame = frames[this.frameIndex];
|
---|
| 250 |
|
---|
| 251 | if (this.color) {
|
---|
| 252 | frame = chalk[this.color](frame);
|
---|
| 253 | }
|
---|
| 254 |
|
---|
| 255 | this.frameIndex = ++this.frameIndex % frames.length;
|
---|
| 256 | const fullPrefixText = (typeof this.prefixText === 'string' && this.prefixText !== '') ? this.prefixText + ' ' : '';
|
---|
| 257 | const fullText = typeof this.text === 'string' ? ' ' + this.text : '';
|
---|
| 258 |
|
---|
| 259 | return fullPrefixText + frame + fullText;
|
---|
| 260 | }
|
---|
| 261 |
|
---|
| 262 | clear() {
|
---|
| 263 | if (!this.isEnabled || !this.stream.isTTY) {
|
---|
| 264 | return this;
|
---|
| 265 | }
|
---|
| 266 |
|
---|
| 267 | for (let i = 0; i < this.linesToClear; i++) {
|
---|
| 268 | if (i > 0) {
|
---|
| 269 | this.stream.moveCursor(0, -1);
|
---|
| 270 | }
|
---|
| 271 |
|
---|
| 272 | this.stream.clearLine();
|
---|
| 273 | this.stream.cursorTo(this.indent);
|
---|
| 274 | }
|
---|
| 275 |
|
---|
| 276 | this.linesToClear = 0;
|
---|
| 277 |
|
---|
| 278 | return this;
|
---|
| 279 | }
|
---|
| 280 |
|
---|
| 281 | render() {
|
---|
| 282 | if (this.isSilent) {
|
---|
| 283 | return this;
|
---|
| 284 | }
|
---|
| 285 |
|
---|
| 286 | this.clear();
|
---|
| 287 | this.stream.write(this.frame());
|
---|
| 288 | this.linesToClear = this.lineCount;
|
---|
| 289 |
|
---|
| 290 | return this;
|
---|
| 291 | }
|
---|
| 292 |
|
---|
| 293 | start(text) {
|
---|
| 294 | if (text) {
|
---|
| 295 | this.text = text;
|
---|
| 296 | }
|
---|
| 297 |
|
---|
| 298 | if (this.isSilent) {
|
---|
| 299 | return this;
|
---|
| 300 | }
|
---|
| 301 |
|
---|
| 302 | if (!this.isEnabled) {
|
---|
| 303 | if (this.text) {
|
---|
| 304 | this.stream.write(`- ${this.text}\n`);
|
---|
| 305 | }
|
---|
| 306 |
|
---|
| 307 | return this;
|
---|
| 308 | }
|
---|
| 309 |
|
---|
| 310 | if (this.isSpinning) {
|
---|
| 311 | return this;
|
---|
| 312 | }
|
---|
| 313 |
|
---|
| 314 | if (this.hideCursor) {
|
---|
| 315 | cliCursor.hide(this.stream);
|
---|
| 316 | }
|
---|
| 317 |
|
---|
| 318 | if (this.discardStdin && process.stdin.isTTY) {
|
---|
| 319 | this.isDiscardingStdin = true;
|
---|
| 320 | stdinDiscarder.start();
|
---|
| 321 | }
|
---|
| 322 |
|
---|
| 323 | this.render();
|
---|
| 324 | this.id = setInterval(this.render.bind(this), this.interval);
|
---|
| 325 |
|
---|
| 326 | return this;
|
---|
| 327 | }
|
---|
| 328 |
|
---|
| 329 | stop() {
|
---|
| 330 | if (!this.isEnabled) {
|
---|
| 331 | return this;
|
---|
| 332 | }
|
---|
| 333 |
|
---|
| 334 | clearInterval(this.id);
|
---|
| 335 | this.id = undefined;
|
---|
| 336 | this.frameIndex = 0;
|
---|
| 337 | this.clear();
|
---|
| 338 | if (this.hideCursor) {
|
---|
| 339 | cliCursor.show(this.stream);
|
---|
| 340 | }
|
---|
| 341 |
|
---|
| 342 | if (this.discardStdin && process.stdin.isTTY && this.isDiscardingStdin) {
|
---|
| 343 | stdinDiscarder.stop();
|
---|
| 344 | this.isDiscardingStdin = false;
|
---|
| 345 | }
|
---|
| 346 |
|
---|
| 347 | return this;
|
---|
| 348 | }
|
---|
| 349 |
|
---|
| 350 | succeed(text) {
|
---|
| 351 | return this.stopAndPersist({symbol: logSymbols.success, text});
|
---|
| 352 | }
|
---|
| 353 |
|
---|
| 354 | fail(text) {
|
---|
| 355 | return this.stopAndPersist({symbol: logSymbols.error, text});
|
---|
| 356 | }
|
---|
| 357 |
|
---|
| 358 | warn(text) {
|
---|
| 359 | return this.stopAndPersist({symbol: logSymbols.warning, text});
|
---|
| 360 | }
|
---|
| 361 |
|
---|
| 362 | info(text) {
|
---|
| 363 | return this.stopAndPersist({symbol: logSymbols.info, text});
|
---|
| 364 | }
|
---|
| 365 |
|
---|
| 366 | stopAndPersist(options = {}) {
|
---|
| 367 | if (this.isSilent) {
|
---|
| 368 | return this;
|
---|
| 369 | }
|
---|
| 370 |
|
---|
| 371 | const prefixText = options.prefixText || this.prefixText;
|
---|
| 372 | const text = options.text || this.text;
|
---|
| 373 | const fullText = (typeof text === 'string') ? ' ' + text : '';
|
---|
| 374 |
|
---|
| 375 | this.stop();
|
---|
| 376 | this.stream.write(`${this.getFullPrefixText(prefixText, ' ')}${options.symbol || ' '}${fullText}\n`);
|
---|
| 377 |
|
---|
| 378 | return this;
|
---|
| 379 | }
|
---|
| 380 | }
|
---|
| 381 |
|
---|
| 382 | const oraFactory = function (options) {
|
---|
| 383 | return new Ora(options);
|
---|
| 384 | };
|
---|
| 385 |
|
---|
| 386 | module.exports = oraFactory;
|
---|
| 387 |
|
---|
| 388 | module.exports.promise = (action, options) => {
|
---|
| 389 | // eslint-disable-next-line promise/prefer-await-to-then
|
---|
| 390 | if (typeof action.then !== 'function') {
|
---|
| 391 | throw new TypeError('Parameter `action` must be a Promise');
|
---|
| 392 | }
|
---|
| 393 |
|
---|
| 394 | const spinner = new Ora(options);
|
---|
| 395 | spinner.start();
|
---|
| 396 |
|
---|
| 397 | (async () => {
|
---|
| 398 | try {
|
---|
| 399 | await action;
|
---|
| 400 | spinner.succeed();
|
---|
| 401 | } catch {
|
---|
| 402 | spinner.fail();
|
---|
| 403 | }
|
---|
| 404 | })();
|
---|
| 405 |
|
---|
| 406 | return spinner;
|
---|
| 407 | };
|
---|