2021-06-03 14:01:24 +02:00
import { promises as fs } from 'fs'
2021-12-21 16:13:45 +01:00
import chalk from 'next/dist/compiled/chalk'
2021-06-10 11:02:50 +02:00
import path from 'path'
2021-04-30 13:09:07 +02:00
import findUp from 'next/dist/compiled/find-up'
import semver from 'next/dist/compiled/semver'
2021-06-03 14:01:24 +02:00
import * as CommentJson from 'next/dist/compiled/comment-json'
2021-04-30 13:09:07 +02:00
2021-06-29 12:12:23 +02:00
import { LintResult , formatResults } from './customFormatter'
2021-04-30 13:09:07 +02:00
import { writeDefaultConfig } from './writeDefaultConfig'
2021-08-04 23:53:15 +02:00
import { hasEslintConfiguration } from './hasEslintConfiguration'
import { ESLINT_PROMPT_VALUES } from '../constants'
2021-06-10 11:02:50 +02:00
import { existsSync , findPagesDir } from '../find-pages-dir'
2021-08-04 23:53:15 +02:00
import { installDependencies } from '../install-dependencies'
import { hasNecessaryDependencies } from '../has-necessary-dependencies'
import { isYarn } from '../is-yarn'
2021-04-30 13:09:07 +02:00
import * as Log from '../../build/output/log'
2021-06-15 03:31:40 +02:00
import { EventLintCheckCompleted } from '../../telemetry/events/build'
2022-01-11 21:40:03 +01:00
import isError , { getProperError } from '../is-error'
2021-04-30 13:09:07 +02:00
type Config = {
plugins : string [ ]
rules : { [ key : string ] : Array < number | string > }
}
2022-02-24 21:43:28 +01:00
// 0 is off, 1 is warn, 2 is error. See https://eslint.org/docs/user-guide/configuring/rules#configuring-rules
const VALID_SEVERITY = [ 'off' , 'warn' , 'error' ] as const
type Severity = typeof VALID_SEVERITY [ number ]
function isValidSeverity ( severity : string ) : severity is Severity {
return VALID_SEVERITY . includes ( severity as Severity )
}
2021-08-04 23:53:15 +02:00
const requiredPackages = [
2021-10-21 00:28:08 +02:00
{ file : 'eslint' , pkg : 'eslint' } ,
2021-08-04 23:53:15 +02:00
{ file : 'eslint-config-next' , pkg : 'eslint-config-next' } ,
]
async function cliPrompt() {
console . log (
chalk . bold (
` ${ chalk . cyan (
'?'
) } How would you like to configure ESLint ? https : //nextjs.org/docs/basic-features/eslint`
)
)
try {
2021-10-16 14:22:42 +02:00
const cliSelect = (
await Promise . resolve ( require ( 'next/dist/compiled/cli-select' ) )
) . default
2021-08-04 23:53:15 +02:00
const { value } = await cliSelect ( {
values : ESLINT_PROMPT_VALUES ,
valueRenderer : (
{
title ,
recommended ,
} : { title : string ; recommended? : boolean ; config : any } ,
selected : boolean
) = > {
const name = selected ? chalk . bold . underline . cyan ( title ) : title
return name + ( recommended ? chalk . bold . yellow ( ' (recommended)' ) : '' )
} ,
selected : chalk.cyan ( '❯ ' ) ,
unselected : ' ' ,
} )
return { config : value?.config }
} catch {
return { config : null }
}
}
2021-04-30 13:09:07 +02:00
async function lint (
baseDir : string ,
2021-06-09 00:46:00 +02:00
lintDirs : string [ ] ,
2021-04-30 13:09:07 +02:00
eslintrcFile : string | null ,
2021-06-10 11:02:50 +02:00
pkgJsonPath : string | null ,
2021-08-04 23:53:15 +02:00
lintDuringBuild : boolean = false ,
2021-06-18 15:17:53 +02:00
eslintOptions : any = null ,
2021-06-29 12:12:23 +02:00
reportErrorsOnly : boolean = false ,
2021-07-24 03:35:56 +02:00
maxWarnings : number = - 1 ,
formatter : string | null = null
2021-06-15 03:31:40 +02:00
) : Promise <
| string
| null
| {
output : string | null
isError : boolean
eventInfo : EventLintCheckCompleted
}
> {
2021-08-04 23:53:15 +02:00
try {
// Load ESLint after we're sure it exists:
const deps = await hasNecessaryDependencies ( baseDir , requiredPackages )
2021-06-08 23:29:34 +02:00
2021-08-04 23:53:15 +02:00
if ( deps . missing . some ( ( dep ) = > dep . pkg === 'eslint' ) ) {
Log . error (
` ESLint must be installed ${
lintDuringBuild ? ' in order to run during builds:' : ':'
} $ { chalk . bold . cyan (
2021-09-16 18:06:57 +02:00
( await isYarn ( baseDir ) )
2021-11-16 10:18:27 +01:00
? 'yarn add --dev eslint'
: 'npm install --save-dev eslint'
2021-08-04 23:53:15 +02:00
) } `
)
return null
}
2021-10-18 21:10:20 +02:00
const mod = await Promise . resolve ( require ( deps . resolved . get ( 'eslint' ) ! ) )
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
const { ESLint } = mod
let eslintVersion = ESLint ? . version ? ? mod ? . CLIEngine ? . version
2021-04-30 13:09:07 +02:00
2021-06-08 23:29:34 +02:00
if ( ! eslintVersion || semver . lt ( eslintVersion , '7.0.0' ) ) {
2021-06-10 11:02:50 +02:00
return ` ${ chalk . red (
'error'
) } - Your project has an older version of ESLint installed $ {
eslintVersion ? ' (' + eslintVersion + ')' : ''
2021-11-16 10:18:27 +01:00
} . Please upgrade to ESLint version 7 or above `
2021-04-30 13:09:07 +02:00
}
2021-06-08 23:29:34 +02:00
2021-08-04 23:53:15 +02:00
let options : any = {
useEslintrc : true ,
baseConfig : { } ,
errorOnUnmatchedPattern : false ,
extensions : [ '.js' , '.jsx' , '.ts' , '.tsx' ] ,
2021-08-23 19:56:21 +02:00
cache : true ,
2021-08-04 23:53:15 +02:00
. . . eslintOptions ,
}
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
let eslint = new ESLint ( options )
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
let nextEslintPluginIsEnabled = false
2022-02-24 21:43:28 +01:00
const nextRulesEnabled = new Map < string , Severity > ( )
2021-08-04 23:53:15 +02:00
const pagesDirRules = [ '@next/next/no-html-link-for-pages' ]
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
for ( const configFile of [ eslintrcFile , pkgJsonPath ] ) {
if ( ! configFile ) continue
const completeConfig : Config = await eslint . calculateConfigForFile (
configFile
)
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
if ( completeConfig . plugins ? . includes ( '@next/next' ) ) {
nextEslintPluginIsEnabled = true
2022-02-24 21:43:28 +01:00
for ( const [ name , [ severity ] ] of Object . entries ( completeConfig . rules ) ) {
if ( ! name . startsWith ( '@next/next/' ) ) {
continue
}
if (
typeof severity === 'number' &&
severity >= 0 &&
severity < VALID_SEVERITY . length
) {
nextRulesEnabled . set ( name , VALID_SEVERITY [ severity ] )
} else if (
typeof severity === 'string' &&
isValidSeverity ( severity )
) {
nextRulesEnabled . set ( name , severity )
}
}
2021-08-04 23:53:15 +02:00
break
}
2021-04-30 13:09:07 +02:00
}
2021-08-04 23:53:15 +02:00
const pagesDir = findPagesDir ( baseDir )
2021-06-03 14:01:24 +02:00
2021-08-04 23:53:15 +02:00
if ( nextEslintPluginIsEnabled ) {
let updatedPagesDir = false
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
for ( const rule of pagesDirRules ) {
if (
! options . baseConfig ! . rules ? . [ rule ] &&
! options . baseConfig ! . rules ? . [
rule . replace ( '@next/next' , '@next/babel-plugin-next' )
]
) {
if ( ! options . baseConfig ! . rules ) {
options . baseConfig ! . rules = { }
}
options . baseConfig ! . rules [ rule ] = [ 1 , pagesDir ]
updatedPagesDir = true
2021-04-30 13:09:07 +02:00
}
}
2021-08-04 23:53:15 +02:00
if ( updatedPagesDir ) {
eslint = new ESLint ( options )
}
} else {
Log . warn (
'The Next.js plugin was not detected in your ESLint configuration. See https://nextjs.org/docs/basic-features/eslint#migrating-existing-config'
)
2021-04-30 13:09:07 +02:00
}
2021-06-15 03:31:40 +02:00
2021-08-04 23:53:15 +02:00
const lintStart = process . hrtime ( )
let results = await eslint . lintFiles ( lintDirs )
let selectedFormatter = null
if ( options . fix ) await ESLint . outputFixes ( results )
if ( reportErrorsOnly ) results = await ESLint . getErrorResults ( results ) // Only return errors if --quiet flag is used
if ( formatter ) selectedFormatter = await eslint . loadFormatter ( formatter )
const formattedResult = formatResults (
baseDir ,
results ,
selectedFormatter ? . format
)
const lintEnd = process . hrtime ( lintStart )
const totalWarnings = results . reduce (
( sum : number , file : LintResult ) = > sum + file . warningCount ,
0
)
return {
output : formattedResult.output ,
isError :
ESLint . getErrorResults ( results ) ? . length > 0 ||
( maxWarnings >= 0 && totalWarnings > maxWarnings ) ,
eventInfo : {
durationInSeconds : lintEnd [ 0 ] ,
eslintVersion : eslintVersion ,
lintedFilesCount : results.length ,
lintFix : ! ! options . fix ,
2021-11-16 14:07:31 +01:00
nextEslintPluginVersion :
nextEslintPluginIsEnabled && deps . resolved . has ( 'eslint-config-next' )
? require ( path . join (
path . dirname ( deps . resolved . get ( 'eslint-config-next' ) ! ) ,
'package.json'
) ) . version
: null ,
2021-08-04 23:53:15 +02:00
nextEslintPluginErrorsCount : formattedResult.totalNextPluginErrorCount ,
nextEslintPluginWarningsCount :
formattedResult . totalNextPluginWarningCount ,
2022-02-24 21:43:28 +01:00
nextRulesEnabled : Object.fromEntries ( nextRulesEnabled ) ,
2021-08-04 23:53:15 +02:00
} ,
}
} catch ( err ) {
if ( lintDuringBuild ) {
Log . error (
2021-09-16 18:06:57 +02:00
` ESLint: ${
isError ( err ) && err . message ? err . message . replace ( /\n/g , ' ' ) : err
} `
2021-08-04 23:53:15 +02:00
)
return null
} else {
2022-01-11 21:40:03 +01:00
throw getProperError ( err )
2021-08-04 23:53:15 +02:00
}
2021-04-30 13:09:07 +02:00
}
}
export async function runLintCheck (
baseDir : string ,
2021-06-09 00:46:00 +02:00
lintDirs : string [ ] ,
2021-06-10 11:02:50 +02:00
lintDuringBuild : boolean = false ,
2021-06-18 15:17:53 +02:00
eslintOptions : any = null ,
2021-06-29 12:12:23 +02:00
reportErrorsOnly : boolean = false ,
2021-07-24 03:35:56 +02:00
maxWarnings : number = - 1 ,
2021-08-04 23:53:15 +02:00
formatter : string | null = null ,
strict : boolean = false
2021-06-15 03:31:40 +02:00
) : ReturnType < typeof lint > {
2021-04-30 13:09:07 +02:00
try {
// Find user's .eslintrc file
const eslintrcFile =
( await findUp (
[
'.eslintrc.js' ,
'.eslintrc.yaml' ,
'.eslintrc.yml' ,
'.eslintrc.json' ,
'.eslintrc' ,
] ,
{
cwd : baseDir ,
}
) ) ? ? null
const pkgJsonPath = ( await findUp ( 'package.json' , { cwd : baseDir } ) ) ? ? null
2021-06-03 14:01:24 +02:00
let packageJsonConfig = null
if ( pkgJsonPath ) {
const pkgJsonContent = await fs . readFile ( pkgJsonPath , {
encoding : 'utf8' ,
} )
packageJsonConfig = CommentJson . parse ( pkgJsonContent )
}
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
const config = await hasEslintConfiguration ( eslintrcFile , packageJsonConfig )
let deps
if ( config . exists ) {
// Run if ESLint config exists
return await lint (
baseDir ,
lintDirs ,
eslintrcFile ,
pkgJsonPath ,
lintDuringBuild ,
eslintOptions ,
reportErrorsOnly ,
maxWarnings ,
formatter
2021-06-03 14:01:24 +02:00
)
2021-08-04 23:53:15 +02:00
} else {
// Display warning if no ESLint configuration is present during "next build"
if ( lintDuringBuild ) {
Log . warn (
` No ESLint configuration detected. Run ${ chalk . bold . cyan (
'next lint'
) } to begin setup `
)
return null
} else {
// Ask user what config they would like to start with for first time "next lint" setup
const { config : selectedConfig } = strict
? ESLINT_PROMPT_VALUES . find (
( opt : { title : string } ) = > opt . title === 'Strict'
) !
: await cliPrompt ( )
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
if ( selectedConfig == null ) {
// Show a warning if no option is selected in prompt
Log . warn (
'If you set up ESLint yourself, we recommend adding the Next.js ESLint plugin. See https://nextjs.org/docs/basic-features/eslint#migrating-existing-config'
)
return null
} else {
// Check if necessary deps installed, and install any that are missing
deps = await hasNecessaryDependencies ( baseDir , requiredPackages )
if ( deps . missing . length > 0 )
await installDependencies ( baseDir , deps . missing , true )
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
// Write default ESLint config.
// Check for /pages and src/pages is to make sure this happens in Next.js folder
if (
existsSync ( path . join ( baseDir , 'pages' ) ) ||
existsSync ( path . join ( baseDir , 'src/pages' ) )
) {
await writeDefaultConfig (
baseDir ,
config ,
selectedConfig ,
eslintrcFile ,
pkgJsonPath ,
packageJsonConfig
)
}
}
2021-04-30 13:09:07 +02:00
2021-08-04 23:53:15 +02:00
Log . ready (
` ESLint has successfully been configured. Run ${ chalk . bold . cyan (
'next lint'
) } again to view warnings and errors . `
)
return null
}
}
2021-04-30 13:09:07 +02:00
} catch ( err ) {
throw err
}
}