Skip to content
Permalink
Browse files

Add ESLint to Next.js (#22437)

For #22228

This PR:

- Adds ESLint to toolchain
  - Included by default for builds (`next build`)
  - Can be enabled for development (`next dev`)
  - Custom formatter built for output
- Adds appropriate tests
- Adds two documentation pages
  • Loading branch information
housseindjirdeh committed Mar 23, 2021
1 parent c219c1d commit e5ef60fecb142c149e0307376a1996f941b2840d
Showing with 900 additions and 25 deletions.
  1. +2 −0 .eslintignore
  2. +70 −0 docs/api-reference/next.config.js/eslint-warnings-errors.md
  3. +50 −0 docs/basic-features/eslint.md
  4. +8 −0 docs/manifest.json
  5. +14 −8 packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js
  6. +14 −0 packages/eslint-plugin-next/lib/utils/url.js
  7. +35 −0 packages/next/build/index.ts
  8. +68 −0 packages/next/lib/eslint/customFormatter.ts
  9. +141 −0 packages/next/lib/verifyAndLint.ts
  10. +4 −0 packages/next/next-server/server/config-shared.ts
  11. +5 −1 packages/next/package.json
  12. +1 −0 packages/next/server/hot-reloader.ts
  13. +12 −0 packages/next/server/on-demand-entry-handler.ts
  14. +3 −0 test-pnp.sh
  15. +8 −3 test/integration/dist-dir/test/index.test.js
  16. +16 −0 test/integration/eslint/custom-eslint-config/.eslintrc.json
  17. +13 −0 test/integration/eslint/custom-eslint-config/pages/index.js
  18. +14 −0 test/integration/eslint/custom-next-config/.eslintrc.json
  19. +6 −0 test/integration/eslint/custom-next-config/next.config.js
  20. +13 −0 test/integration/eslint/custom-next-config/pages/index.js
  21. +36 −0 test/integration/eslint/pkg-json-eslint-config/package.json
  22. +13 −0 test/integration/eslint/pkg-json-eslint-config/pages/index.js
  23. +37 −0 test/integration/eslint/test/custom-eslint-config.test.js
  24. +55 −0 test/integration/eslint/test/custom-next-config.test.js
  25. +44 −0 test/integration/eslint/test/pkg-json-eslint-config.test.js
  26. +1 −1 test/integration/index-index/pages/index/index.js
  27. +1 −1 test/integration/index-index/pages/index/index/index.js
  28. +1 −1 test/integration/index-index/pages/index/project/index.js
  29. +1 −1 test/integration/index-index/pages/index/user.js
  30. +5 −0 test/integration/production-build-dir/next.config.js
  31. +2 −1 test/integration/production/pages/error-in-ssr-render.js
  32. +2 −2 test/integration/tsconfig-verifier/pages/index.tsx
  33. +3 −0 test/integration/webpack-require-hook/next.config.js
  34. +202 −6 yarn.lock
@@ -2,6 +2,7 @@ node_modules
**/.next/**
**/_next/**
**/dist/**
e2e-tests/**
examples/with-typescript-eslint-jest/**
examples/with-kea/**
packages/next/bundles/webpack/packages/*.runtime.js
@@ -16,4 +17,5 @@ packages/next-codemod/**/*.js
packages/next-codemod/**/*.d.ts
packages/next-env/**/*.d.ts
test/integration/async-modules/**
test/integration/eslint/**
test-timings.json
@@ -0,0 +1,70 @@
---
description: Learn how to opt-in and out of ESLint during development mode and production builds.
---

# ESLint Warnings and Errors

## During builds

Next.js fails your **production build** (`next build`) when ESLint errors are present in your
project.

If you'd like Next.js to dangerously produce production code even when your application has errors,
you can disable ESLint running during the build process.

> It's recommended to run ESLint as part of the production build process to ensure your application
> is resilient against runtime issues.
Open `next.config.js` and disable the `build` option in the `eslint` config:

```js
module.exports = {
eslint: {
// !! WARN !!
// Dangerously allow production builds to successfully complete even if
// your project has ESLint errors.
// !! WARN !!
build: false,
},
}
```

## During development

By default, Next.js does not run ESLint during **development** (`next dev`).

If you would like Next.js to lint files separately in development mode, you can enable it in your
configuration.

> Enabling ESLint during development mode will slow down how fast pages are compiled. Until this is
> optimized, we recommend that you [integrate ESLint in your code
> editor](https://eslint.org/docs/user-guide/integrations#editors).
Open `next.config.js` and enable the `dev` option in the `eslint` config:

```js
module.exports = {
eslint: {
// !! WARN !!
// This can slow down how long pages take to compile during development
// !! WARN !!
dev: true,
},
}
```

## Related

<div class="card">
<a href="/docs/api-reference/next.config.js/introduction.md">
<b>Introduction to next.config.js:</b>
<small>Learn more about the configuration file used by Next.js.</small>
</a>
</div>

<div class="card">
<a href="/docs/basic-features/eslint.md">
<b>ESLint:</b>
<small>Learn more about how to use ESLint in Next.js.</small>
</a>
</div>
@@ -0,0 +1,50 @@
---
description: Next.js uses ESLint to find and resolve issues affecting the user or developer
experience.
---

# ESLint

Next.js uses [ESLint](https://eslint.org/) to find and resolve issues affecting the user or
developer experience. A minimal set of Next.js rules are provided by default, but can be extended by
adding an `.eslintrc` file to your project.

## Default configuration

By default, Next.js provides a set of [recommended ESLint
rules](https://github.com/vercel/next.js/blob/canary/packages/eslint-plugin-next/lib/index.js#L10-L18)
that are automatically linted against as part of `next build`. If you would like to control which
ESLint rules are checked during builds, you will need to add an `.eslintrc` file to the root of your
project.

Here's an example of an `.eslintrc.json` file:

```json
{
"extends": ["plugin:@next/next/recommended"],
"parser": "@babel/eslint-parser",
"parserOptions": {
"requireConfigFile": false,
"sourceType": "module",
"babelOptions": {
"presets": ["next/babel"]
}
}
}
```

- Extending the original base of rules (`plugin:@next/next/recommended`) is highly recommended to
catch and fix significant Next.js issues in your application
- Including `@babel/eslint-parser` with the `next/babel` preset ensures that all language features
supported by Next.js will also be supported by ESLint. Although `@babel/eslint-parser` can parse
TypeScript, consider using
[`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser)
if you have TypeScript enabled in your application to check for type-specific linting rules.

> If you add an `.eslintrc` file to your application and don't include
> `plugin:@next/next/recommended`in the config, its rules will not be checked during development or
> production builds. This is **not recommended**.
If you want to enable ESLint to run during development, or disable it for production builds; refer
to the documentation for [ESLint Warnings and
Errors](/docs/api-reference/next.config.js/eslint-warnings-errors.md).
@@ -33,6 +33,10 @@
"title": "Fast Refresh",
"path": "/docs/basic-features/fast-refresh.md"
},
{
"title": "ESLint",
"path": "/docs/basic-features/eslint.md"
},
{
"title": "TypeScript",
"path": "/docs/basic-features/typescript.md"
@@ -336,6 +340,10 @@
"title": "Configuring onDemandEntries",
"path": "/docs/api-reference/next.config.js/configuring-onDemandEntries.md"
},
{
"title": "ESLint Warnings and Errors",
"path": "/docs/api-reference/next.config.js/eslint-warnings-errors.md"
},
{
"title": "Ignoring TypeScript Errors",
"path": "/docs/api-reference/next.config.js/ignoring-typescript-errors.md"
@@ -1,10 +1,18 @@
const path = require('path')
const fs = require('fs')
const { getUrlFromPagesDirectory, normalizeURL } = require('../utils/url')
const {
getUrlFromPagesDirectory,
normalizeURL,
execOnce,
} = require('../utils/url')

const pagesDirWarning = execOnce((pagesDirs) => {
console.warn(
`Pages directory cannot be found at ${pagesDirs.join(' or ')}. ` +
`If using a custom path, please configure with the no-html-link-for-pages rule in your eslint config file`
)
})

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
@@ -26,10 +34,8 @@ module.exports = {
]
const pagesDir = pagesDirs.find((dir) => fs.existsSync(dir))
if (!pagesDir) {
throw new Error(
`Pages directory cannot be found at ${pagesDirs.join(' or ')}. ` +
`If using a custom path, please configure with the no-html-link-for-pages rule`
)
pagesDirWarning(pagesDirs)
return {}
}

const urls = getUrlFromPagesDirectory('/', pagesDir)
@@ -76,7 +76,21 @@ function normalizeURL(url) {
return url
}

function execOnce(fn) {
let used = false
let result

return (...args) => {
if (!used) {
used = true
result = fn(...args)
}
return result
}
}

module.exports = {
getUrlFromPagesDirectory,
normalizeURL,
execOnce,
}
@@ -175,6 +175,41 @@ export default async function build(
telemetry.record(events)
)

if (config.eslint?.build) {
await nextBuildSpan
.traceChild('verify-and-lint')
.traceAsyncFn(async () => {
const lintWorkers = new Worker(
require.resolve('../lib/verifyAndLint'),
{
numWorkers: config.experimental.cpus,
enableWorkerThreads: config.experimental.workerThreads,
}
) as Worker & {
verifyAndLint: typeof import('../lib/verifyAndLint').verifyAndLint
}

lintWorkers.getStdout().pipe(process.stdout)
lintWorkers.getStderr().pipe(process.stderr)

const lintResults = await lintWorkers.verifyAndLint(
dir,
pagesDir,
null
)

if (lintResults.hasErrors) {
console.error(chalk.red('Failed to compile.'))
console.error(lintResults.results)
process.exit(1)
} else if (lintResults.hasMessages) {
console.log(lintResults.results)
}

lintWorkers.end()
})
}

const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors)
await nextBuildSpan
.traceChild('verify-typescript-setup')
@@ -0,0 +1,68 @@
import { ESLint, Linter } from 'eslint'

import chalk from 'chalk'
import path from 'path'

export enum MessageSeverity {
Warning = 1,
Error = 2,
}

function formatMessage(
dir: string,
messages: Linter.LintMessage[],
filePath: string
): string | void {
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: ESLint.LintResult[]
): string {
return (
results
.filter(({ messages }) => messages?.length)
.map(({ messages, filePath }) =>
formatMessage(baseDir, messages, filePath)
)
.join('\n') + '\n'
)
}

0 comments on commit e5ef60f

Please sign in to comment.