Skip to content

Commit cc5549b

Browse files
RafaelGSSlouwers
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: nodejs#54389 Fixes: nodejs#54365 Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Claudio Wunder <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent ccb5f07 commit cc5549b

File tree

4 files changed

+146
-13
lines changed

4 files changed

+146
-13
lines changed

doc/api/util.md

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,32 +1802,63 @@ console.log(util.stripVTControlCharacters('\u001B[4mvalue\u001B[0m'));
18021802
// Prints "value"
18031803
```
18041804
1805-
## `util.styleText(format, text)`
1805+
## `util.styleText(format, text[, options])`
18061806
18071807
> Stability: 1.1 - Active development
18081808
18091809
<!-- YAML
18101810
added:
18111811
- v21.7.0
18121812
- v20.12.0
1813+
changes:
1814+
- version: REPLACEME
1815+
pr-url: https://github.com/nodejs/node/pull/54389
1816+
description: Respect isTTY and environment variables
1817+
such as NO_COLORS, NODE_DISABLE_COLORS, and FORCE_COLOR.
18131818
-->
18141819
18151820
* `format` {string | Array} A text format or an Array
18161821
of text formats defined in `util.inspect.colors`.
18171822
* `text` {string} The text to to be formatted.
1823+
* `options` {Object}
1824+
* `validateStream` {boolean} When true, `stream` is checked to see if it can handle colors. **Default:** `true`.
1825+
* `stream` {Stream} A stream that will be validated if it can be colored. **Default:** `process.stdout`.
18181826
1819-
This function returns a formatted text considering the `format` passed.
1827+
This function returns a formatted text considering the `format` passed
1828+
for printing in a terminal, it is aware of the terminal's capabilities
1829+
and act according to the configuration set via `NO_COLORS`,
1830+
`NODE_DISABLE_COLORS` and `FORCE_COLOR` environment variables.
18201831
18211832
```mjs
18221833
import { styleText } from 'node:util';
1823-
const errorMessage = styleText('red', 'Error! Error!');
1824-
console.log(errorMessage);
1834+
import { stderr } from 'node:process';
1835+
1836+
const successMessage = styleText('green', 'Success!');
1837+
console.log(successMessage);
1838+
1839+
const errorMessage = styleText(
1840+
'red',
1841+
'Error! Error!',
1842+
// Validate if process.stderr has TTY
1843+
{ stream: stderr },
1844+
);
1845+
console.error(successMessage);
18251846
```
18261847
18271848
```cjs
18281849
const { styleText } = require('node:util');
1829-
const errorMessage = styleText('red', 'Error! Error!');
1830-
console.log(errorMessage);
1850+
const { stderr } = require('node:process');
1851+
1852+
const successMessage = styleText('green', 'Success!');
1853+
console.log(successMessage);
1854+
1855+
const errorMessage = styleText(
1856+
'red',
1857+
'Error! Error!',
1858+
// Validate if process.stderr has TTY
1859+
{ stream: stderr },
1860+
);
1861+
console.error(successMessage);
18311862
```
18321863
18331864
`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
@@ -56,12 +56,25 @@ const {
5656
} = require('internal/util/inspect');
5757
const { debuglog } = require('internal/util/debuglog');
5858
const {
59+
validateBoolean,
5960
validateFunction,
6061
validateNumber,
6162
validateString,
6263
validateOneOf,
6364
} = require('internal/validators');
65+
const {
66+
isReadableStream,
67+
isWritableStream,
68+
isNodeStream,
69+
} = require('internal/streams/utils');
6470
const types = require('internal/util/types');
71+
72+
let utilColors;
73+
function lazyUtilColors() {
74+
utilColors ??= require('internal/util/colors');
75+
return utilColors;
76+
}
77+
6578
const binding = internalBinding('util');
6679

6780
const {
@@ -92,10 +105,25 @@ function escapeStyleCode(code) {
92105
/**
93106
* @param {string | string[]} format
94107
* @param {string} text
108+
* @param {object} [options={}]
109+
* @param {boolean} [options.validateStream=true] - Whether to validate the stream.
110+
* @param {Stream} [options.stream=process.stdout] - The stream used for validation.
95111
* @returns {string}
96112
*/
97-
function styleText(format, text) {
113+
function styleText(format, text, { validateStream = true, stream = process.stdout } = {}) {
98114
validateString(text, 'text');
115+
validateBoolean(validateStream, 'options.validateStream');
116+
117+
if (validateStream) {
118+
if (
119+
!isReadableStream(stream) &&
120+
!isWritableStream(stream) &&
121+
!isNodeStream(stream)
122+
) {
123+
throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream);
124+
}
125+
}
126+
99127
if (ArrayIsArray(format)) {
100128
let left = '';
101129
let right = '';
@@ -115,6 +143,18 @@ function styleText(format, text) {
115143
if (formatCodes == null) {
116144
validateOneOf(format, 'format', ObjectKeys(inspect.colors));
117145
}
146+
147+
// Check colorize only after validating arg type and value
148+
if (
149+
validateStream &&
150+
(
151+
!stream ||
152+
!lazyUtilColors().shouldColorize(stream)
153+
)
154+
) {
155+
return text;
156+
}
157+
118158
return `${escapeStyleCode(formatCodes[0])}${text}${escapeStyleCode(formatCodes[1])}`;
119159
}
120160

test/parallel/test-bootstrap-modules.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ expected.beforePreExec = new Set([
4646
'NativeModule internal/assert',
4747
'NativeModule internal/util/inspect',
4848
'NativeModule internal/util/debuglog',
49+
'NativeModule internal/streams/utils',
4950
'NativeModule internal/timers',
5051
'NativeModule events',
5152
'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-
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)