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:
parent
b05719f928
commit
bbc28ccae0
31 changed files with 483 additions and 202 deletions
|
@ -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.
|
||||
|
|
119
docs/basic-features/eslint.md
Normal file
119
docs/basic-features/eslint.md
Normal 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).
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
3
packages/create-next-app/templates/default/eslintrc
Normal file
3
packages/create-next-app/templates/default/eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["next", "next/core-web-vitals"]
|
||||
}
|
|
@ -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' })
|
||||
}
|
||||
|
|
3
packages/create-next-app/templates/typescript/eslintrc
Normal file
3
packages/create-next-app/templates/typescript/eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["next", "next/core-web-vitals"]
|
||||
}
|
|
@ -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' })
|
||||
}
|
||||
|
|
8
packages/eslint-config-next/core-web-vitals.js
Normal file
8
packages/eslint-config-next/core-web-vitals.js
Normal 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,
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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.',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
76
packages/next/cli/next-lint.ts
Executable 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 }
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -39,5 +39,5 @@ do
|
|||
yarn config set enableGlobalCache true
|
||||
yarn link --all --private -r ../..
|
||||
|
||||
yarn build
|
||||
yarn build --no-lint
|
||||
done
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
module.exports = { experimental: { eslint: true } }
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
module.exports = { experimental: { eslint: true } }
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"eslint-config-next": "*"
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue