Skip to content

Commit a176319

Browse files
feat!: replace chalk with styleText and add color to ResultsMeta (#20227)
* wip * wip * wip * wip * wip * wip: revert `ci.yml` * wip: remove `eslint-disable` comment * wip * wip: fix `FORCE_COLOR` related error and add tests to cover it * wip: fix CI failure * wip: temporarily turn on `fail-fast` * wip: temp * wip * wip: simplify tests * wip: update documentation * wip: update docs * wip: pass `color` option to `ResultsMeta` * wip: accept `color` option in `stylish` * wip: add more tests for `stylish.js` * wip: resolve CI failure * wip: remove duplicate test cases * wip: add more descriptions * wip: simplify code in `cli.js` * wip: add test cases for `cli.js` * wip: add more test cases for `stylish.js` * wip: simplify `reset` and update `check-rule-examples` * Revert "wip: simplify `reset` and update `check-rule-examples`" This reverts commit e8a4da9. * wip: update migration docs * wip: simplify `reset` logic * wip: update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> * wip: update comment on `styleText` workaround for Node.js compatibility * wip: update `resultsMeta` description to include `color` property in Node.js API * wip: update `resultsMeta` description in `index.d.ts` * wip: add `color` property to `LintResultData` * wip: use fixture formatter * wip: update `check-rule-examples` * wip: restore `FORCE_COLOR` in test * wip: use `beforeEach` and `afterEach` --------- Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
1 parent 54bf0a3 commit a176319

14 files changed

Lines changed: 482 additions & 186 deletions

File tree

docs/src/extend/custom-formatters.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ The `results` object passed into a formatter is an array of [`LintResult`](../in
8787

8888
The formatter function receives a `context` object as its second argument. The object has the following properties:
8989

90+
- `color` (optional): If `--color` was set, this property is `true`. If `--no-color` was set, it is `false`. If neither option was provided, the property is omitted.
9091
- `cwd`: The current working directory. This value comes from the `cwd` constructor option of the [ESLint](../integrate/nodejs-api#-new-eslintoptions) class.
9192
- `maxWarningsExceeded` (optional): If `--max-warnings` was set and the number of warnings exceeded the limit, this property's value is an object containing two properties:
9293
- `maxWarnings`: the value of the `--max-warnings` option

docs/src/integrate/nodejs-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ This edit information means replacing the range of the `range` property by the `
546546
The `LoadedFormatter` value is the object to convert the [LintResult] objects to text. The [eslint.loadFormatter()][eslint-loadformatter] method returns it. It has the following method:
547547
548548
- `format` (`(results: LintResult[], resultsMeta?: ResultsMeta) => string | Promise<string>`)<br>
549-
The method to convert the [LintResult] objects to text. `resultsMeta` is an optional parameter that is primarily intended for use by the ESLint CLI and can contain only a `maxWarningsExceeded` property that would be passed through the [`context`](../extend/custom-formatters#the-context-argument) object when this method calls the underlying formatter function. Note that ESLint automatically generates `cwd` and `rulesMeta` properties of the `context` object, so you typically don't need to pass in the second argument when calling this method.
549+
The method to convert the [LintResult] objects to text. `resultsMeta` is an optional parameter that is primarily intended for use by the ESLint CLI and can contain `color` and `maxWarningsExceeded` properties that would be passed through the [`context`](../extend/custom-formatters#the-context-argument) object when this method calls the underlying formatter function. Note that ESLint automatically generates `cwd` and `rulesMeta` properties of the `context` object, so you typically don't need to pass in the second argument when calling this method.
550550
551551
---
552552

docs/src/use/command-line-interface.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,12 @@ These options force the enabling/disabling of colorized output.
597597

598598
You can use these options to override the default behavior, which is to enable colorized output unless no TTY is detected, such as when piping `eslint` through `cat` or `less`.
599599

600+
::: warning
601+
602+
When neither `--color` nor `--no-color` is specified, the formatter may decide whether to colorize the output based on the runtime environment. For example, when using the default `stylish` formatter under Node.js, [`FORCE_COLOR`](https://nodejs.org/api/cli.html#force_color1-2-3), [`NO_COLOR`](https://nodejs.org/api/cli.html#no_colorany), or [`NODE_DISABLE_COLORS`](https://nodejs.org/api/cli.html#node_disable_colors1) environment variables may affect whether colors are used.
603+
604+
:::
605+
600606
##### `--color` and `--no-color` example
601607

602608
{{ npx_tabs ({

docs/src/use/migrate-to-10.0.0.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The lists below are ordered roughly by the number of users each change is expect
2323
- [`eslint-env` comments are reported as errors](#eslint-env-comments)
2424
- [Jiti < v2.2.0 are no longer supported](#drop-old-jiti)
2525
- [POSIX character classes in glob patterns](#posix-character-classes)
26+
- [`stylish` formatter now uses native `styleText` instead of `chalk`](#stylish-formatter)
2627
- [Deprecated options of the `radix` rule](#radix)
2728
- [`no-shadow-restricted-names` now reports `globalThis` by default](#no-shadow-restricted-names)
2829
- [`func-names` schema is stricter](#func-names)
@@ -168,6 +169,26 @@ Here, `[[:upper:]]` is a POSIX character class that matches uppercase letters in
168169

169170
**Related issue(s):** [eslint/rewrite#66](https://github.com/eslint/rewrite/issues/66)
170171

172+
## <a name="stylish-formatter"></a> `stylish` formatter now uses native `styleText` instead of `chalk`
173+
174+
Starting in ESLint v10.0.0, the built-in [`stylish`](./formatters#stylish) formatter no longer depends on the third-party [`chalk`](https://github.com/chalk/chalk) library for colorized output. Instead, it now uses Node.js's native [`styleText`](https://nodejs.org/api/util.html#utilstyletextformat-text-options) API, which introduces two breaking changes regarding how colorized output is determined:
175+
176+
1. First, `styleText` checks more environment variables when determining whether to disable colorized output and follows Node.js's own rules for when to enable or disable colors. This means it respects a wider set of environment variables and terminal capabilities than ESLint's previous `chalk`-based logic. For example:
177+
178+
- [`NO_COLOR`](https://nodejs.org/api/cli.html#no_colorany) now disables colors consistently across tools that honor this convention.
179+
- [`NODE_DISABLE_COLORS`](https://nodejs.org/api/cli.html#node_disable_colors1) is also respected, aligning ESLint's behavior with Node.js itself.
180+
181+
Please note that the [`FORCE_COLOR`](https://nodejs.org/api/cli.html#force_color1-2-3) environment variable is still supported to force-enable colors.
182+
183+
2. Second, `--color` and `--no-color` CLI flags now have higher precedence than environment variables when determining whether to use colorized output. This change ensures that explicit user preferences via CLI flags are prioritized. However, if neither flag is provided, environment variables will be considered as before.
184+
185+
**To address:**
186+
187+
- Review any environment configuration related to terminal colors (for example, CI defaults or shell profiles). If ESLint's output appears uncolored after upgrading to v10.0.0, check whether `NO_COLOR` or `NODE_DISABLE_COLORS` (or similar settings) are being set in your environment.
188+
- If you rely on mixed approaches (for example, using `--color` flag but also setting `NO_COLOR` environment variable), be aware that the CLI flags now take precedence and adjust your setup accordingly.
189+
190+
**Related issue(s):** [#20012](https://github.com/eslint/eslint/issues/20012)
191+
171192
## <a name="radix"></a> Deprecated options of the `radix` rule
172193

173194
As of ESLint v10.0.0, string options `"always"` and `"as-needed"` of the [`radix`](../rules/radix) rule are deprecated. Setting either of these options doesn't change the behavior of this rule, which now always enforces providing a radix, as it was the case when the `"always"` option (default) was specified. Since the default radix depends on the first argument of `parseInt()`, this rule assumes that the second argument (the radix) is always needed.

lib/cli-engine/formatters/stylish.js

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,30 @@
44
*/
55
"use strict";
66

7-
const chalk = require("chalk"),
8-
util = require("node:util"),
7+
const util = require("node:util"),
98
table = require("../../shared/text-table");
109

1110
//------------------------------------------------------------------------------
1211
// Helpers
1312
//------------------------------------------------------------------------------
1413

14+
/**
15+
* Returns a styling function based on the color option.
16+
* @param {boolean|undefined} color Indicates whether to use colors.
17+
* @returns {Function} A function that styles text.
18+
*/
19+
function getStyleText(color) {
20+
if (typeof color === "undefined") {
21+
return (format, text) =>
22+
util.styleText(format, text, { validateStream: true });
23+
}
24+
if (color) {
25+
return (format, text) =>
26+
util.styleText(format, text, { validateStream: false });
27+
}
28+
return (_, text) => text;
29+
}
30+
1531
/**
1632
* Given a word and a count, append an s if count is not one.
1733
* @param {string} word A word in its singular form.
@@ -26,7 +42,9 @@ function pluralize(word, count) {
2642
// Public Interface
2743
//------------------------------------------------------------------------------
2844

29-
module.exports = function (results) {
45+
module.exports = function (results, data) {
46+
const styleText = getStyleText(data?.color);
47+
3048
let output = "\n",
3149
errorCount = 0,
3250
warningCount = 0,
@@ -46,17 +64,17 @@ module.exports = function (results) {
4664
fixableErrorCount += result.fixableErrorCount;
4765
fixableWarningCount += result.fixableWarningCount;
4866

49-
output += `${chalk.underline(result.filePath)}\n`;
67+
output += `${styleText("underline", result.filePath)}\n`;
5068

5169
output += `${table(
5270
messages.map(message => {
5371
let messageType;
5472
5573
if (message.fatal || message.severity === 2) {
56-
messageType = chalk.red("error");
74+
messageType = styleText("red", "error");
5775
summaryColor = "red";
5876
} else {
59-
messageType = chalk.yellow("warning");
77+
messageType = styleText("yellow", "warning");
6078
}
6179
6280
return [
@@ -65,7 +83,7 @@ module.exports = function (results) {
6583
String(message.column || 0),
6684
messageType,
6785
message.message.replace(/([^ ])\.$/u, "$1"),
68-
chalk.dim(message.ruleId || ""),
86+
message.ruleId ? styleText("dim", message.ruleId) : "",
6987
];
7088
}),
7189
{
@@ -78,45 +96,58 @@ module.exports = function (results) {
7896
.split("\n")
7997
.map(el =>
8098
el.replace(/(\d+)\s+(\d+)/u, (m, p1, p2) =>
81-
chalk.dim(`${p1}:${p2}`),
99+
styleText("dim", `${p1}:${p2}`),
82100
),
83101
)
84102
.join("\n")}\n\n`;
85103
});
86104

87105
const total = errorCount + warningCount;
88106

107+
/*
108+
* We can't use a single `styleText` call like `styleText([summaryColor, "bold"], text)` here.
109+
* This is a bug in `util.styleText` in Node.js versions earlier than v22.15.0 (https://github.com/nodejs/node/issues/56717).
110+
* As a workaround, we use nested `styleText` calls.
111+
*/
89112
if (total > 0) {
90-
output += chalk[summaryColor].bold(
91-
[
92-
"\u2716 ",
93-
total,
94-
pluralize(" problem", total),
95-
" (",
96-
errorCount,
97-
pluralize(" error", errorCount),
98-
", ",
99-
warningCount,
100-
pluralize(" warning", warningCount),
101-
")\n",
102-
].join(""),
103-
);
104-
105-
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
106-
output += chalk[summaryColor].bold(
113+
output += `${styleText(
114+
summaryColor,
115+
styleText(
116+
"bold",
107117
[
108-
" ",
109-
fixableErrorCount,
110-
pluralize(" error", fixableErrorCount),
111-
" and ",
112-
fixableWarningCount,
113-
pluralize(" warning", fixableWarningCount),
114-
" potentially fixable with the `--fix` option.\n",
118+
"\u2716 ",
119+
total,
120+
pluralize(" problem", total),
121+
" (",
122+
errorCount,
123+
pluralize(" error", errorCount),
124+
", ",
125+
warningCount,
126+
pluralize(" warning", warningCount),
127+
")",
115128
].join(""),
116-
);
129+
),
130+
)}\n`;
131+
132+
if (fixableErrorCount > 0 || fixableWarningCount > 0) {
133+
output += `${styleText(
134+
summaryColor,
135+
styleText(
136+
"bold",
137+
[
138+
" ",
139+
fixableErrorCount,
140+
pluralize(" error", fixableErrorCount),
141+
" and ",
142+
fixableWarningCount,
143+
pluralize(" warning", fixableWarningCount),
144+
" potentially fixable with the `--fix` option.",
145+
].join(""),
146+
),
147+
)}\n`;
117148
}
118149
}
119150

120151
// Resets output color, for prevent change on top level
121-
return total > 0 ? chalk.reset(output) : "";
152+
return total > 0 ? styleText("reset", output) : "";
122153
};

lib/cli.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -454,14 +454,24 @@ const cli = {
454454
const tooManyWarnings =
455455
options.maxWarnings >= 0 &&
456456
resultCounts.warningCount > options.maxWarnings;
457-
const resultsMeta = tooManyWarnings
458-
? {
459-
maxWarningsExceeded: {
460-
maxWarnings: options.maxWarnings,
461-
foundWarnings: resultCounts.warningCount,
462-
},
463-
}
464-
: {};
457+
const resultsMeta = /** @type {ResultsMeta} */ ({});
458+
459+
/*
460+
* `--color` was set, `options.color` is `true`.
461+
* `--no-color` was set, `options.color` is `false`.
462+
* Neither option was provided, `options.color` is omitted, so `undefined`.
463+
*/
464+
if (options.color !== void 0) {
465+
debug(`Color setting for output: ${options.color}`);
466+
resultsMeta.color = options.color;
467+
}
468+
469+
if (tooManyWarnings) {
470+
resultsMeta.maxWarningsExceeded = {
471+
maxWarnings: options.maxWarnings,
472+
foundWarnings: resultCounts.warningCount,
473+
};
474+
}
465475

466476
if (
467477
await printResults(

lib/types/index.d.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,9 +1261,8 @@ export namespace ESLint {
12611261
foundWarnings: number;
12621262
}
12631263

1264-
interface LintResultData {
1264+
interface LintResultData extends ResultsMeta {
12651265
cwd: string;
1266-
maxWarningsExceeded?: MaxWarningsExceeded | undefined;
12671266
rulesMeta: {
12681267
[ruleId: string]: Rule.RuleMetaData;
12691268
};
@@ -1295,6 +1294,14 @@ export namespace ESLint {
12951294
* Metadata about results for formatters.
12961295
*/
12971296
interface ResultsMeta {
1297+
/**
1298+
* Whether or not to use color in the formatter output.
1299+
* - If `--color` was set, this property is `true`.
1300+
* - If `--no-color` was set, it is `false`.
1301+
* - If neither option was provided, the property is omitted.
1302+
*/
1303+
color?: boolean | undefined;
1304+
12981305
/**
12991306
* Present if the maxWarnings threshold was exceeded.
13001307
*/
@@ -1306,9 +1313,9 @@ export namespace ESLint {
13061313
/**
13071314
* Used to call the underlying formatter.
13081315
* @param results An array of lint results to format.
1309-
* @param resultsMeta An object with an optional `maxWarningsExceeded` property that will be
1316+
* @param resultsMeta An object with optional `color` and `maxWarningsExceeded` properties that will be
13101317
* passed to the underlying formatter function along with other properties set by ESLint.
1311-
* This argument can be omitted if `maxWarningsExceeded` is not needed.
1318+
* This argument can be omitted if `color` and `maxWarningsExceeded` are not needed.
13121319
* @return The formatter output.
13131320
*/
13141321
format(

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@
117117
"@humanwhocodes/retry": "^0.4.2",
118118
"@types/estree": "^1.0.6",
119119
"ajv": "^6.12.4",
120-
"chalk": "^4.0.0",
121120
"cross-spawn": "^7.0.6",
122121
"debug": "^4.3.2",
123122
"escape-string-regexp": "^4.0.0",
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
/*global module*/
22
module.exports = function(results, context) {
3-
return context.cwd;
3+
return context;
44
};

0 commit comments

Comments
 (0)