rsnext/packages/next/lib/eslint/customFormatter.ts
Houssein Djirdeh 7a1c9eb17e
[ESLint] Introduce a new setup process when next lint is run for the first time (#26584)
This PR introduces an improved developer experience when `next lint` is run for the first time.

### Current behavior

`eslint-config-next` is a required package that must be installed before proceeding with `next lint` or `next build`:

![image](https://user-images.githubusercontent.com/12476932/123468791-43088100-d5c0-11eb-9ad0-5beb80b6c968.png)

Although this has helped many developers start using the new ESLint config, this has also resulted in a few issues:

- Users are required to install the full config (`eslint-config-next`) even if they do not use it or use the Next.js plugin directly (`eslint-plugin-next`).
  - #26348

- There's some confusion  on why `eslint-config-next` needs to be installed or how it should be used instead of `eslint-plugin-next`.
  - #26574
  - #26475
  - #26438

### New behavior

Instead of enforcing `eslint-config-next` as a required package, this PR prompts the user by asking what config they would like to start. This happens when `next lint` is run for the first time **and** if no ESLint configuration is detected in the application.

<img src="https://user-images.githubusercontent.com/12476932/124331177-e1668a80-db5c-11eb-8915-38d3dc20f5d4.gif" width="800" />

- The CLI will take care of installing `eslint` or `eslint-config-next` if either is not already installed
- Users now have the option to choose between a strict configuration (`next/core-web-vitals`) or just the base configuration (`next`)
- For users that decide to create their own ESLint configuration, or already have an existing one, **installing `eslint-config-next` will not be a requirement for `next lint` or `next build` to run**. A warning message will just show if the Next.js ESLint plugin is not detected in an ESLint config. 

  <img width="682" alt="Screen Shot 2021-06-25 at 3 02 12 PM" src="https://user-images.githubusercontent.com/12476932/123473329-6cc4a680-d5c6-11eb-9a57-d5c0b89a2732.png">

---

In addition, this PR also:

- Fixes #26348
- Updates documentation to make it more clear what approach to take for new and existing ESLint configurations
2021-08-04 21:53:15 +00:00

136 lines
3.1 KiB
TypeScript

import chalk from 'chalk'
import path from 'path'
// eslint-disable-next-line no-shadow
export enum MessageSeverity {
Warning = 1,
Error = 2,
}
interface LintMessage {
ruleId: string | null
severity: 1 | 2
message: string
line: number
column: number
}
export interface LintResult {
filePath: string
messages: LintMessage[]
errorCount: number
warningCount: number
output?: string
source?: string
}
function pluginCount(
messages: LintMessage[]
): { nextPluginErrorCount: number; nextPluginWarningCount: number } {
let nextPluginWarningCount = 0
let nextPluginErrorCount = 0
for (let i = 0; i < messages.length; i++) {
const { severity, ruleId } = messages[i]
if (ruleId?.includes('@next/next')) {
if (severity === MessageSeverity.Warning) {
nextPluginWarningCount += 1
} else {
nextPluginErrorCount += 1
}
}
}
return {
nextPluginErrorCount,
nextPluginWarningCount,
}
}
function formatMessage(
dir: string,
messages: LintMessage[],
filePath: string
): string {
let fileName = path.posix.normalize(
path.relative(dir, filePath).replace(/\\/g, '/')
)
if (!fileName.startsWith('.')) {
fileName = './' + fileName
}
let output = '\n' + chalk.cyan(fileName)
for (let i = 0; i < messages.length; i++) {
const { message, severity, line, column, ruleId } = messages[i]
output = output + '\n'
if (line && column) {
output =
output +
chalk.yellow(line.toString()) +
':' +
chalk.yellow(column.toString()) +
' '
}
if (severity === MessageSeverity.Warning) {
output += chalk.yellow.bold('Warning') + ': '
} else {
output += chalk.red.bold('Error') + ': '
}
output += message
if (ruleId) {
output += ' ' + chalk.gray.bold(ruleId)
}
}
return output
}
export function formatResults(
baseDir: string,
results: LintResult[],
format: (r: LintResult[]) => string
): {
output: string
totalNextPluginErrorCount: number
totalNextPluginWarningCount: number
} {
let totalNextPluginErrorCount = 0
let totalNextPluginWarningCount = 0
let resultsWithMessages = results.filter(({ messages }) => messages?.length)
// Track number of Next.js plugin errors and warnings
resultsWithMessages.forEach(({ messages }) => {
const res = pluginCount(messages)
totalNextPluginErrorCount += res.nextPluginErrorCount
totalNextPluginWarningCount += res.nextPluginWarningCount
})
// Use user defined formatter or Next.js's built-in custom formatter
const output = format
? format(resultsWithMessages)
: resultsWithMessages
.map(({ messages, filePath }) =>
formatMessage(baseDir, messages, filePath)
)
.join('\n')
return {
output:
resultsWithMessages.length > 0
? output +
`\n\n${chalk.cyan(
'info'
)} - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules`
: '',
totalNextPluginErrorCount,
totalNextPluginWarningCount,
}
}