Skip to content

Commit e84812c

Browse files
RafaelGSStargos
authored andcommitted
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: #54389Fixes: #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

4 files changed

Lines changed: 146 additions & 13 deletions

File tree

‎doc/api/util.md‎

Lines changed: 37 additions & 6 deletions
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-
consterrorMessage=styleText('red', 'Error! Error!');
1818-
console.log(errorMessage);
1828+
import { stderr } from'node:process';
1829+
1830+
constsuccessMessage=styleText('green', 'Success!');
1831+
console.log(successMessage);
1832+
1833+
consterrorMessage=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-
consterrorMessage=styleText('red', 'Error! Error!');
1824-
console.log(errorMessage);
1844+
const { stderr } =require('node:process');
1845+
1846+
constsuccessMessage=styleText('green', 'Success!');
1847+
console.log(successMessage);
1848+
1849+
consterrorMessage=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‎

Lines changed: 41 additions & 1 deletion
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
consttypes=require('internal/util/types');
81+
82+
letutilColors;
83+
functionlazyUtilColors(){
84+
utilColors??=require('internal/util/colors');
85+
returnutilColors;
86+
}
87+
7588
constbinding=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-
functionstyleText(format,text){
230+
functionstyleText(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+
thrownewERR_INVALID_ARG_TYPE('stream',['ReadableStream','WritableStream','Stream'],stream);
241+
}
242+
}
243+
216244
if(ArrayIsArray(format)){
217245
letleft='';
218246
letright='';
@@ -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+
returntext;
273+
}
274+
235275
return`${escapeStyleCode(formatCodes[0])}${text}${escapeStyleCode(formatCodes[1])}`;
236276
}
237277

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

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
'use strict';
2-
require('../common');
3-
constassert=require('assert');
4-
constutil=require('util');
2+
3+
constcommon=require('../common');
4+
constassert=require('node:assert');
5+
constutil=require('node:util');
6+
const{ WriteStream }=require('node:tty');
7+
8+
conststyled='\u001b[31mtest\u001b[39m';
9+
constnoChange='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+
constfd=common.getTTYfd();
79+
if(fd!==-1){
80+
constwriteStream=newWriteStream(fd);
81+
82+
constoriginalEnv=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+
constoutput=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)