+1
-1
@@ -40,3 +40,3 @@ import {type SpinnerName} from 'cli-spinners'; | ||
| /** | ||
| The name of one of the provided spinners. See [`example.js`](https://github.com/BendingBender/ora/blob/main/example.js) in this repo if you want to test out different spinners. On Windows (expect for Windows Terminal), it will always use the line spinner as the Windows command-line doesn't have proper Unicode support. | ||
| The name of one of the provided spinners. See `example.js` in this repo if you want to test out different spinners. On Windows (except for Windows Terminal), it will always use the line spinner as the Windows command-line doesn't have proper Unicode support. | ||
@@ -43,0 +43,0 @@ @default 'dots' |
+67
-48
@@ -18,2 +18,3 @@ import process from 'node:process'; | ||
| #lastSpinnerFrameTime = 0; | ||
| #lastIndent = 0; | ||
| #options; | ||
@@ -118,6 +119,10 @@ #spinner; | ||
| if (typeof spinner === 'object') { | ||
| if (spinner.frames === undefined) { | ||
| throw new Error('The given spinner must have a `frames` property'); | ||
| if (!Array.isArray(spinner.frames) || spinner.frames.length === 0 || spinner.frames.some(frame => typeof frame !== 'string')) { | ||
| throw new Error('The given spinner must have a non-empty `frames` array of strings'); | ||
| } | ||
| if (spinner.interval !== undefined && !(Number.isInteger(spinner.interval) && spinner.interval > 0)) { | ||
| throw new Error('`spinner.interval` must be a positive integer if provided'); | ||
| } | ||
| this.#spinner = spinner; | ||
@@ -167,24 +172,26 @@ } else if (!isUnicodeSupported()) { | ||
| #getFullPrefixText(prefixText = this.#prefixText, postfix = ' ') { | ||
| if (typeof prefixText === 'string' && prefixText !== '') { | ||
| return prefixText + postfix; | ||
| #formatAffix(value, separator, placeBefore = false) { | ||
| const resolved = typeof value === 'function' ? value() : value; | ||
| if (typeof resolved === 'string' && resolved !== '') { | ||
| return placeBefore ? (separator + resolved) : (resolved + separator); | ||
| } | ||
| if (typeof prefixText === 'function') { | ||
| return prefixText() + postfix; | ||
| } | ||
| return ''; | ||
| } | ||
| #getFullPrefixText(prefixText = this.#prefixText, postfix = ' ') { | ||
| return this.#formatAffix(prefixText, postfix, false); | ||
| } | ||
| #getFullSuffixText(suffixText = this.#suffixText, prefix = ' ') { | ||
| if (typeof suffixText === 'string' && suffixText !== '') { | ||
| return prefix + suffixText; | ||
| } | ||
| return this.#formatAffix(suffixText, prefix, true); | ||
| } | ||
| if (typeof suffixText === 'function') { | ||
| return prefix + suffixText(); | ||
| #computeLineCountFrom(text, columns) { | ||
| let count = 0; | ||
| for (const line of stripAnsi(text).split('\n')) { | ||
| count += Math.max(1, Math.ceil(stringWidth(line) / columns)); | ||
| } | ||
| return ''; | ||
| return count; | ||
| } | ||
@@ -194,10 +201,12 @@ | ||
| const columns = this.#stream.columns ?? 80; | ||
| const fullPrefixText = this.#getFullPrefixText(this.#prefixText, '-'); | ||
| const fullSuffixText = this.#getFullSuffixText(this.#suffixText, '-'); | ||
| const fullText = ' '.repeat(this.#indent) + fullPrefixText + '--' + this.#text + '--' + fullSuffixText; | ||
| this.#lineCount = 0; | ||
| for (const line of stripAnsi(fullText).split('\n')) { | ||
| this.#lineCount += Math.max(1, Math.ceil(stringWidth(line, {countAnsiEscapeCodes: true}) / columns)); | ||
| } | ||
| // Simple side-effect free approximation (do not call functions) | ||
| const prefixText = typeof this.#prefixText === 'function' ? '' : this.#prefixText; | ||
| const suffixText = typeof this.#suffixText === 'function' ? '' : this.#suffixText; | ||
| const fullPrefixText = (typeof prefixText === 'string' && prefixText !== '') ? prefixText + ' ' : ''; | ||
| const fullSuffixText = (typeof suffixText === 'string' && suffixText !== '') ? ' ' + suffixText : ''; | ||
| const spinnerChar = '-'; | ||
| const fullText = ' '.repeat(this.#indent) + fullPrefixText + spinnerChar + (typeof this.#text === 'string' ? ' ' + this.#text : '') + fullSuffixText; | ||
| this.#lineCount = this.#computeLineCountFrom(fullText, columns); | ||
| } | ||
@@ -245,5 +254,5 @@ | ||
| const fullPrefixText = (typeof this.#prefixText === 'string' && this.#prefixText !== '') ? this.#prefixText + ' ' : ''; | ||
| const fullPrefixText = this.#getFullPrefixText(this.#prefixText, ' '); | ||
| const fullText = typeof this.text === 'string' ? ' ' + this.text : ''; | ||
| const fullSuffixText = (typeof this.#suffixText === 'string' && this.#suffixText !== '') ? ' ' + this.#suffixText : ''; | ||
| const fullSuffixText = this.#getFullSuffixText(this.#suffixText, ' '); | ||
@@ -268,7 +277,7 @@ return fullPrefixText + frame + fullText + fullSuffixText; | ||
| if (this.#indent || this.lastIndent !== this.#indent) { | ||
| if (this.#indent || this.#lastIndent !== this.#indent) { | ||
| this.#stream.cursorTo(this.#indent); | ||
| } | ||
| this.lastIndent = this.#indent; | ||
| this.#lastIndent = this.#indent; | ||
| this.#linesToClear = 0; | ||
@@ -280,3 +289,3 @@ | ||
| render() { | ||
| if (this.#isSilent) { | ||
| if (!this.#isEnabled || this.#isSilent) { | ||
| return this; | ||
@@ -286,5 +295,18 @@ } | ||
| this.clear(); | ||
| this.#stream.write(this.frame()); | ||
| this.#linesToClear = this.#lineCount; | ||
| let frameContent = this.frame(); | ||
| const columns = this.#stream.columns ?? 80; | ||
| const actualLineCount = this.#computeLineCountFrom(frameContent, columns); | ||
| // If content would exceed viewport height, truncate it to prevent garbage | ||
| const consoleHeight = this.#stream.rows; | ||
| if (consoleHeight && consoleHeight > 1 && actualLineCount > consoleHeight) { | ||
| const lines = frameContent.split('\n'); | ||
| const maxLines = consoleHeight - 1; // Reserve one line for truncation message | ||
| frameContent = [...lines.slice(0, maxLines), '... (content truncated to fit terminal)'].join('\n'); | ||
| } | ||
| this.#stream.write(frameContent); | ||
| this.#linesToClear = this.#computeLineCountFrom(frameContent, columns); | ||
| return this; | ||
@@ -303,4 +325,6 @@ } | ||
| if (!this.#isEnabled) { | ||
| if (this.text) { | ||
| this.#stream.write(`- ${this.text}\n`); | ||
| const line = ' '.repeat(this.#indent) + this.#getFullPrefixText(this.#prefixText, ' ') + (this.text ? `- ${this.text}` : '') + this.#getFullSuffixText(this.#suffixText, ' '); | ||
| if (line.trim() !== '') { | ||
| this.#stream.write(line + '\n'); | ||
| } | ||
@@ -331,12 +355,11 @@ | ||
| stop() { | ||
| if (!this.#isEnabled) { | ||
| return this; | ||
| } | ||
| clearInterval(this.#id); | ||
| this.#id = undefined; | ||
| this.#frameIndex = 0; | ||
| this.clear(); | ||
| if (this.#options.hideCursor) { | ||
| cliCursor.show(this.#stream); | ||
| if (this.#isEnabled) { | ||
| this.clear(); | ||
| if (this.#options.hideCursor) { | ||
| cliCursor.show(this.#stream); | ||
| } | ||
| } | ||
@@ -416,15 +439,11 @@ | ||
| spinner.succeed( | ||
| successText === undefined | ||
| ? undefined | ||
| : (typeof successText === 'string' ? successText : successText(result)), | ||
| ); | ||
| spinner.succeed(successText === undefined | ||
| ? undefined | ||
| : (typeof successText === 'string' ? successText : successText(result))); | ||
| return result; | ||
| } catch (error) { | ||
| spinner.fail( | ||
| failText === undefined | ||
| ? undefined | ||
| : (typeof failText === 'string' ? failText : failText(error)), | ||
| ); | ||
| spinner.fail(failText === undefined | ||
| ? undefined | ||
| : (typeof failText === 'string' ? failText : failText(error))); | ||
@@ -431,0 +450,0 @@ throw error; |
+13
-13
| { | ||
| "name": "ora", | ||
| "version": "8.2.0", | ||
| "version": "9.0.0", | ||
| "description": "Elegant terminal spinner", | ||
@@ -20,6 +20,6 @@ "license": "MIT", | ||
| "engines": { | ||
| "node": ">=18" | ||
| "node": ">=20" | ||
| }, | ||
| "scripts": { | ||
| "test": "xo && ava && tsd" | ||
| "test": "xo && NODE_ENV=test node --test test.js && tsd" | ||
| }, | ||
@@ -47,20 +47,20 @@ "files": [ | ||
| "dependencies": { | ||
| "chalk": "^5.3.0", | ||
| "chalk": "^5.6.2", | ||
| "cli-cursor": "^5.0.0", | ||
| "cli-spinners": "^2.9.2", | ||
| "cli-spinners": "^3.2.0", | ||
| "is-interactive": "^2.0.0", | ||
| "is-unicode-supported": "^2.0.0", | ||
| "log-symbols": "^6.0.0", | ||
| "is-unicode-supported": "^2.1.0", | ||
| "log-symbols": "^7.0.1", | ||
| "stdin-discarder": "^0.2.2", | ||
| "string-width": "^7.2.0", | ||
| "strip-ansi": "^7.1.0" | ||
| "string-width": "^8.1.0", | ||
| "strip-ansi": "^7.1.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.5.0", | ||
| "ava": "^5.3.1", | ||
| "@types/node": "^24.5.0", | ||
| "ava": "^6.4.1", | ||
| "get-stream": "^9.0.1", | ||
| "transform-tty": "^1.0.11", | ||
| "tsd": "^0.31.1", | ||
| "xo": "^0.59.3" | ||
| "tsd": "^0.33.0", | ||
| "xo": "^1.2.2" | ||
| } | ||
| } |
+47
-1
@@ -83,3 +83,3 @@ # ora | ||
| The color of the spinner. | ||
| The color of the spinner. Set to `false` to disable coloring. | ||
@@ -316,2 +316,48 @@ ##### hideCursor | ||
| ### Can I display multiple spinners simultaneously? | ||
| No. Ora is designed to display a single spinner at a time. For multiple concurrent progress indicators, consider alternatives like [listr2](https://github.com/listr2/listr2) or [spinnies](https://github.com/jcarpanelli/spinnies). | ||
| ### Can I use Ora with [log-update](https://github.com/sindresorhus/log-update)? | ||
| Yes, use the `.frame()` method to get the current spinner frame and include it in your log-update output. | ||
| ### Does Ora work in Node.js Worker threads? | ||
| No. Ora requires an interactive terminal environment and Worker threads are not considered interactive, so the spinner will not animate. Run the spinner in the main thread and control it via worker messages: | ||
| ```js | ||
| // main.js | ||
| import {Worker} from 'node:worker_threads'; | ||
| import ora from 'ora'; | ||
| const spinner = ora().start(); | ||
| const worker = new Worker('./worker.js'); | ||
| worker.on('message', message => { | ||
| switch (message.type) { | ||
| case 'ora:text': | ||
| spinner.text = message.text; | ||
| break; | ||
| case 'ora:succeed': | ||
| spinner.succeed(message.text); | ||
| break; | ||
| case 'ora:fail': | ||
| spinner.fail(message.text); | ||
| break; | ||
| } | ||
| }); | ||
| ``` | ||
| ```js | ||
| // worker.js | ||
| import {parentPort} from 'node:worker_threads'; | ||
| parentPort.postMessage({type: 'ora:text', text: 'Working...'}); | ||
| // Do work... | ||
| parentPort.postMessage({type: 'ora:succeed', text: 'Done!'}); | ||
| ``` | ||
| ## Related | ||
@@ -318,0 +364,0 @@ |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
30350
10.31%595
2.06%376
13.94%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated