diff --git a/doc/api/util.md b/doc/api/util.md index fd93b52722934f..761b14621066c1 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2574,7 +2574,9 @@ changes: * `format` {string | Array} A text format or an Array of text formats defined in `util.inspect.colors`, or a hex color in `#RGB` or `#RRGGBB` form. -* `text` {string} The text to be formatted. +* `text` {string|number|boolean|bigint} The text to be formatted. + Non-string scalar values (`number`, `boolean`, `bigint`) are coerced + to a string internally via `String(text)`. * `options` {Object} * `validateStream` {boolean} When true, `stream` is checked to see if it can handle colors. **Default:** `true`. * `stream` {Stream} A stream that will be validated if it can be colored. **Default:** `process.stdout`. diff --git a/lib/util.js b/lib/util.js index e828229380d9e8..90104c0c6d04c6 100644 --- a/lib/util.js +++ b/lib/util.js @@ -39,6 +39,7 @@ const { ReflectApply, RegExpPrototypeExec, SafeMap, + String, StringPrototypeSlice, StringPrototypeToWellFormed, } = primordials; @@ -237,7 +238,7 @@ function rgbToAnsi24Bit(r, g, b) { /** * @param {string | string[]} format - * @param {string} text + * @param {string | number | boolean | bigint} text * @param {object} [options] * @param {boolean} [options.validateStream] - Whether to validate the stream. * @param {Stream} [options.stream] - The stream used for validation. @@ -246,6 +247,14 @@ function rgbToAnsi24Bit(r, g, b) { function styleText(format, text, options) { const validateStream = options?.validateStream ?? true; + // Coerce scalar values (number, boolean, bigint) to string. + // This avoids requiring callers to manually cast values like + // `String(count)` before passing them to styleText. + const textType = typeof text; + if (textType === 'number' || textType === 'boolean' || textType === 'bigint') { + text = String(text); + } + // Fast path: single format string with validateStream=false if (!validateStream && typeof format === 'string' && typeof text === 'string') { const cache = getStyleCache(); @@ -268,7 +277,9 @@ function styleText(format, text, options) { } } - validateString(text, 'text'); + if (textType !== 'number' && textType !== 'boolean' && textType !== 'bigint') { + validateString(text, 'text'); + } if (options !== undefined) { validateObject(options, 'options'); } diff --git a/test/parallel/test-util-styletext.js b/test/parallel/test-util-styletext.js index 3db01bec1c3acd..7586028d298d43 100644 --- a/test/parallel/test-util-styletext.js +++ b/test/parallel/test-util-styletext.js @@ -23,13 +23,39 @@ const noChange = 'test'; }, { code: 'ERR_INVALID_ARG_VALUE', }, invalidOption); +}); + +// Non-string scalars for `text` should NOT throw — they are coerced internally. +[ + null, + undefined, + Symbol(), + () => {}, + {}, +].forEach((invalidText) => { assert.throws(() => { - util.styleText('red', invalidOption); + util.styleText('red', invalidText); }, { code: 'ERR_INVALID_ARG_TYPE' - }, invalidOption); + }, invalidText); }); +// Scalar values are coerced to string (number, boolean, bigint). +assert.strictEqual( + util.styleText('red', 42, { validateStream: false }), + '\u001b[31m42\u001b[39m', +); + +assert.strictEqual( + util.styleText('red', true, { validateStream: false }), + '\u001b[31mtrue\u001b[39m', +); + +assert.strictEqual( + util.styleText('red', 3n, { validateStream: false }), + '\u001b[31m3\u001b[39m', +); + assert.throws(() => { util.styleText('invalid', 'text'); }, {