- Start Date: 2019-10-20
- RFC PR: #45
- Authors: Toru Nagashima (@mysticatea)
This RFC adds the custom formatter v2 that supports streaming based on RFC40.
When ESLint verifies many files, it spent time silently. People may be worried about "Is ESLint hanging up?", "When does it finish?", etc. Streaming support will resolve that.
This RFC has two parts.
- Adding new formatter style.
- Updating
ESLint#getFormatter()
method.
This RFC adds a new formatter style.
module.exports = {
formatterVersion: 2,
async *format(resultIterator, context) {
//...
},
}
This is the constant 2
.
We may add other numbers such as 3
in the future if we want to change the arguments of the format()
function. Between the current and 2
, the first argument was changed from Array<LintResult>
to AsyncIterable<LintResult>
. ESLint distinguishes by this property whether the formatter supports the new argument.
Tha main function of the formatter. This function transforms the lint results to strings asynchronously.
Name | Type | Description |
---|---|---|
resultIterator |
AsyncIterable<LintResult> |
The stream of the lint results. |
context |
Object |
The formatting context. |
The context
parameter has the following properties:
context.eslintVersion
... The version of ESLint. E.g."7.0.0"
. If a formatter prints the version, it may be useful for debug.context.formatterVersion
... The actual version of the formatter spec of this call. This is always2
for now. In the future, if we added v3 formatter, this property will help custom formatter authors to support both v2 and v3.context.isTTY
... Iftrue
then ESLint outputs the formatted texts to TTY. This means we can use CSI sequences to represent stuff such as progress bars. Otherwise, if--output
option is present or the eslint command is redirected to another program, this isfalse
.context.getRuleMeta(ruleId)
... The function to get the metadata of a rule. This corresponds tometadata.rulesMeta
property of the current formatter. This RFC changes themetadata.rulesMeta
property to a function because we cannot know all plugin rules that the linting will use at this time, because of asynchronous. On the other hand, we can get the rules of the yielded results.
The return value is an async iterable object that iterates strings.
The formatter author can include CSI sequences to represent stuff such as progress bars if context.isTTY
is true
.
See /lib/cli-engine/formatters/stylish.js.
module.exports = {
formatterVersion: 2,
async *format(resultIterator) {
for await (const { filePath, messages } of resultIterator) {
for (const { column, line, message } of messages) {
yield `${filePath}:${line}:${column} - ${message}\n`
}
}
},
}
const { ESLint } = require("eslint")
const ClearLine = "\x1b[1K\x1b[1G"
module.exports = {
formatterVersion: 2,
async *format(resultIterator, { isTTY }) {
const results = []
let fileCount = 0
let errorCount = 0
// Show progress
for await (const result of resultIterator) {
results.push(result)
fileCount += 1
errorCount += result.errorCount
if (isTTY) {
yield [
results.length === 0 ? "" : ClearLine,
fileCount,
"file(s) linted and",
errorCount,
"error(s) found.",
].join(" ")
}
}
if (isTTY && results.length >= 1) {
yield ClearLine
}
// Sort results
results.sort(ESLint.compareResultsByFilePath)
// Show results
for (const { filePath, messages } of results) {
for (const { column, line, message } of messages) {
yield `${filePath}:${line}:${column} - ${message}\n`
}
}
},
}
/lib/cli-engine/formatters/stylish.js supports both v1 formatter and v2 formatter. The essence is:
function stylish(results) {
// ... (format results) ...
return formattedText
}
stylish.formatterVersion = 2
stylish.format = async function*(resultIterator) {
const results = []
// ... (show progress while collecting results) ...
yield stylish(results)
}
module.exports = stylish
In this case, old ESLint just calls the stylish
function and new ESLint will call the stylish.format
function.
The ESLint
class is introduced in RFC40.
This RFC adds the second parameter to pass isTTY
into the formatter's adapter. For example:
const { ESLint } = require("eslint")
const eslint = new ESLint()
const stylish = eslint.getFormatter("stylish")
const resultIterator = eslint.executeOnFiles(patterns)
const options = { isTTY: process.stdout.isTTY }
// ↓ Here!
for await (const text of stylish(resultIterator, options)) {
process.stdout.write(text)
}
process.stdout.write("\n")
If the options
is not present, defaults to { isTTY: false }
.
If the loaded formatter has formatterVersion
property with an integer, the adapter gives the format
method of the formatter the lint result iterator. Otherwise, the adapter collects the results, sort the results, then gives the formatter the collected results (for compatible with v1).
See also a PoC /lib/eslint/eslint.js#L393-L421.
If the formatterVersion
was less than 2
, the ESLint#getFormatter()
method throws an error "unknown version."
If the formatterVersion
was greater than 2
, the ESLint#getFormatter()
method considers the formatter as supporting v2 as well because the formatter knows the existence of v2. This behavior will help the formatter authors to support all of the formatter versions in their custom formatters in the future.
We should update:
- the "Working with Custom Formatters" page.
- the "Node.js API" page for the new parameter of the adapter that
ESLint#getFormatter()
method returns.
It increases maintenance costs.
Once RFC40 is implemented, this RFC doesn't have any breaking changes.
- Using Streams. Because the eventual destination is a writable stream, it's reasonable if the formatter is a transform stream that is from lint results to strings. However, the stream formatter forces the
ESLint#executeOnFiles()
to return a readable stream of lint results instead of an async iterable for consistency, and streams don't have the interoperability with the async iteration language feature enough at this time (Node v10). Async iteration is greater than streams for some points: the error propagation and syntax supports (for-await-of
loops and async generators).
- eslint/eslint#1098 - Show results for each file as eslint is running
- #40 - New:
ESLint
Class ReplacingCLIEngine