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 content = await fs.readFile(eslintrcFile, { encoding: 'utf8' }).then(
(txt) => txt.trim().replace(/\n/g, ''),
() => null
)
if (
content === '' ||
content === '{}' ||
content === '---' ||
content === 'module.exports = {}'
) {
const ext = path.extname(eslintrcFile)
let fileContent
let newFileContent
if (ext === '.yaml' || ext === '.yml') {
fileContent = "extends: 'next'"
newFileContent = "extends: 'next'"
} else {
fileContent = CommentJson.stringify(defaultConfig, null, 2)
newFileContent = CommentJson.stringify(defaultConfig, null, 2)
if (ext === '.js') {
fileContent = 'module.exports = ' + fileContent
newFileContent = 'module.exports = ' + newFileContent
}
}
await fs.writeFile(eslintrcFile, fileContent + os.EOL)
await fs.writeFile(eslintrcFile, newFileContent + os.EOL)
console.log(
'\n' +
chalk.green(
`We detected ESLint in your project and updated the ${chalk.bold(
`We detected an empty ESLint configuration file (${chalk.bold(
path.basename(eslintrcFile)
)} file for you.`
) +
'\n'
)}) and updated it for you to include the base Next.js ESLint configuration.`
)
} else if (pkgJsonPath) {
const pkgJsonContent = await fs.readFile(pkgJsonPath, {
encoding: 'utf8',
})
let packageJsonConfig = CommentJson.parse(pkgJsonContent)
)
}
} 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(
'\n' +
chalk.green(
`We detected ESLint in your project and updated the ${chalk.bold(
`We detected an empty ${chalk.bold(
'eslintConfig'
)} field for you in package.json...`
) +
'\n'
)} field in package.json and updated it for you to include the base Next.js ESLint configuration.`
)
)
} else {
await fs.writeFile(
'.eslintrc',
CommentJson.stringify(defaultConfig, null, 2) + os.EOL
)
console.log(
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(
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).'
)
: 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 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,18 +19,20 @@ async function eslintVersion() {
}
describe('ESLint', () => {
it('should populate eslint config automatically for first time setup', async () => {
describe('Next Build', () => {
test('first time setup', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await writeFile(eslintrc, '')
const { stdout } = await runNextCommand(['build', dirFirstTimeSetup], {
const { stdout, stderr } = await nextBuild(dirFirstTimeSetup, [], {
stdout: true,
stderr: true,
})
const output = stdout + stderr
const eslintrcContent = await readFile(eslintrc, 'utf8')
expect(stdout).toContain(
'We detected ESLint in your project and updated the .eslintrc file for you.'
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"}'
@ -38,31 +40,66 @@ describe('ESLint', () => {
})
test('shows warnings and errors', async () => {
let output = ''
const { stdout, stderr } = await runNextCommand(
['build', dirCustomConfig],
{
const { stdout, stderr } = await nextBuild(dirCustomConfig, [], {
stdout: true,
stderr: true,
}
)
})
output = stdout + stderr
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 to run ESLint during the build process'
)
expect(output).toContain('Please upgrade to v7 or later')
} else {
expect(output).toContain('Failed to compile')
expect(output).toContain(
'Error: Comments inside children section of tag should be placed inside braces'
)
}
})
})
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')
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 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,