feat: enable @typescript-eslint/recommended in create-next-app --typescript (#52845)

Co-authored-by: eps1lon <sebastian.silbermann@vercel.com>
This commit is contained in:
Josh Goldberg ✨ 2024-06-27 05:37:01 -04:00 committed by GitHub
parent 689e4b82f2
commit 72a64081b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 289 additions and 57 deletions

View file

@ -202,6 +202,19 @@ The `next/core-web-vitals` rule set is enabled when `next lint` is run for the f
> The `next/core-web-vitals` entry point is automatically included for new applications built with [Create Next App](/docs/app/api-reference/create-next-app).
### TypeScript
In addition to the Next.js ESLint rules, `create-next-app --typescript` will also add TypeScript-specific lint rules with `next/typescript` to your config:
```json filename=".eslintrc.json"
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
```
Those rules are based on [`plugin:@typescript-eslint/recommended`](https://typescript-eslint.io/linting/configs#recommended).
See [typescript-eslint > Configs](https://typescript-eslint.io/linting/configs) for more details.
## Usage With Other Tools
### Prettier

View file

@ -24,8 +24,8 @@
"@types/node-fetch": "^3.0.3",
"@types/react": "^17.0.2",
"@types/react-dom": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"cross-env": "^7.0.3",
"nodemon": "^2.0.12",
"ts-node": "^10.2.1",

View file

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

View file

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

View file

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

View file

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

View file

@ -30,12 +30,13 @@ sortedPaths.push(...keptPaths)
const hookPropertyMap = new Map(
[
['eslint-plugin-import', 'eslint-plugin-import'],
['eslint-plugin-react', 'eslint-plugin-react'],
['eslint-plugin-jsx-a11y', 'eslint-plugin-jsx-a11y'],
].map(([request, replacement]) => [
'@typescript-eslint/eslint-plugin',
'eslint-plugin-import',
'eslint-plugin-react',
'eslint-plugin-jsx-a11y',
].map((request) => [
request,
require.resolve(replacement, { paths: sortedPaths }),
require.resolve(request, { paths: sortedPaths }),
])
)
@ -96,10 +97,6 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
warnOnUnsupportedTypeScriptVersion: true,
},
},
],

View file

@ -12,6 +12,7 @@
"dependencies": {
"@next/eslint-plugin-next": "15.0.0-canary.46",
"@rushstack/eslint-patch": "^1.3.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",

View file

@ -0,0 +1,3 @@
module.exports = {
extends: ['plugin:@typescript-eslint/recommended'],
}

View file

@ -79,26 +79,6 @@ export const SSG_FALLBACK_EXPORT_ERROR = `Pages with \`fallback\` enabled in \`g
export const ESLINT_DEFAULT_DIRS = ['app', 'pages', 'components', 'lib', 'src']
export const ESLINT_PROMPT_VALUES = [
{
title: 'Strict',
recommended: true,
config: {
extends: 'next/core-web-vitals',
},
},
{
title: 'Base',
config: {
extends: 'next',
},
},
{
title: 'Cancel',
config: null,
},
]
export const SERVER_RUNTIME: Record<string, ServerRuntime> = {
edge: 'edge',
experimentalEdge: 'experimental-edge',

View file

@ -0,0 +1,32 @@
import findUp from 'next/dist/compiled/find-up'
export const getESLintStrictValue = async (cwd: string) => {
const tsConfigLocation = await findUp('tsconfig.json', { cwd })
const hasTSConfig = tsConfigLocation !== undefined
return {
title: 'Strict',
recommended: true,
config: {
extends: hasTSConfig
? ['next/core-web-vitals', 'next/typescript']
: 'next/core-web-vitals',
},
}
}
export const getESLintPromptValues = async (cwd: string) => {
return [
await getESLintStrictValue(cwd),
{
title: 'Base',
config: {
extends: 'next',
},
},
{
title: 'Cancel',
config: null,
},
]
}

View file

@ -12,7 +12,6 @@ import { writeDefaultConfig } from './writeDefaultConfig'
import { hasEslintConfiguration } from './hasEslintConfiguration'
import { writeOutputFile } from './writeOutputFile'
import { ESLINT_PROMPT_VALUES } from '../constants'
import { findPagesDir } from '../find-pages-dir'
import { installDependencies } from '../install-dependencies'
import { hasNecessaryDependencies } from '../has-necessary-dependencies'
@ -21,6 +20,10 @@ import * as Log from '../../build/output/log'
import type { EventLintCheckCompleted } from '../../telemetry/events/build'
import isError, { getProperError } from '../is-error'
import { getPkgManager } from '../helpers/get-pkg-manager'
import {
getESLintStrictValue,
getESLintPromptValues,
} from './getESLintPromptValues'
type Config = {
plugins: string[]
@ -44,7 +47,7 @@ const requiredPackages = [
},
]
async function cliPrompt(): Promise<{ config?: any }> {
async function cliPrompt(cwd: string): Promise<{ config?: any }> {
console.log(
bold(
`${cyan(
@ -58,7 +61,7 @@ async function cliPrompt(): Promise<{ config?: any }> {
await Promise.resolve(require('next/dist/compiled/cli-select'))
).default
const { value } = await cliSelect({
values: ESLINT_PROMPT_VALUES,
values: await getESLintPromptValues(cwd),
valueRenderer: (
{
title,
@ -356,10 +359,8 @@ export async function runLintCheck(
} else {
// Ask user what config they would like to start with for first time "next lint" setup
const { config: selectedConfig } = strict
? ESLINT_PROMPT_VALUES.find(
(opt: { title: string }) => opt.title === 'Strict'
)!
: await cliPrompt()
? await getESLintStrictValue(baseDir)
: await cliPrompt(baseDir)
if (selectedConfig == null) {
// Show a warning if no option is selected in prompt

View file

@ -779,6 +779,9 @@ importers:
'@rushstack/eslint-patch':
specifier: ^1.3.3
version: 1.3.3
'@typescript-eslint/eslint-plugin':
specifier: ^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0
version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.31.0)(typescript@4.8.2)
'@typescript-eslint/parser':
specifier: ^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0
version: 6.14.0(eslint@8.31.0)(typescript@4.8.2)
@ -3760,6 +3763,16 @@ packages:
jsdoc-type-pratt-parser: 4.0.0
dev: true
/@eslint-community/eslint-utils@4.4.0(eslint@8.31.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
dependencies:
eslint: 8.31.0
eslint-visitor-keys: 3.4.3
dev: false
/@eslint-community/eslint-utils@4.4.0(eslint@8.56.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -3778,7 +3791,6 @@ packages:
/@eslint-community/regexpp@4.5.1:
resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
dev: true
/@eslint/eslintrc@0.4.3:
resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==}
@ -7581,13 +7593,8 @@ packages:
'@types/node': 20.12.3
dev: true
/@types/semver@7.5.0:
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
dev: true
/@types/semver@7.5.6:
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
dev: true
/@types/send@0.14.4:
resolution: {integrity: sha512-SCVCRRjSbpwoKgA34wK8cq14OUPu4qrKigO85/ZH6J04NGws37khLtq7YQr17zyOH01p4T5oy8e1TxEzql01Pg==}
@ -7709,6 +7716,35 @@ packages:
'@types/node': 20.12.3
optional: true
/@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.31.0)(typescript@4.8.2):
resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
'@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@eslint-community/regexpp': 4.5.1
'@typescript-eslint/parser': 6.14.0(eslint@8.31.0)(typescript@4.8.2)
'@typescript-eslint/scope-manager': 6.14.0
'@typescript-eslint/type-utils': 6.14.0(eslint@8.31.0)(typescript@4.8.2)
'@typescript-eslint/utils': 6.14.0(eslint@8.31.0)(typescript@4.8.2)
'@typescript-eslint/visitor-keys': 6.14.0
debug: 4.3.4
eslint: 8.31.0
graphemer: 1.4.0
ignore: 5.2.4
natural-compare: 1.4.0
semver: 7.5.4
ts-api-utils: 1.0.1(typescript@4.8.2)
typescript: 4.8.2
transitivePeerDependencies:
- supports-color
dev: false
/@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.4.5):
resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==}
engines: {node: ^16.0.0 || >=18.0.0}
@ -7795,6 +7831,26 @@ packages:
'@typescript-eslint/types': 6.14.0
'@typescript-eslint/visitor-keys': 6.14.0
/@typescript-eslint/type-utils@6.14.0(eslint@8.31.0)(typescript@4.8.2):
resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 6.14.0(typescript@4.8.2)
'@typescript-eslint/utils': 6.14.0(eslint@8.31.0)(typescript@4.8.2)
debug: 4.3.4
eslint: 8.31.0
ts-api-utils: 1.0.1(typescript@4.8.2)
typescript: 4.8.2
transitivePeerDependencies:
- supports-color
dev: false
/@typescript-eslint/type-utils@6.14.0(eslint@8.56.0)(typescript@5.4.5):
resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==}
engines: {node: ^16.0.0 || >=18.0.0}
@ -7907,6 +7963,25 @@ packages:
- typescript
dev: true
/@typescript-eslint/utils@6.14.0(eslint@8.31.0)(typescript@4.8.2):
resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.31.0)
'@types/json-schema': 7.0.12
'@types/semver': 7.5.6
'@typescript-eslint/scope-manager': 6.14.0
'@typescript-eslint/types': 6.14.0
'@typescript-eslint/typescript-estree': 6.14.0(typescript@4.8.2)
eslint: 8.31.0
semver: 7.6.2
transitivePeerDependencies:
- supports-color
- typescript
dev: false
/@typescript-eslint/utils@6.14.0(eslint@8.56.0)(typescript@5.4.5):
resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==}
engines: {node: ^16.0.0 || >=18.0.0}
@ -7915,7 +7990,7 @@ packages:
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
'@types/json-schema': 7.0.12
'@types/semver': 7.5.0
'@types/semver': 7.5.6
'@typescript-eslint/scope-manager': 6.14.0
'@typescript-eslint/types': 6.14.0
'@typescript-eslint/typescript-estree': 6.14.0(typescript@5.4.5)
@ -14342,7 +14417,6 @@ packages:
/graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
dev: true
/graphql@16.8.1:
resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
@ -14891,7 +14965,7 @@ packages:
engines: {node: '>= 6'}
dependencies:
agent-base: 6.0.2
debug: 4.3.4
debug: 4.1.1
transitivePeerDependencies:
- supports-color
@ -17674,7 +17748,6 @@ packages:
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
dev: true
/lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
@ -23350,7 +23423,6 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
/semver@7.6.2:
resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==}
@ -26619,7 +26691,6 @@ packages:
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true
/yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}

View file

@ -120,7 +120,9 @@ describe('Next Lint', () => {
'We created the .eslintrc.json file for you and included your selected configuration'
)
expect(eslintrcJson).toMatchObject({ extends: 'next/core-web-vitals' })
})
test('creates .eslintrc.json file with a default app router configuration', async () => {
// App Router
const { stdout: appStdout, eslintrcJson: appEslintrcJson } =
await nextLintTemp(null, true)

View file

@ -223,7 +223,6 @@ exports[`Next Build production mode first time setup with TypeScript 1`] = `
},
"requireConfigFile": false,
"sourceType": "module",
"warnOnUnsupportedTypeScriptVersion": true,
},
"plugins": [
"react-hooks",
@ -231,6 +230,7 @@ exports[`Next Build production mode first time setup with TypeScript 1`] = `
"react",
"import",
"@next/next",
"@typescript-eslint",
],
"rules": {
"@next/next/google-font-display": [
@ -296,6 +296,63 @@ exports[`Next Build production mode first time setup with TypeScript 1`] = `
"@next/next/no-unwanted-polyfillio": [
"warn",
],
"@typescript-eslint/ban-ts-comment": [
"error",
],
"@typescript-eslint/ban-types": [
"error",
],
"@typescript-eslint/no-array-constructor": [
"error",
],
"@typescript-eslint/no-duplicate-enum-values": [
"error",
],
"@typescript-eslint/no-explicit-any": [
"error",
],
"@typescript-eslint/no-extra-non-null-assertion": [
"error",
],
"@typescript-eslint/no-loss-of-precision": [
"error",
],
"@typescript-eslint/no-misused-new": [
"error",
],
"@typescript-eslint/no-namespace": [
"error",
],
"@typescript-eslint/no-non-null-asserted-optional-chain": [
"error",
],
"@typescript-eslint/no-this-alias": [
"error",
],
"@typescript-eslint/no-unnecessary-type-constraint": [
"error",
],
"@typescript-eslint/no-unsafe-declaration-merging": [
"error",
],
"@typescript-eslint/no-unused-vars": [
"error",
],
"@typescript-eslint/no-var-requires": [
"error",
],
"@typescript-eslint/prefer-as-const": [
"error",
],
"@typescript-eslint/triple-slash-reference": [
"error",
],
"constructor-super": [
"off",
],
"getter-return": [
"off",
],
"import/no-anonymous-default-export": [
"warn",
],
@ -325,6 +382,69 @@ exports[`Next Build production mode first time setup with TypeScript 1`] = `
"jsx-a11y/role-supports-aria-props": [
"warn",
],
"no-array-constructor": [
"off",
],
"no-const-assign": [
"off",
],
"no-dupe-args": [
"off",
],
"no-dupe-class-members": [
"off",
],
"no-dupe-keys": [
"off",
],
"no-func-assign": [
"off",
],
"no-import-assign": [
"off",
],
"no-loss-of-precision": [
"off",
],
"no-new-symbol": [
"off",
],
"no-obj-calls": [
"off",
],
"no-redeclare": [
"off",
],
"no-setter-return": [
"off",
],
"no-this-before-super": [
"off",
],
"no-undef": [
"off",
],
"no-unreachable": [
"off",
],
"no-unsafe-negation": [
"off",
],
"no-unused-vars": [
"off",
],
"no-var": [
"error",
],
"prefer-const": [
"error",
],
"prefer-rest-params": [
"error",
],
"prefer-spread": [
"error",
],
"react-hooks/exhaustive-deps": [
"warn",
],

View file

@ -15,6 +15,10 @@ describe('Next Build', () => {
test('first time setup', async () => {
const next = await createNext({
files: new FileRef(dirFirstTimeSetup),
dependencies: {
// create-next-install will replace this with a version built from the local source
'eslint-config-next': 'canary',
},
skipStart: true,
})
@ -33,14 +37,16 @@ describe('Next Build', () => {
execSync(`pnpm next lint --strict`, {
cwd: next.testDir,
encoding: 'utf8',
stdio: 'inherit',
})
}).toThrow('Command failed: pnpm next lint --strict')
const eslintConfigAfterSetupJSON = await execSync(
const eslintConfigAfterSetupJSON = execSync(
`pnpm eslint --print-config pages/index.js`,
{
cwd: next.testDir,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'inherit'],
}
)
const { parser, settings, ...eslintConfigAfterSetup } = JSON.parse(
@ -89,6 +95,10 @@ describe('Next Build', () => {
test('first time setup with TypeScript', async () => {
const next = await createNext({
files: new FileRef(dirFirstTimeSetupTS),
dependencies: {
// create-next-install will replace this with a version built from the local source
'eslint-config-next': 'canary',
},
skipStart: true,
})
@ -107,14 +117,16 @@ describe('Next Build', () => {
execSync(`pnpm next lint --strict`, {
cwd: next.testDir,
encoding: 'utf8',
stdio: 'inherit',
})
}).toThrow('Command failed: pnpm next lint --strict')
const eslintConfigAfterSetupJSON = await execSync(
const eslintConfigAfterSetupJSON = execSync(
`pnpm eslint --print-config pages/index.tsx`,
{
cwd: next.testDir,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'inherit'],
}
)
const { parser, settings, ...eslintConfigAfterSetup } = JSON.parse(