Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ESLint] Introduce a new setup process when next lint is run for the first time #26584

Merged
merged 15 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/basic-features/eslint.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ If either of the two configuration options are selected, Next.js will automatica

You can now run `next lint` every time you want to run ESLint to catch errors. Once ESLint has been set up, it will also automatically run during every build (`next build`). Errors will fail the build, while warnings will not.

> If you do not want ESLint to run as a build step, refer to the documentation for [Ignoring ESLint](/docs/api-reference/next.config.js/ignoring-eslint.md).
> If you do not want ESLint to run during `next build`, refer to the documentation for [Ignoring ESLint](/docs/api-reference/next.config.js/ignoring-eslint.md).

We recommend using an appropriate [integration](https://eslint.org/docs/user-guide/integrations#editors) to view warnings and errors directly in your code editor during development.

Expand Down Expand Up @@ -115,7 +115,7 @@ module.exports = {
Similarly, the `--dir` flag can be used for `next lint`:

```bash
yarn lint --dir pages --dir utils
next lint --dir pages --dir utils
```

## Disabling Rules
Expand Down
8 changes: 7 additions & 1 deletion packages/next/cli/next-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const nextLint: cliCommand = async (argv) => {
'--help': Boolean,
'--base-dir': String,
'--dir': [String],
'--strict': Boolean,

// Aliases
'-h': '--help',
Expand Down Expand Up @@ -100,6 +101,9 @@ const nextLint: cliCommand = async (argv) => {
--ext [String] Specify JavaScript file extensions - default: .js, .jsx, .ts, .tsx
--resolve-plugins-relative-to path::String A folder where plugins should be resolved from, CWD by default

Initial setup:
--strict Creates an .eslintrc file using the Next.js strict configuration automatically (can only be done when no .eslintrc file is present)
styfle marked this conversation as resolved.
Show resolved Hide resolved

Specifying rules:
--rulesdir [path::String] Use additional rules from this directory

Expand Down Expand Up @@ -156,6 +160,7 @@ const nextLint: cliCommand = async (argv) => {
const reportErrorsOnly = Boolean(args['--quiet'])
const maxWarnings = args['--max-warnings'] ?? -1
const formatter = args['--format'] || null
const strict = Boolean(args['--strict'])

runLintCheck(
baseDir,
Expand All @@ -164,7 +169,8 @@ const nextLint: cliCommand = async (argv) => {
eslintOptions(args),
reportErrorsOnly,
maxWarnings,
formatter
formatter,
strict
)
.then(async (lintResults) => {
const lintOutput =
Expand Down
4 changes: 2 additions & 2 deletions packages/next/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const ESLINT_PROMPT_VALUES = [
},
},
{
title: 'None',
title: 'Cancel',
config: null,
},
]
] as any
styfle marked this conversation as resolved.
Show resolved Hide resolved
33 changes: 17 additions & 16 deletions packages/next/lib/eslint/runLintCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,16 @@ async function lint(
const mod = await import(deps.resolved.get('eslint')!)

const { ESLint } = mod
let eslintVersion = ESLint?.version

if (!ESLint) {
eslintVersion = mod?.CLIEngine?.version

if (!eslintVersion || semver.lt(eslintVersion, '7.0.0')) {
return `${chalk.red(
'error'
)} - Your project has an older version of ESLint installed${
eslintVersion ? ' (' + eslintVersion + ')' : ''
}. Please upgrade to ESLint version 7 or later`
}

return null
let eslintVersion = ESLint?.version ?? mod?.CLIEngine?.version

if (!eslintVersion || semver.lt(eslintVersion, '7.0.0')) {
return `${chalk.red(
'error'
)} - Your project has an older version of ESLint installed${
eslintVersion ? ' (' + eslintVersion + ')' : ''
}. Please upgrade to ESLint version 7 or later`
}

let options: any = {
useEslintrc: true,
baseConfig: {},
Expand Down Expand Up @@ -231,7 +226,8 @@ export async function runLintCheck(
eslintOptions: any = null,
reportErrorsOnly: boolean = false,
maxWarnings: number = -1,
formatter: string | null = null
formatter: string | null = null,
strict: boolean = false
): ReturnType<typeof lint> {
try {
// Find user's .eslintrc file
Expand Down Expand Up @@ -285,7 +281,11 @@ export async function runLintCheck(
return null
} else {
// Ask user what config they would like to start with for first time "next lint" setup
const { config: selectedConfig } = await cliPrompt()
const { config: selectedConfig } = strict
? ESLINT_PROMPT_VALUES.find(
(opt: { title: string }) => opt.title === 'Strict'
)
: await cliPrompt()

if (selectedConfig == null) {
// Show a warning if no option is selected in prompt
Expand All @@ -306,6 +306,7 @@ export async function runLintCheck(
existsSync(path.join(baseDir, 'src/pages'))
) {
await writeDefaultConfig(
baseDir,
config,
selectedConfig,
eslintrcFile,
Expand Down
5 changes: 3 additions & 2 deletions packages/next/lib/eslint/writeDefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ConfigAvailable } from './hasEslintConfiguration'
import * as Log from '../../build/output/log'

export async function writeDefaultConfig(
baseDir: string,
{ exists, emptyEslintrc, emptyPkgJsonConfig }: ConfigAvailable,
selectedConfig: any,
eslintrcFile: string | null,
Expand Down Expand Up @@ -51,10 +52,10 @@ export async function writeDefaultConfig(
)
} else if (!exists) {
await fs.writeFile(
'.eslintrc.json',
path.join(baseDir, '.eslintrc.json'),
CommentJson.stringify(selectedConfig, null, 2) + os.EOL
)

console.log(path.join(baseDir, '.eslintrc.json'))
styfle marked this conversation as resolved.
Show resolved Hide resolved
console.log(
chalk.green(
`We created the ${chalk.bold(
Expand Down
9 changes: 9 additions & 0 deletions test/integration/eslint/no-config/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default class Test {
render() {
return (
<div>
<h1>Hello title</h1>
</div>
)
}
}
95 changes: 79 additions & 16 deletions test/integration/eslint/test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from 'fs-extra'
import { join } from 'path'
import os from 'os'
import execa from 'execa'

import { writeFile } from 'fs-extra'
import { dirname, join } from 'path'

import findUp from 'next/dist/compiled/find-up'
import { nextBuild, nextLint } from 'next-test-utils'
Expand All @@ -26,12 +27,13 @@ const dirMaxWarnings = join(__dirname, '../max-warnings')
const dirEmptyDirectory = join(__dirname, '../empty-directory')
const dirEslintIgnore = join(__dirname, '../eslint-ignore')
const dirNoEslintPlugin = join(__dirname, '../no-eslint-plugin')
const dirNoConfig = join(__dirname, '../no-config')

describe('ESLint', () => {
describe('Next Build', () => {
test('first time setup', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
styfle marked this conversation as resolved.
Show resolved Hide resolved
await writeFile(eslintrc, '')
await fs.writeFile(eslintrc, '')

const { stdout, stderr } = await nextBuild(dirFirstTimeSetup, [], {
stdout: true,
Expand Down Expand Up @@ -144,21 +146,82 @@ describe('ESLint', () => {
})

describe('Next Lint', () => {
test('show a prompt to set up ESLint if no configuration detected', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await writeFile(eslintrc, '')
describe('First Time Setup ', () => {
async function nextLintTemp() {
const folder = join(
os.tmpdir(),
Math.random().toString(36).substring(2)
)
await fs.mkdirp(folder)
await fs.copy(dirNoConfig, folder)

try {
const nextDir = dirname(require.resolve('next/package'))
const nextBin = join(nextDir, 'dist/bin/next')

const { stdout } = await execa('node', [
nextBin,
'lint',
folder,
'--strict',
])

const pkgJson = JSON.parse(
await fs.readFile(join(folder, 'package.json'), 'utf8')
)
const eslintrcJson = JSON.parse(
await fs.readFile(join(folder, '.eslintrc.json'), 'utf8')
)

return { stdout, pkgJson, eslintrcJson }
} finally {
await fs.remove(folder)
}
}

const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], {
stdout: true,
stderr: true,
test('show a prompt to set up ESLint if no configuration detected', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await fs.writeFile(eslintrc, '')

const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], {
stdout: true,
stderr: true,
})
const output = stdout + stderr
expect(output).toContain('How would you like to configure ESLint?')

// Different options that can be selected
expect(output).toContain('Strict (recommended)')
expect(output).toContain('Base')
expect(output).toContain('Cancel')
})
const output = stdout + stderr
expect(output).toContain('How would you like to configure ESLint?')

// Different options that can be selected
expect(output).toContain('Strict (recommended)')
expect(output).toContain('Base')
expect(output).toContain('None')
test('installs eslint and eslint-config-next as devDependencies if missing', async () => {
const { stdout, pkgJson } = await nextLintTemp()

expect(stdout.replace(/(\r\n|\n|\r)/gm, '')).toContain(
'Installing devDependencies:- eslint- eslint-config-next'
)
expect(pkgJson.devDependencies).toHaveProperty('eslint')
expect(pkgJson.devDependencies).toHaveProperty('eslint-config-next')
})

test('creates .eslintrc.json file with a default configuration', async () => {
const { stdout, eslintrcJson } = await nextLintTemp()

expect(stdout).toContain(
'We created the .eslintrc.json file for you and included your selected configuration'
)
expect(eslintrcJson).toMatchObject({ extends: 'next/core-web-vitals' })
})

test('shows a successful message when completed', async () => {
const { stdout, eslintrcJson } = await nextLintTemp()

expect(stdout).toContain(
'ESLint has successfully been configured. Run next lint again to view warnings and errors'
)
})
})

test('shows warnings and errors', async () => {
Expand Down Expand Up @@ -216,7 +279,7 @@ describe('ESLint', () => {

test('success message when no warnings or errors', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await writeFile(eslintrc, '{ "extends": "next", "root": true }')
await fs.writeFile(eslintrc, '{ "extends": "next", "root": true }')

const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], {
stdout: true,
Expand Down