Skip to content

Commit e84812c

Browse files
RafaelGSStargos
authored andcommittedOct 2, 2024
lib: respect terminal capabilities on styleText
This PR changes styleText API to respect terminal capabilities and environment variables such as NO_COLOR, NODE_DISABLE_COLORS, and FORCE_COLOR. PR-URL: #54389 Fixes: #54365 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Claudio Wunder <cwunder@gnome.org> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 229e102 commit e84812c

File tree

4 files changed

+146
-13
lines changed

4 files changed

+146
-13
lines changed
 

‎doc/api/util.md

+37-6
Original file line numberDiff line numberDiff line change
@@ -1798,30 +1798,61 @@ console.log(util.stripVTControlCharacters('\u001B[4mvalue\u001B[0m'));
17981798
// Prints "value"
17991799
```
18001800
1801-
## `util.styleText(format, text)`
1801+
## `util.styleText(format, text[, options])`
18021802
18031803
> Stability: 1.1 - Active development
18041804
18051805
<!-- YAML
18061806
added: v20.12.0
1807+
changes:
1808+
- version: REPLACEME
1809+
pr-url: https://github.com/nodejs/node/pull/54389
1810+
description: Respect isTTY and environment variables
1811+
such as NO_COLORS, NODE_DISABLE_COLORS, and FORCE_COLOR.
18071812
-->
18081813
18091814
* `format` {string | Array} A text format or an Array
18101815
of text formats defined in `util.inspect.colors`.
18111816
* `text` {string} The text to to be formatted.
1817+
* `options` {Object}
1818+
* `validateStream` {boolean} When true, `stream` is checked to see if it can handle colors. **Default:** `true`.
1819+
* `stream` {Stream} A stream that will be validated if it can be colored. **Default:** `process.stdout`.
18121820
1813-
This function returns a formatted text considering the `format` passed.
1821+
This function returns a formatted text considering the `format` passed
1822+
for printing in a terminal, it is aware of the terminal's capabilities
1823+
and act according to the configuration set via `NO_COLORS`,
1824+
`NODE_DISABLE_COLORS` and `FORCE_COLOR` environment variables.
18141825
18151826
```mjs
18161827
import { styleText } from 'node:util';
1817-
const errorMessage = styleText('red', 'Error! Error!');
1818-
console.log(errorMessage);
1828+
import { stderr } from 'node:process';
1829+
1830+
const successMessage = styleText('green', 'Success!');
1831+
console.log(successMessage);
1832+
1833+
const errorMessage = styleText(
1834+
'red',
1835+
'Error! Error!',
1836+
// Validate if process.stderr has TTY
1837+
{ stream: stderr },
1838+
);
1839+
console.error(successMessage);
18191840
```
18201841
18211842
```cjs
18221843
const { styleText } = require('node:util');
1823-
const errorMessage = styleText('red', 'Error! Error!');
1824-
console.log(errorMessage);
1844+
const { stderr } = require('node:process');
1845+
1846+
const successMessage = styleText('green', 'Success!');
1847+
console.log(successMessage);
1848+
1849+
const errorMessage = styleText(
1850+
'red',
1851+
'Error! Error!',
1852+
// Validate if process.stderr has TTY
1853+
{ stream: stderr },
1854+
);
1855+
console.error(successMessage);
18251856
```
18261857
18271858
`util.inspect.colors` also provides text formats such as `italic`, and

‎lib/util.js

+41-1
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,26 @@ const {
6565
} = require('internal/util/inspect');
6666
const { debuglog } = require('internal/util/debuglog');
6767
const {
68+
validateBoolean,
6869
validateFunction,
6970
validateNumber,
7071
validateString,
7172
validateOneOf,
7273
} = require('internal/validators');
7374
const { isBuffer } = require('buffer').Buffer;
75+
const {
76+
isReadableStream,
77+
isWritableStream,
78+
isNodeStream,
79+
} = require('internal/streams/utils');
7480
const types = require('internal/util/types');
81+
82+
let utilColors;
83+
function lazyUtilColors() {
84+
utilColors ??= require('internal/util/colors');
85+
return utilColors;
86+
}
87+
7588
const binding = internalBinding('util');
7689

7790
const {
@@ -209,10 +222,25 @@ function escapeStyleCode(code) {
209222
/**
210223
* @param {string | string[]} format
211224
* @param {string} text
225+
* @param {object} [options={}]
226+
* @param {boolean} [options.validateStream=true] - Whether to validate the stream.
227+
* @param {Stream} [options.stream=process.stdout] - The stream used for validation.
212228
* @returns {string}
213229
*/
214-
function styleText(format, text) {
230+
function styleText(format, text, { validateStream = true, stream = process.stdout } = {}) {
215231
validateString(text, 'text');
232+
validateBoolean(validateStream, 'options.validateStream');
233+
234+
if (validateStream) {
235+
if (
236+
!isReadableStream(stream) &&
237+
!isWritableStream(stream) &&
238+
!isNodeStream(stream)
239+
) {
240+
throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream);
241+
}
242+
}
243+
216244
if (ArrayIsArray(format)) {
217245
let left = '';
218246
let right = '';
@@ -232,6 +260,18 @@ function styleText(format, text) {
232260
if (formatCodes == null) {
233261
validateOneOf(format, 'format', ObjectKeys(inspect.colors));
234262
}
263+
264+
// Check colorize only after validating arg type and value
265+
if (
266+
validateStream &&
267+
(
268+
!stream ||
269+
!lazyUtilColors().shouldColorize(stream)
270+
)
271+
) {
272+
return text;
273+
}
274+
235275
return `${escapeStyleCode(formatCodes[0])}${text}${escapeStyleCode(formatCodes[1])}`;
236276
}
237277

‎test/parallel/test-bootstrap-modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ expected.beforePreExec = new Set([
4343
'NativeModule internal/assert',
4444
'NativeModule internal/util/inspect',
4545
'NativeModule internal/util/debuglog',
46+
'NativeModule internal/streams/utils',
4647
'NativeModule internal/timers',
4748
'NativeModule events',
4849
'Internal Binding buffer',

‎test/parallel/test-util-styletext.js

+67-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
'use strict';
2-
require('../common');
3-
const assert = require('assert');
4-
const util = require('util');
2+
3+
const common = require('../common');
4+
const assert = require('node:assert');
5+
const util = require('node:util');
6+
const { WriteStream } = require('node:tty');
7+
8+
const styled = '\u001b[31mtest\u001b[39m';
9+
const noChange = 'test';
510

611
[
712
undefined,
@@ -31,13 +36,69 @@ assert.throws(() => {
3136
code: 'ERR_INVALID_ARG_VALUE',
3237
});
3338

34-
assert.strictEqual(util.styleText('red', 'test'), '\u001b[31mtest\u001b[39m');
39+
assert.strictEqual(
40+
util.styleText('red', 'test', { validateStream: false }),
41+
'\u001b[31mtest\u001b[39m',
42+
);
43+
44+
assert.strictEqual(
45+
util.styleText(['bold', 'red'], 'test', { validateStream: false }),
46+
'\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m',
47+
);
3548

36-
assert.strictEqual(util.styleText(['bold', 'red'], 'test'), '\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m');
37-
assert.strictEqual(util.styleText(['bold', 'red'], 'test'), util.styleText('bold', util.styleText('red', 'test')));
49+
assert.strictEqual(
50+
util.styleText(['bold', 'red'], 'test', { validateStream: false }),
51+
util.styleText(
52+
'bold',
53+
util.styleText('red', 'test', { validateStream: false }),
54+
{ validateStream: false },
55+
),
56+
);
3857

3958
assert.throws(() => {
4059
util.styleText(['invalid'], 'text');
4160
}, {
4261
code: 'ERR_INVALID_ARG_VALUE',
4362
});
63+
64+
assert.throws(() => {
65+
util.styleText('red', 'text', { stream: {} });
66+
}, {
67+
code: 'ERR_INVALID_ARG_TYPE',
68+
});
69+
70+
// does not throw
71+
util.styleText('red', 'text', { stream: {}, validateStream: false });
72+
73+
assert.strictEqual(
74+
util.styleText('red', 'test', { validateStream: false }),
75+
styled,
76+
);
77+
78+
const fd = common.getTTYfd();
79+
if (fd !== -1) {
80+
const writeStream = new WriteStream(fd);
81+
82+
const originalEnv = process.env;
83+
[
84+
{ isTTY: true, env: {}, expected: styled },
85+
{ isTTY: false, env: {}, expected: noChange },
86+
{ isTTY: true, env: { NODE_DISABLE_COLORS: '1' }, expected: noChange },
87+
{ isTTY: true, env: { NO_COLOR: '1' }, expected: noChange },
88+
{ isTTY: true, env: { FORCE_COLOR: '1' }, expected: styled },
89+
{ isTTY: true, env: { FORCE_COLOR: '1', NODE_DISABLE_COLORS: '1' }, expected: styled },
90+
{ isTTY: false, env: { FORCE_COLOR: '1', NO_COLOR: '1', NODE_DISABLE_COLORS: '1' }, expected: styled },
91+
{ isTTY: true, env: { FORCE_COLOR: '1', NO_COLOR: '1', NODE_DISABLE_COLORS: '1' }, expected: styled },
92+
].forEach((testCase) => {
93+
writeStream.isTTY = testCase.isTTY;
94+
process.env = {
95+
...process.env,
96+
...testCase.env
97+
};
98+
const output = util.styleText('red', 'test', { stream: writeStream });
99+
assert.strictEqual(output, testCase.expected);
100+
process.env = originalEnv;
101+
});
102+
} else {
103+
common.skip('Could not create TTY fd');
104+
}

0 commit comments

Comments
 (0)
Please sign in to comment.