diff --git a/docs/api-reference/cli.md b/docs/api-reference/cli.md index bd9959252349e..ab91148fb834a 100644 --- a/docs/api-reference/cli.md +++ b/docs/api-reference/cli.md @@ -21,7 +21,7 @@ Usage $ next 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. diff --git a/docs/basic-features/eslint.md b/docs/basic-features/eslint.md new file mode 100644 index 0000000000000..51579519951d6 --- /dev/null +++ b/docs/basic-features/eslint.md @@ -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). diff --git a/docs/getting-started.md b/docs/getting-started.md index 9c7d752b89959..14f8a870151c3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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. diff --git a/docs/manifest.json b/docs/manifest.json index 9d5cd74aa1c40..ceefa4ceca61c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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" diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index b4b5eaa7f60bf..f2d424a88eda7 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -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: diff --git a/packages/create-next-app/templates/default/eslintrc b/packages/create-next-app/templates/default/eslintrc new file mode 100644 index 0000000000000..97a2bb84efb39 --- /dev/null +++ b/packages/create-next-app/templates/default/eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["next", "next/core-web-vitals"] +} diff --git a/packages/create-next-app/templates/default/pages/api/hello.js b/packages/create-next-app/templates/default/pages/api/hello.js index 9987aff4c39a9..df63de88fa67c 100644 --- a/packages/create-next-app/templates/default/pages/api/hello.js +++ b/packages/create-next-app/templates/default/pages/api/hello.js @@ -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' }) } diff --git a/packages/create-next-app/templates/typescript/eslintrc b/packages/create-next-app/templates/typescript/eslintrc new file mode 100644 index 0000000000000..97a2bb84efb39 --- /dev/null +++ b/packages/create-next-app/templates/typescript/eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["next", "next/core-web-vitals"] +} diff --git a/packages/create-next-app/templates/typescript/pages/api/hello.ts b/packages/create-next-app/templates/typescript/pages/api/hello.ts index 3d66af99d6010..f8bcc7e5caed1 100644 --- a/packages/create-next-app/templates/typescript/pages/api/hello.ts +++ b/packages/create-next-app/templates/typescript/pages/api/hello.ts @@ -5,6 +5,9 @@ type Data = { name: string } -export default (req: NextApiRequest, res: NextApiResponse) => { +export default function handler( + req: NextApiRequest, + res: NextApiResponse +) { res.status(200).json({ name: 'John Doe' }) } diff --git a/packages/eslint-config-next/core-web-vitals.js b/packages/eslint-config-next/core-web-vitals.js new file mode 100644 index 0000000000000..d81198b1893b8 --- /dev/null +++ b/packages/eslint-config-next/core-web-vitals.js @@ -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, + }, +} diff --git a/packages/eslint-config-next/index.js b/packages/eslint-config-next/index.js index 88865fd93b028..2d8c7a9902e4e 100644 --- a/packages/eslint-config-next/index.js +++ b/packages/eslint-config-next/index.js @@ -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', { diff --git a/packages/eslint-plugin-next/lib/rules/no-sync-scripts.js b/packages/eslint-plugin-next/lib/rules/no-sync-scripts.js index c4103525dae3c..fb4326c19fb50 100644 --- a/packages/eslint-plugin-next/lib/rules/no-sync-scripts.js +++ b/packages/eslint-plugin-next/lib/rules/no-sync-scripts.js @@ -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.', }) } }, diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts index a81a185983e6c..13279f0dda78b 100755 --- a/packages/next/bin/next.ts +++ b/packages/next/bin/next.ts @@ -20,6 +20,7 @@ const commands: { [command: string]: () => Promise } = { 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), } diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index f73d2e24af505..e5c44640df2e9 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -119,7 +119,8 @@ export default async function build( dir: string, conf = null, reactProductionProfiling = false, - debugOutput = false + debugOutput = false, + runLint = true ): Promise { 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 ) diff --git a/packages/next/cli/next-build.ts b/packages/next/cli/next-build.ts index ad3b78232abcb..c9afca528d5b2 100755 --- a/packages/next/cli/next-build.ts +++ b/packages/next/cli/next-build.ts @@ -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') diff --git a/packages/next/cli/next-lint.ts b/packages/next/cli/next-lint.ts new file mode 100755 index 0000000000000..cc8216c840151 --- /dev/null +++ b/packages/next/cli/next-lint.ts @@ -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 + 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 [options] + + 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 } diff --git a/packages/next/lib/eslint/getLintIntent.ts b/packages/next/lib/eslint/getLintIntent.ts deleted file mode 100644 index af617083c879f..0000000000000 --- a/packages/next/lib/eslint/getLintIntent.ts +++ /dev/null @@ -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 { - 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 -} diff --git a/packages/next/lib/eslint/runLintCheck.ts b/packages/next/lib/eslint/runLintCheck.ts index ed05b9d18533d..5dd2e8cdb1d9f 100644 --- a/packages/next/lib/eslint/runLintCheck.ts +++ b/packages/next/lib/eslint/runLintCheck.ts @@ -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 } } -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 { @@ -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 { 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 + let packageJsonConfig = null + if (pkgJsonPath) { + const pkgJsonContent = await fs.readFile(pkgJsonPath, { + encoding: 'utf8', + }) + packageJsonConfig = CommentJson.parse(pkgJsonContent) + } - const { eslintConfig: pkgJsonEslintConfig = null } = !!pkgJsonPath - ? await import(pkgJsonPath!) - : {} - - // Check if the project uses ESLint - const eslintIntent = await getLintIntent(eslintrcFile, pkgJsonEslintConfig) - - if (!eslintIntent) { + // 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 } - const firstTimeSetup = eslintIntent.firstTimeSetup - // 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 } diff --git a/packages/next/lib/eslint/writeDefaultConfig.ts b/packages/next/lib/eslint/writeDefaultConfig.ts index 2e24b27ab125c..96a244d217670 100644 --- a/packages/next/lib/eslint/writeDefaultConfig.ts +++ b/packages/next/lib/eslint/writeDefaultConfig.ts @@ -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) + 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 - if (ext === '.yaml' || ext === '.yml') { - fileContent = "extends: 'next'" - } else { - fileContent = CommentJson.stringify(defaultConfig, null, 2) + let newFileContent + if (ext === '.yaml' || ext === '.yml') { + newFileContent = "extends: 'next'" + } else { + newFileContent = CommentJson.stringify(defaultConfig, null, 2) - if (ext === '.js') { - fileContent = 'module.exports = ' + fileContent + if (ext === '.js') { + newFileContent = 'module.exports = ' + newFileContent + } } - } - await fs.writeFile(eslintrcFile, fileContent + os.EOL) + await fs.writeFile(eslintrcFile, newFileContent + os.EOL) - console.log( - '\n' + + console.log( 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' - ) - } else if (pkgJsonPath) { - const pkgJsonContent = await fs.readFile(pkgJsonPath, { - encoding: 'utf8', - }) - let packageJsonConfig = CommentJson.parse(pkgJsonContent) - + )}) 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.` + ) ) } } diff --git a/packages/next/lib/has-necessary-dependencies.ts b/packages/next/lib/has-necessary-dependencies.ts index ecbd4748cd483..9d6abff18d371 100644 --- a/packages/next/lib/has-necessary-dependencies.ts +++ b/packages/next/lib/has-necessary-dependencies.ts @@ -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 { 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' ) diff --git a/packages/next/lib/verifyAndLint.ts b/packages/next/lib/verifyAndLint.ts index 523bf9339d70b..3b0a48a56157f 100644 --- a/packages/next/lib/verifyAndLint.ts +++ b/packages/next/lib/verifyAndLint.ts @@ -3,7 +3,6 @@ import { Worker } from 'jest-worker' export async function verifyAndLint( dir: string, - pagesDir: string, numWorkers: number | undefined, enableWorkerThreads: boolean | undefined ): Promise { @@ -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) } diff --git a/packages/next/next-server/server/config-shared.ts b/packages/next/next-server/server/config-shared.ts index 242a247ec05ef..c0d3e3d40b15c 100644 --- a/packages/next/next-server/server/config-shared.ts +++ b/packages/next/next-server/server/config-shared.ts @@ -35,7 +35,6 @@ export type NextConfig = { [key: string]: any } & { excludeDefaultMomentLocales?: boolean webpack5?: boolean } - experimental: { cpus?: number plugins?: boolean @@ -56,7 +55,6 @@ export type NextConfig = { [key: string]: any } & { validator?: string skipValidation?: boolean } - eslint?: boolean reactRoot?: boolean enableBlurryPlaceholder?: boolean disableOptimizedLoading?: boolean @@ -113,7 +111,6 @@ export const defaultConfig: NextConfig = { scriptLoader: false, stats: false, externalDir: false, - eslint: false, reactRoot: Number(process.env.NEXT_PRIVATE_REACT_ROOT) > 0, enableBlurryPlaceholder: false, disableOptimizedLoading: true, diff --git a/test-pnp.sh b/test-pnp.sh index 29874580db9b4..2bf265c728ab6 100755 --- a/test-pnp.sh +++ b/test-pnp.sh @@ -39,5 +39,5 @@ do yarn config set enableGlobalCache true yarn link --all --private -r ../.. - yarn build + yarn build --no-lint done diff --git a/test/eslint-plugin-next/no-sync-scripts.unit.test.js b/test/eslint-plugin-next/no-sync-scripts.unit.test.js index d89c67914d949..934b89b311ef7 100644 --- a/test/eslint-plugin-next/no-sync-scripts.unit.test.js +++ b/test/eslint-plugin-next/no-sync-scripts.unit.test.js @@ -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', }, ], diff --git a/test/integration/create-next-app/index.test.js b/test/integration/create-next-app/index.test.js index 1a7672570d3ca..2ed045c99a834 100644 --- a/test/integration/create-next-app/index.test.js +++ b/test/integration/create-next-app/index.test.js @@ -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', ] diff --git a/test/integration/eslint/custom-config/next.config.js b/test/integration/eslint/custom-config/next.config.js deleted file mode 100644 index fa70d79c18529..0000000000000 --- a/test/integration/eslint/custom-config/next.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { experimental: { eslint: true } } diff --git a/test/integration/eslint/custom-config/package.json b/test/integration/eslint/custom-config/package.json deleted file mode 100644 index ecc0b774b6ba2..0000000000000 --- a/test/integration/eslint/custom-config/package.json +++ /dev/null @@ -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" - } -} diff --git a/test/integration/eslint/first-time-setup/next.config.js b/test/integration/eslint/first-time-setup/next.config.js deleted file mode 100644 index fa70d79c18529..0000000000000 --- a/test/integration/eslint/first-time-setup/next.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { experimental: { eslint: true } } diff --git a/test/integration/eslint/first-time-setup/package.json b/test/integration/eslint/first-time-setup/package.json deleted file mode 100644 index e786fb4fba286..0000000000000 --- a/test/integration/eslint/first-time-setup/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "devDependencies": { - "eslint-config-next": "*" - } -} diff --git a/test/integration/eslint/test/index.test.js b/test/integration/eslint/test/index.test.js index 74059886b691b..7a4a71d9e6102 100644 --- a/test/integration/eslint/test/index.test.js +++ b/test/integration/eslint/test/index.test.js @@ -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 { 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"}' + ) }) - const eslintrcContent = await readFile(eslintrc, 'utf8') + test('shows warnings and errors', async () => { + const { stdout, stderr } = await nextBuild(dirCustomConfig, [], { + stdout: true, + stderr: true, + }) + + const output = stdout + stderr + const version = await eslintVersion() - 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"}' - ) + 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' + ) + } + }) }) - test('shows warnings and errors', async () => { - let output = '' + describe('Next Lint', () => { + test('first time setup', async () => { + const eslintrc = join(dirFirstTimeSetup, '.eslintrc') + await writeFile(eslintrc, '') - const { stdout, stderr } = await runNextCommand( - ['build', dirCustomConfig], - { + const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], { stdout: true, stderr: true, - } - ) + }) + const output = stdout + stderr + const eslintrcContent = await readFile(eslintrc, 'utf8') - 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' + 'We detected an empty ESLint configuration file (.eslintrc) and updated it for you to include the base Next.js ESLint configuration.' ) - } else { - expect(output).toContain('Failed to compile') - expect(output).toContain( - 'Error: Comments inside children section of tag should be placed inside braces' + 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' + ) + } + }) }) }) diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index 193cc3b176717..1354c088a1e37 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -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,