next lint + ESLint in Create Next App (#25064)

Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
Co-authored-by: Tim Neutkens <timneutkens@me.com>
This commit is contained in:
Houssein Djirdeh 2021-06-03 05:01:24 -07:00 committed by GitHub
parent b05719f928
commit bbc28ccae0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 483 additions and 202 deletions

View file

@ -21,7 +21,7 @@ Usage
$ next <command>
Available commands
build, start, export, dev, telemetry
build, start, export, dev, lint, telemetry
Options
--version, -v Version number
@ -84,6 +84,16 @@ The application will start at `http://localhost:3000` by default. The default po
npx next start -p 4000
```
## Lint
`next lint` runs ESLint for all files in the `pages` directory and provides a guided setup to install any required dependencies if ESLint is not already configured in your application.
You can also run ESLint on other directories with the `--dir` flag:
```bash
next lint --dir components
```
## Telemetry
Next.js collects **completely anonymous** telemetry data about general usage.

View file

@ -0,0 +1,119 @@
---
description: Next.js supports ESLint by default. You can get started with ESLint in Next.js here.
---
# ESLint
Since version **11.0.0**, Next.js provides an integrated [ESLint](https://eslint.org/) experience out of the box. To get started, run `next lint`:
```bash
next lint
```
If you don't already have ESLint configured in your application, you will be guided through the installation of any required packages.
```bash
next lint
# You'll see instructions like these:
#
# Please install eslint and eslint-config-next by running:
#
# yarn add --dev eslint eslint-config-next
#
# ...
```
If no ESLint configuration is present, Next.js will create an `.eslintrc` file in the root of your project and automatically configure it with the base configuration:
```
{
"extends": "next"
}
```
Now you can run `next lint` every time you want to run ESLint to catch errors
> The default base configuration (`"extends": "next"`) can be updated at any time and will only be included if no ESLint configuration is present.
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.
## Linting During Builds
Once ESLint has been set up, it will 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, it can be disabled using the `--no-lint` flag:
```bash
next build --no-lint
```
This is not recommended unless you have configured ESLint to run in a separate part of your workflow (for example, in CI or a pre-commit hook).
## Linting Custom Directories
By default, Next.js will only run ESLint for all files in the `pages/` directory. However, you can specify other custom directories to run by using the `--dir` flag in `next lint`:
```bash
next lint --dir components --dir lib
```
## ESLint Plugin
Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next), that makes it easier to catch common issues and problems in a Next.js application. The full set of rules can be found in the [package repository](https://github.com/vercel/next.js/tree/master/packages/eslint-plugin-next/lib/rules).
## Base Configuration
The Next.js base ESLint configuration is automatically generated when `next lint` is run for the first time:
```
{
"extends": "next"
}
```
This configuration extends recommended rule sets from various Eslint plugins:
- [`eslint-plugin-react`](https://www.npmjs.com/package/eslint-plugin-react)
- [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks)
- [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next)
You can see the full details of the shareable configuration in the [`eslint-config-next`](https://www.npmjs.com/package/eslint-config-next) package.
If you would like to modify any rules provided by the supported plugins (`react`, `react-hooks`, `next`), you can directly modify them using the `rules` property:
```
{
"extends": "next",
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "error",
}
}
```
> **Note**: If you need to also include a separate, custom ESLint configuration, it is highly recommended that `eslint-config-next` is extended last after other configurations. For example:
>
> ```
> {
> "extends": ["eslint:recommended", "next"]
> }
> ```
>
> The `next` configuration already handles setting default values for the `parser`, `plugins` and `settings` properties.
> There is no need to manually re-declare any of these properties unless you need a different configuration for your use case.
> If you include any other shareable configurations, you will need to make sure that these properties are not overwritten or modified.
### Core Web Vitals
A stricter `next/core-web-vitals` entrypoint can also be specified in `.eslintrc`:
```
{
"extends": ["next", "next/core-web-vitals"]
}
```
`next/core-web-vitals` updates `eslint-plugin-next` to error on a number of rules that are warnings by default if they affect [Core Web Vitals](https://web.dev/vitals/).
> Both `next` and `next/core-web-vitals` entry points are automatically included for new applications built with [Create Next App](/docs/api-reference/create-next-app.md).

View file

@ -55,7 +55,8 @@ Open `package.json` and add the following `scripts`:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"lint": "next lint"
}
```
@ -64,6 +65,7 @@ These scripts refer to the different stages of developing an application:
- `dev` - Runs [`next dev`](/docs/api-reference/cli.md#development) which starts Next.js in development mode
- `build` - Runs [`next build`](/docs/api-reference/cli.md#build) which builds the application for production usage
- `start` - Runs [`next start`](/docs/api-reference/cli.md#production) which starts a Next.js production server
- `lint` - Runs [`next lint`](/docs/api-reference/cli.md#lint) which sets up Next.js' built-in ESLint configuration
Next.js is built around the concept of [pages](/docs/basic-features/pages.md). A page is a [React Component](https://reactjs.org/docs/components-and-props.html) exported from a `.js`, `.jsx`, `.ts`, or `.tsx` file in the `pages` directory.

View file

@ -37,6 +37,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"

View file

@ -187,6 +187,7 @@ export async function createApp({
dev: 'next dev',
build: 'next build',
start: 'next start',
lint: 'next lint',
},
}
/**
@ -207,7 +208,7 @@ export async function createApp({
/**
* Default devDependencies.
*/
const devDependencies = []
const devDependencies = ['eslint', 'eslint-config-next']
/**
* TypeScript projects will have type definitions and other devDependencies.
*/
@ -250,7 +251,8 @@ export async function createApp({
cwd: path.join(__dirname, 'templates', template),
rename: (name) => {
switch (name) {
case 'gitignore': {
case 'gitignore':
case 'eslintrc': {
return '.'.concat(name)
}
// README.md is ignored by webpack-asset-relocator-loader used by ncc:

View file

@ -0,0 +1,3 @@
{
"extends": ["next", "next/core-web-vitals"]
}

View file

@ -1,5 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default (req, res) => {
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}

View file

@ -0,0 +1,3 @@
{
"extends": ["next", "next/core-web-vitals"]
}

View file

@ -5,6 +5,9 @@ type Data = {
name: string
}
export default (req: NextApiRequest, res: NextApiResponse<Data>) => {
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

View file

@ -0,0 +1,8 @@
module.exports = {
extends: ['.'].map(require.resolve),
rules: {
'@next/next/no-sync-scripts': 2,
'@next/next/no-html-link-for-pages': 2,
'@next/next/no-img-element': 2,
},
}

View file

@ -16,6 +16,7 @@ module.exports = {
rules: {
'import/no-anonymous-default-export': 'warn',
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'jsx-a11y/alt-text': [
'warn',
{

View file

@ -18,7 +18,7 @@ module.exports = function (context) {
context.report({
node,
message:
'Synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
'External synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
})
}
},

View file

@ -20,6 +20,7 @@ const commands: { [command: string]: () => Promise<cliCommand> } = {
start: () => import('../cli/next-start').then((i) => i.nextStart),
export: () => import('../cli/next-export').then((i) => i.nextExport),
dev: () => import('../cli/next-dev').then((i) => i.nextDev),
lint: () => import('../cli/next-lint').then((i) => i.nextLint),
telemetry: () => import('../cli/next-telemetry').then((i) => i.nextTelemetry),
}

View file

@ -119,7 +119,8 @@ export default async function build(
dir: string,
conf = null,
reactProductionProfiling = false,
debugOutput = false
debugOutput = false,
runLint = true
): Promise<void> {
const nextBuildSpan = trace('next-build')
@ -212,13 +213,12 @@ export default async function build(
typeCheckingSpinner.stopAndPersist()
}
if (config.experimental.eslint) {
if (runLint) {
await nextBuildSpan
.traceChild('verify-and-lint')
.traceAsyncFn(async () => {
await verifyAndLint(
dir,
pagesDir,
config.experimental.cpus,
config.experimental.workerThreads
)

View file

@ -13,6 +13,7 @@ const nextBuild: cliCommand = (argv) => {
'--help': Boolean,
'--profile': Boolean,
'--debug': Boolean,
'--no-lint': Boolean,
// Aliases
'-h': '--help',
'-d': '--debug',
@ -41,6 +42,7 @@ const nextBuild: cliCommand = (argv) => {
Options
--profile Can be used to enable React Production Profiling
--no-lint Disable linting
`,
0
)
@ -48,6 +50,9 @@ const nextBuild: cliCommand = (argv) => {
if (args['--profile']) {
Log.warn('Profiling is enabled. Note: This may affect performance')
}
if (args['--no-lint']) {
Log.warn('Linting is disabled')
}
const dir = resolve(args._[0] || '.')
// Check if the provided directory exists
@ -93,7 +98,9 @@ const nextBuild: cliCommand = (argv) => {
}
return preflight()
.then(() => build(dir, null, args['--profile'], args['--debug']))
.then(() =>
build(dir, null, args['--profile'], args['--debug'], !args['--no-lint'])
)
.catch((err) => {
console.error('')
console.error('> Build error occurred')

76
packages/next/cli/next-lint.ts Executable file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env node
import { existsSync } from 'fs'
import arg from 'next/dist/compiled/arg/index.js'
import { resolve, join } from 'path'
import { cliCommand } from '../bin/next'
import { runLintCheck } from '../lib/eslint/runLintCheck'
import { printAndExit } from '../server/lib/utils'
const nextLint: cliCommand = (argv) => {
const validArgs: arg.Spec = {
// Types
'--help': Boolean,
'--dir': [String],
// Aliases
'-h': '--help',
'-d': '--dir',
}
let args: arg.Result<arg.Spec>
try {
args = arg(validArgs, { argv })
} catch (error) {
if (error.code === 'ARG_UNKNOWN_OPTION') {
return printAndExit(error.message, 1)
}
throw error
}
if (args['--help']) {
printAndExit(
`
Description
Run ESLint on every file in specified directories.
If not configured, ESLint will be set up for the first time.
Usage
$ next lint <baseDir> [options]
<baseDir> represents the directory of the Next.js application.
If no directory is provided, the current directory will be used.
Options
-h - list this help
-d - set directory, or directories, to run ESLint (defaults to only 'pages')
`,
0
)
}
const baseDir = resolve(args._[0] || '.')
// Check if the provided directory exists
if (!existsSync(baseDir)) {
printAndExit(`> No such directory exists as the project root: ${baseDir}`)
}
const dirs: string[] = args['--dir']
const lintDirs = dirs
? dirs.reduce((res: string[], d: string) => {
const currDir = join(baseDir, d)
if (!existsSync(currDir)) return res
res.push(currDir)
return res
}, [])
: null
runLintCheck(baseDir, lintDirs)
.then((results) => {
if (results) console.log(results)
})
.catch((err) => {
printAndExit(err.message)
})
}
export { nextLint }

View file

@ -1,32 +0,0 @@
import { promises as fs } from 'fs'
import * as CommentJson from 'next/dist/compiled/comment-json'
export type LintIntent = { firstTimeSetup: boolean }
export async function getLintIntent(
eslintrcFile: string | null,
pkgJsonEslintConfig: string | null
): Promise<LintIntent | false> {
if (eslintrcFile) {
const content = await fs.readFile(eslintrcFile, { encoding: 'utf8' }).then(
(txt) => txt.trim().replace(/\n/g, ''),
() => null
)
// User is setting up ESLint for the first time setup if eslint config exists but is empty
return {
firstTimeSetup:
content === '' ||
content === '{}' ||
content === '---' ||
content === 'module.exports = {}',
}
} else if (pkgJsonEslintConfig) {
return {
firstTimeSetup: CommentJson.stringify(pkgJsonEslintConfig) === '{}',
}
}
return false
}

View file

@ -1,13 +1,14 @@
import { promises } from 'fs'
import { extname } from 'path'
import { promises as fs } from 'fs'
import chalk from 'chalk'
import findUp from 'next/dist/compiled/find-up'
import semver from 'next/dist/compiled/semver'
import * as CommentJson from 'next/dist/compiled/comment-json'
import { formatResults } from './customFormatter'
import { getLintIntent } from './getLintIntent'
import { writeDefaultConfig } from './writeDefaultConfig'
import { getPackageVersion } from '../get-package-version'
import { findPagesDir } from '../find-pages-dir'
import { CompileError } from '../compile-error'
import {
@ -22,12 +23,14 @@ type Config = {
rules: { [key: string]: Array<number | string> }
}
const linteableFileTypes = ['jsx', 'js', 'ts', 'tsx']
const linteableFiles = (dir: string) => {
return `${dir}/**/*.{${['jsx', 'js', 'ts', 'tsx'].join(',')}}`
}
async function lint(
deps: NecessaryDependencies,
baseDir: string,
pagesDir: string,
lintDirs: string[] | null,
eslintrcFile: string | null,
pkgJsonPath: string | null
): Promise<string | null> {
@ -41,8 +44,8 @@ async function lint(
})
if (eslintVersion && semver.lt(eslintVersion, '7.0.0')) {
Log.warn(
`Your project has an older version of ESLint installed (${eslintVersion}). Please upgrade to v7 or later to run ESLint during the build process.`
Log.error(
`Your project has an older version of ESLint installed (${eslintVersion}). Please upgrade to v7 or later`
)
}
return null
@ -70,6 +73,8 @@ async function lint(
}
}
const pagesDir = findPagesDir(baseDir)
if (nextEslintPluginIsEnabled) {
let updatedPagesDir = false
@ -93,9 +98,12 @@ async function lint(
}
}
const results = await eslint.lintFiles([
`${pagesDir}/**/*.{${linteableFileTypes.join(',')}}`,
])
// If no directories to lint are provided, only the pages directory will be linted
const filesToLint = lintDirs
? lintDirs.map(linteableFiles)
: linteableFiles(pagesDir)
const results = await eslint.lintFiles(filesToLint)
if (ESLint.getErrorResults(results)?.length > 0) {
throw new CompileError(await formatResults(baseDir, results))
@ -105,19 +113,10 @@ async function lint(
export async function runLintCheck(
baseDir: string,
pagesDir: string
lintDirs: string[] | null,
lintDuringBuild: boolean = false
): Promise<string | null> {
try {
// Check if any pages exist that can be linted
const pages = await promises.readdir(pagesDir)
if (
!pages.some((page) =>
linteableFileTypes.includes(extname(page).replace('.', ''))
)
) {
return null
}
// Find user's .eslintrc file
const eslintrcFile =
(await findUp(
@ -134,33 +133,39 @@ export async function runLintCheck(
)) ?? null
const pkgJsonPath = (await findUp('package.json', { cwd: baseDir })) ?? null
const { eslintConfig: pkgJsonEslintConfig = null } = !!pkgJsonPath
? await import(pkgJsonPath!)
: {}
// Check if the project uses ESLint
const eslintIntent = await getLintIntent(eslintrcFile, pkgJsonEslintConfig)
if (!eslintIntent) {
return null
let packageJsonConfig = null
if (pkgJsonPath) {
const pkgJsonContent = await fs.readFile(pkgJsonPath, {
encoding: 'utf8',
})
packageJsonConfig = CommentJson.parse(pkgJsonContent)
}
const firstTimeSetup = eslintIntent.firstTimeSetup
// Warning displayed if no ESLint configuration is present during build
if (lintDuringBuild && !eslintrcFile && !packageJsonConfig.eslintConfig) {
Log.warn(
`No ESLint configuration detected. Run ${chalk.bold.cyan(
'next lint'
)} to begin setup`
)
return null
}
// Ensure ESLint and necessary plugins and configs are installed:
const deps: NecessaryDependencies = await hasNecessaryDependencies(
baseDir,
false,
!!eslintIntent,
eslintrcFile
true,
eslintrcFile ?? '',
!!packageJsonConfig.eslintConfig,
lintDuringBuild
)
// Create the user's eslintrc config for them
if (firstTimeSetup) await writeDefaultConfig(eslintrcFile, pkgJsonPath)
// Write default ESLint config if none is present
await writeDefaultConfig(eslintrcFile, pkgJsonPath, packageJsonConfig)
// Run ESLint
return await lint(deps, baseDir, pagesDir, eslintrcFile, pkgJsonPath)
return await lint(deps, baseDir, lintDirs, eslintrcFile, pkgJsonPath)
} catch (err) {
throw err
}

View file

@ -7,58 +7,79 @@ import * as CommentJson from 'next/dist/compiled/comment-json'
export async function writeDefaultConfig(
eslintrcFile: string | null,
pkgJsonPath: string | null
pkgJsonPath: string | null,
packageJsonConfig: { eslintConfig: any } | null
) {
const defaultConfig = {
extends: 'next',
}
if (eslintrcFile) {
const ext = path.extname(eslintrcFile)
let fileContent
if (ext === '.yaml' || ext === '.yml') {
fileContent = "extends: 'next'"
} else {
fileContent = CommentJson.stringify(defaultConfig, null, 2)
if (ext === '.js') {
fileContent = 'module.exports = ' + fileContent
}
}
await fs.writeFile(eslintrcFile, fileContent + os.EOL)
console.log(
'\n' +
chalk.green(
`We detected ESLint in your project and updated the ${chalk.bold(
path.basename(eslintrcFile)
)} file for you.`
) +
'\n'
const content = await fs.readFile(eslintrcFile, { encoding: 'utf8' }).then(
(txt) => txt.trim().replace(/\n/g, ''),
() => null
)
} else if (pkgJsonPath) {
const pkgJsonContent = await fs.readFile(pkgJsonPath, {
encoding: 'utf8',
})
let packageJsonConfig = CommentJson.parse(pkgJsonContent)
if (
content === '' ||
content === '{}' ||
content === '---' ||
content === 'module.exports = {}'
) {
const ext = path.extname(eslintrcFile)
let newFileContent
if (ext === '.yaml' || ext === '.yml') {
newFileContent = "extends: 'next'"
} else {
newFileContent = CommentJson.stringify(defaultConfig, null, 2)
if (ext === '.js') {
newFileContent = 'module.exports = ' + newFileContent
}
}
await fs.writeFile(eslintrcFile, newFileContent + os.EOL)
console.log(
chalk.green(
`We detected an empty ESLint configuration file (${chalk.bold(
path.basename(eslintrcFile)
)}) and updated it for you to include the base Next.js ESLint configuration.`
)
)
}
} else if (
packageJsonConfig?.eslintConfig &&
Object.entries(packageJsonConfig?.eslintConfig).length === 0
) {
packageJsonConfig.eslintConfig = defaultConfig
if (pkgJsonPath)
await fs.writeFile(
pkgJsonPath,
CommentJson.stringify(packageJsonConfig, null, 2) + os.EOL
)
console.log(
chalk.green(
`We detected an empty ${chalk.bold(
'eslintConfig'
)} field in package.json and updated it for you to include the base Next.js ESLint configuration.`
)
)
} else {
await fs.writeFile(
pkgJsonPath,
CommentJson.stringify(packageJsonConfig, null, 2) + os.EOL
'.eslintrc',
CommentJson.stringify(defaultConfig, null, 2) + os.EOL
)
console.log(
'\n' +
chalk.green(
`We detected ESLint in your project and updated the ${chalk.bold(
'eslintConfig'
)} field for you in package.json...`
) +
'\n'
chalk.green(
`We created the ${chalk.bold(
'.eslintrc'
)} file for you and included the base Next.js ESLint configuration.`
)
)
}
}

View file

@ -1,5 +1,5 @@
import chalk from 'chalk'
import path from 'path'
import { basename, join } from 'path'
import { fileExists } from './file-exists'
import { getOxfordCommaList } from './oxford-comma-list'
@ -24,7 +24,9 @@ export async function hasNecessaryDependencies(
baseDir: string,
checkTSDeps: boolean,
checkESLintDeps: boolean,
eslintrcFile: string | null = null
eslintrcFile: string = '',
pkgJsonEslintConfig: boolean = false,
lintDuringBuild: boolean = false
): Promise<NecessaryDependencies> {
if (!checkTSDeps && !checkESLintDeps) {
return { resolved: undefined! }
@ -55,28 +57,39 @@ export async function hasNecessaryDependencies(
const packagesHuman = getOxfordCommaList(missingPackages.map((p) => p.pkg))
const packagesCli = missingPackages.map((p) => p.pkg).join(' ')
const yarnLockFile = path.join(baseDir, 'yarn.lock')
const yarnLockFile = join(baseDir, 'yarn.lock')
const isYarn = await fileExists(yarnLockFile).catch(() => false)
const removalMsg = checkTSDeps
? chalk.bold(
'If you are not trying to use TypeScript, please remove the ' +
chalk.cyan('tsconfig.json') +
' file from your package root (and any TypeScript files in your pages directory).'
)
: chalk.bold(
`If you are not trying to use ESLint, please remove the ${
eslintrcFile
? chalk.cyan(path.basename(eslintrcFile)) +
' file from your application'
: chalk.cyan('eslintConfig') + ' field from your package.json file'
}.`
)
const removalTSMsg =
'\n\n' +
chalk.bold(
'If you are not trying to use TypeScript, please remove the ' +
chalk.cyan('tsconfig.json') +
' file from your package root (and any TypeScript files in your pages directory).'
)
const removalLintMsg =
`\n\n` +
(lintDuringBuild
? `If you do not want to run ESLint during builds, run ${chalk.bold.cyan(
'next build --no-lint'
)}` +
(!!eslintrcFile
? ` or remove the ${chalk.bold(
basename(eslintrcFile)
)} file from your package root.`
: pkgJsonEslintConfig
? ` or remove the ${chalk.bold(
'eslintConfig'
)} field from package.json.`
: '')
: `Once installed, run ${chalk.bold.cyan('next lint')} again.`)
const removalMsg = checkTSDeps ? removalTSMsg : removalLintMsg
throw new FatalError(
chalk.bold.red(
`It looks like you're trying to use ${
checkTSDeps ? 'TypeScript' : 'ESLint'
} but do not have the required package(s) installed.`
checkTSDeps
? `It looks like you're trying to use TypeScript but do not have the required package(s) installed.`
: `To use ESLint, additional required package(s) must be installed.`
) +
'\n\n' +
chalk.bold(`Please install ${chalk.bold(packagesHuman)} by running:`) +
@ -86,7 +99,6 @@ export async function hasNecessaryDependencies(
' ' +
packagesCli
)}` +
'\n\n' +
removalMsg +
'\n'
)

View file

@ -3,7 +3,6 @@ import { Worker } from 'jest-worker'
export async function verifyAndLint(
dir: string,
pagesDir: string,
numWorkers: number | undefined,
enableWorkerThreads: boolean | undefined
): Promise<void> {
@ -18,7 +17,7 @@ export async function verifyAndLint(
lintWorkers.getStdout().pipe(process.stdout)
lintWorkers.getStderr().pipe(process.stderr)
const lintResults = await lintWorkers.runLintCheck(dir, pagesDir)
const lintResults = await lintWorkers.runLintCheck(dir, null, true)
if (lintResults) {
console.log(lintResults)
}

View file

@ -35,7 +35,6 @@ export type NextConfig = { [key: string]: any } & {
excludeDefaultMomentLocales?: boolean
webpack5?: boolean
}
experimental: {
cpus?: number
plugins?: boolean
@ -55,7 +54,6 @@ export type NextConfig = { [key: string]: any } & {
validator?: string
skipValidation?: boolean
}
eslint?: boolean
reactRoot?: boolean
enableBlurryPlaceholder?: boolean
disableOptimizedLoading?: boolean
@ -111,7 +109,6 @@ export const defaultConfig: NextConfig = {
scrollRestoration: false,
stats: false,
externalDir: false,
eslint: false,
reactRoot: Number(process.env.NEXT_PRIVATE_REACT_ROOT) > 0,
enableBlurryPlaceholder: false,
disableOptimizedLoading: true,

View file

@ -39,5 +39,5 @@ do
yarn config set enableGlobalCache true
yarn link --all --private -r ../..
yarn build
yarn build --no-lint
done

View file

@ -60,7 +60,7 @@ ruleTester.run('sync-scripts', rule, {
errors: [
{
message:
'Synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
'External synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
type: 'JSXOpeningElement',
},
],
@ -82,7 +82,7 @@ ruleTester.run('sync-scripts', rule, {
errors: [
{
message:
'Synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
'External synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
type: 'JSXOpeningElement',
},
],

View file

@ -52,6 +52,9 @@ describe('create next app', () => {
expect(
fs.existsSync(path.join(cwd, projectName, 'pages/index.js'))
).toBeTruthy()
expect(
fs.existsSync(path.join(cwd, projectName, '.eslintrc'))
).toBeTruthy()
expect(
fs.existsSync(path.join(cwd, projectName, 'node_modules/next'))
).toBe(true)
@ -121,6 +124,9 @@ describe('create next app', () => {
expect(
fs.existsSync(path.join(cwd, projectName, 'next-env.d.ts'))
).toBeTruthy()
expect(
fs.existsSync(path.join(cwd, projectName, '.eslintrc'))
).toBeTruthy()
expect(
fs.existsSync(path.join(cwd, projectName, 'node_modules/next'))
).toBe(true)
@ -138,6 +144,8 @@ describe('create next app', () => {
])
expect(Object.keys(pkgJSON.devDependencies)).toEqual([
'@types/react',
'eslint',
'eslint-config-next',
'typescript',
])
})
@ -242,7 +250,12 @@ describe('create next app', () => {
)
expect(res.exitCode).toBe(0)
const files = ['package.json', 'pages/index.js', '.gitignore']
const files = [
'package.json',
'pages/index.js',
'.gitignore',
'.eslintrc',
]
files.forEach((file) =>
expect(fs.existsSync(path.join(cwd, projectName, file))).toBeTruthy()
)
@ -309,6 +322,7 @@ describe('create next app', () => {
'pages/index.js',
'.gitignore',
'node_modules/next',
'.eslintrc',
]
files.forEach((file) =>
expect(fs.existsSync(path.join(cwd, file))).toBeTruthy()
@ -327,6 +341,7 @@ describe('create next app', () => {
'pages/index.js',
'.gitignore',
'node_modules/next',
'.eslintrc',
]
files.forEach((file) =>
expect(fs.existsSync(path.join(cwd, projectName, file))).toBeTruthy()
@ -344,6 +359,7 @@ describe('create next app', () => {
'package.json',
'pages/index.js',
'.gitignore',
'.eslintrc',
'package-lock.json',
'node_modules/next',
]

View file

@ -1 +0,0 @@
module.exports = { experimental: { eslint: true } }

View file

@ -1,10 +0,0 @@
{
"name": "eslint-custom-config",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"eslint-config-next": "*",
"eslint": "7.23.0"
}
}

View file

@ -1 +0,0 @@
module.exports = { experimental: { eslint: true } }

View file

@ -1,5 +0,0 @@
{
"devDependencies": {
"eslint-config-next": "*"
}
}

View file

@ -1,5 +1,5 @@
import { join } from 'path'
import { runNextCommand } from 'next-test-utils'
import { nextBuild, nextLint } from 'next-test-utils'
import { writeFile, readFile } from 'fs-extra'
import semver from 'next/dist/compiled/semver'
@ -19,50 +19,87 @@ async function eslintVersion() {
}
describe('ESLint', () => {
it('should populate eslint config automatically for first time setup', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await writeFile(eslintrc, '')
describe('Next Build', () => {
test('first time setup', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await writeFile(eslintrc, '')
const { stdout } = await runNextCommand(['build', dirFirstTimeSetup], {
stdout: true,
})
const eslintrcContent = await readFile(eslintrc, 'utf8')
expect(stdout).toContain(
'We detected ESLint in your project and updated the .eslintrc file for you.'
)
expect(eslintrcContent.trim().replace(/\s/g, '')).toMatch(
'{"extends":"next"}'
)
})
test('shows warnings and errors', async () => {
let output = ''
const { stdout, stderr } = await runNextCommand(
['build', dirCustomConfig],
{
const { stdout, stderr } = await nextBuild(dirFirstTimeSetup, [], {
stdout: true,
stderr: true,
})
const output = stdout + stderr
const eslintrcContent = await readFile(eslintrc, 'utf8')
expect(output).toContain(
'We detected an empty ESLint configuration file (.eslintrc) and updated it for you to include the base Next.js ESLint configuration.'
)
expect(eslintrcContent.trim().replace(/\s/g, '')).toMatch(
'{"extends":"next"}'
)
})
test('shows warnings and errors', async () => {
const { stdout, stderr } = await nextBuild(dirCustomConfig, [], {
stdout: true,
stderr: true,
})
const output = stdout + stderr
const version = await eslintVersion()
if (!version || (version && semver.lt(version, '7.0.0'))) {
expect(output).toContain(
'Your project has an older version of ESLint installed'
)
expect(output).toContain('Please upgrade to v7 or later')
} else {
expect(output).toContain(
'Error: Comments inside children section of tag should be placed inside braces'
)
}
)
})
})
output = stdout + stderr
const version = await eslintVersion()
describe('Next Lint', () => {
test('first time setup', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await writeFile(eslintrc, '')
const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], {
stdout: true,
stderr: true,
})
const output = stdout + stderr
const eslintrcContent = await readFile(eslintrc, 'utf8')
if (!version || (version && semver.lt(version, '7.0.0'))) {
expect(output).toContain(
'Your project has an older version of ESLint installed'
'We detected an empty ESLint configuration file (.eslintrc) and updated it for you to include the base Next.js ESLint configuration.'
)
expect(output).toContain(
'Please upgrade to v7 or later to run ESLint during the build process'
expect(eslintrcContent.trim().replace(/\s/g, '')).toMatch(
'{"extends":"next"}'
)
} else {
expect(output).toContain('Failed to compile')
expect(output).toContain(
'Error: Comments inside children section of tag should be placed inside braces'
)
}
})
test('shows warnings and errors', async () => {
const { stdout, stderr } = await nextLint(dirCustomConfig, [], {
stdout: true,
stderr: true,
})
const output = stdout + stderr
const version = await eslintVersion()
if (!version || (version && semver.lt(version, '7.0.0'))) {
expect(output).toContain(
'Your project has an older version of ESLint installed'
)
expect(output).toContain('Please upgrade to v7 or later')
} else {
expect(output).toContain(
'Error: Comments inside children section of tag should be placed inside braces'
)
}
})
})
})

View file

@ -254,6 +254,10 @@ export function nextExportDefault(dir, opts = {}) {
return runNextCommand(['export', dir], opts)
}
export function nextLint(dir, args = [], opts = {}) {
return runNextCommand(['lint', dir, ...args], opts)
}
export function nextStart(dir, port, opts = {}) {
return runNextCommandDev(['start', '-p', port, dir], undefined, {
...opts,