Skip to content

Commit 300bfe5

Browse files
[BREAKING] Support ESLint v9 in plugin, config and next lint (#71218)
> [!WARNING] > **Breaking Change:** Now uses `[email protected]` which has a new violation disallowing Component names starting with anything but an uppercase letter. See https://github.com/facebook/react/releases/tag/eslint-plugin-react-hooks%405.0.0 for more details. Adds support of ESLint v9 to `eslint-plugin-next`, `eslint-config-next` and `next lint`. Does not require using the new flat config format. `next lint` will automatically ensure the old config format can be used. ### Why? As `eslint-plugin-react-hooks` has been updated for ESLint v9 support and is a helpful package for Next v15 upgrade, unblock the restrictions to upgrade to ESLint v9. Also, ESLint v8 is [End of Life](https://eslint.org/blog/2024/09/eslint-v8-eol-version-support/#:~:text=ESLint%20v8.-,x%20end%20of%20life%20is%20October%205%2C%202024,x%20on%20October%205%2C%202024.) support since Oct 5th, so is good to unblock v9 now. Plugins bumped: - [x] [@rushstack/eslint-patch](microsoft/rushstack#4719) ([v1.10.3](https://www.npmjs.com/package/@rushstack/eslint-patch/v/1.10.3?activeTab=versions) no release post, confirmed on NPM) - [x] [@typescript-eslint/eslint-plugin](typescript-eslint/typescript-eslint#9002) ([v8.0.0](typescript-eslint/typescript-eslint#9002 (comment))) - [x] [eslint-plugin-import](import-js/eslint-plugin-import#2996) ([v2.31.0](https://github.com/import-js/eslint-plugin-import/releases/tag/v2.31.0)) - [x] [eslint-plugin-jsx-a11y](jsx-eslint/eslint-plugin-jsx-a11y#1009) ([v6.10.0](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases/tag/v6.10.0)) - [x] [eslint-plugin-react](jsx-eslint/eslint-plugin-react#3759) ([v7.35.0](https://github.com/jsx-eslint/eslint-plugin-react/releases/tag/v7.35.0)) - [x] [eslint-plugin-react-hooks](facebook/react#28773) ([v5.0.0](https://github.com/facebook/react/releases/tag/eslint-plugin-react-hooks%405.0.0)) We have to switch to ESLint v9 in our repo due to a pnpm bug where it automatically uses ESLint v9 even though we only installed it via `eslint-v9: npm:[email protected]`. This is a pnpm bug that wouldn't happen with Yarn v1, v4 nor NPM. Closes #64409 Closes #64114 Closes #64453 Closes NEXT-3293 --------- Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>
1 parent 863168d commit 300bfe5

File tree

67 files changed

+3666
-2332
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+3666
-2332
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ packages/next-swc/docs/assets/**/*
4545
test/lib/amp-validator-wasm.js
4646
test/production/pages-dir/production/fixture/amp-validator-wasm.js
4747
test/e2e/async-modules/amp-validator-wasm.js
48+
test/development/next-lint-eslint-formatter-compact/**/*.js
4849

4950
# turbopack crates
5051
turbopack/crates/*/tests/**

.eslintrc.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@
6666
"@typescript-eslint/array-type": "off",
6767
"@typescript-eslint/ban-ts-comment": "off",
6868
"@typescript-eslint/ban-tslint-comment": "off",
69-
"@typescript-eslint/ban-types": "off",
69+
"@typescript-eslint/no-empty-object-type": "off",
70+
"@typescript-eslint/no-restricted-types": "off",
71+
"@typescript-eslint/no-unsafe-function-type": "off",
72+
"@typescript-eslint/no-wrapper-object-types": "off",
7073
"@typescript-eslint/class-literal-property-style": "off",
7174
"@typescript-eslint/consistent-generic-constructors": "off",
7275
"@typescript-eslint/consistent-indexed-object-style": "off",
@@ -77,6 +80,7 @@
7780
"@typescript-eslint/no-empty-interface": "off",
7881
"@typescript-eslint/no-explicit-any": "off",
7982
"@typescript-eslint/no-inferrable-types": "off",
83+
"@typescript-eslint/no-require-imports": "off",
8084
"@typescript-eslint/no-var-requires": "off",
8185
"@typescript-eslint/prefer-for-of": "off",
8286
"@typescript-eslint/prefer-function-type": "off",
@@ -104,6 +108,7 @@
104108
"args": "none",
105109
"ignoreRestSiblings": true,
106110
"argsIgnorePattern": "^_",
111+
"caughtErrors": "none",
107112
"caughtErrorsIgnorePattern": "^_",
108113
"destructuredArrayIgnorePattern": "^_",
109114
"varsIgnorePattern": "^_"
@@ -178,6 +183,7 @@
178183
{
179184
"args": "all",
180185
"argsIgnorePattern": "^_",
186+
"caughtErrors": "none",
181187
"ignoreRestSiblings": true
182188
}
183189
]
@@ -308,6 +314,7 @@
308314
"error",
309315
{
310316
"args": "none",
317+
"caughtErrors": "none",
311318
"ignoreRestSiblings": true
312319
}
313320
],

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ dist
55
target
66
packages/next/wasm/@next
77
tarballs/
8-
packages/next/*.tgz
8+
packages/**/*.tgz
99

1010
# dependencies
1111
node_modules

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"typescript",
1717
"typescriptreact"
1818
],
19+
"eslint.useFlatConfig": false,
1920
// Set Jest runMode to on-demand as otherwise it will start running all tests the first time.
2021
// Equivalent to deprecated option "jest.autoRun": "off"
2122
"jest.runMode": "on-demand",

examples/with-supertokens/app/config/frontend.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import ThirdPartyReact from "supertokens-auth-react/recipe/thirdparty";
22
import EmailPasswordReact from "supertokens-auth-react/recipe/emailpassword";
33
import Session from "supertokens-auth-react/recipe/session";
44
import { appInfo } from "./appInfo";
5-
import { useRouter } from "next/navigation";
5+
import { type useRouter } from "next/navigation";
66
import { SuperTokensConfig } from "supertokens-auth-react/lib/build/types";
77
import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui";
88
import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";

lint-staged.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
'*.{js,jsx,mjs,ts,tsx,mts}': [
33
'prettier --with-node-modules --ignore-path .prettierignore --write',
4-
'eslint --fix',
4+
'cross-env ESLINT_USE_FLAT_CONFIG=false eslint --config .eslintrc.json --fix',
55
],
66
'*.{json,md,mdx,css,html,yml,yaml,scss}': [
77
'prettier --with-node-modules --ignore-path .prettierignore --write',

package.json

+8-7
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"git-clean": "git clean -d -x -e node_modules -e packages -f",
3535
"typescript": "tsc --noEmit",
3636
"lint-typescript": "turbo run typescript",
37-
"lint-eslint": "eslint . --ext js,jsx,ts,tsx --config .eslintrc.cli.json --no-eslintrc",
37+
"lint-eslint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --ext js,jsx,ts,tsx --config .eslintrc.cli.json --no-eslintrc",
3838
"lint-ast-grep": "ast-grep scan",
3939
"lint-no-typescript": "run-p prettier-check lint-eslint lint-language",
4040
"types-and-precompiled": "run-p lint-typescript check-precompiled validate-externals-doc",
@@ -121,8 +121,8 @@
121121
"@types/relay-runtime": "14.1.13",
122122
"@types/string-hash": "1.1.1",
123123
"@types/trusted-types": "2.0.3",
124-
"@typescript-eslint/eslint-plugin": "7.16.0",
125-
"@typescript-eslint/parser": "7.16.0",
124+
"@typescript-eslint/eslint-plugin": "8.0.0",
125+
"@typescript-eslint/parser": "8.0.0",
126126
"@vercel/devlow-bench": "workspace:*",
127127
"@vercel/fetch": "6.1.1",
128128
"@vercel/og": "0.6.3",
@@ -144,15 +144,16 @@
144144
"dd-trace": "4.12.0",
145145
"es5-ext": "0.10.53",
146146
"escape-string-regexp": "2.0.0",
147-
"eslint": "8.56.0",
147+
"eslint": "9.12.0",
148148
"eslint-config-next": "workspace:*",
149149
"eslint-formatter-codeframe": "7.32.1",
150150
"eslint-plugin-eslint-plugin": "5.2.1",
151-
"eslint-plugin-import": "2.29.1",
151+
"eslint-plugin-import": "2.31.0",
152152
"eslint-plugin-jest": "27.6.3",
153153
"eslint-plugin-jsdoc": "48.0.4",
154-
"eslint-plugin-react": "7.33.2",
155-
"eslint-plugin-react-hooks": "4.6.0",
154+
"eslint-plugin-react": "7.35.0",
155+
"eslint-plugin-react-hooks": "5.0.0",
156+
"eslint-v8": "npm:eslint@^8.57.0",
156157
"event-stream": "4.0.1",
157158
"execa": "2.0.3",
158159
"expect-type": "0.14.2",

packages/eslint-config-next/package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@
1111
"homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config",
1212
"dependencies": {
1313
"@next/eslint-plugin-next": "15.0.0-canary.190",
14-
"@rushstack/eslint-patch": "^1.3.3",
14+
"@rushstack/eslint-patch": "^1.10.3",
1515
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
1616
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
1717
"eslint-import-resolver-node": "^0.3.6",
1818
"eslint-import-resolver-typescript": "^3.5.2",
19-
"eslint-plugin-import": "^2.28.1",
20-
"eslint-plugin-jsx-a11y": "^6.7.1",
21-
"eslint-plugin-react": "^7.33.2",
22-
"eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
19+
"eslint-plugin-import": "^2.31.0",
20+
"eslint-plugin-jsx-a11y": "^6.10.0",
21+
"eslint-plugin-react": "^7.35.0",
22+
"eslint-plugin-react-hooks": "^5.0.0"
2323
},
2424
"peerDependencies": {
25-
"eslint": "^7.23.0 || ^8.0.0",
25+
"eslint": "^7.23.0 || ^8.0.0 || ^9.0.0",
2626
"typescript": ">=3.3.1"
2727
},
2828
"peerDependenciesMeta": {

packages/next/src/cli/next-lint.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,23 @@ export type NextLintOptions = {
2323
config?: string
2424
dir?: string[]
2525
errorOnUnmatchedPattern?: boolean
26-
ext: string[]
2726
file?: string[]
2827
fix?: boolean
2928
fixType?: string
3029
format?: string
3130
ignore: boolean
32-
ignorePath?: string
33-
inlineConfig: boolean
34-
maxWarnings: number
3531
outputFile?: string
3632
quiet?: boolean
33+
strict?: boolean
34+
// TODO(jiwon): ESLint v9 unsupported options
35+
// we currently delete them at `runLintCheck` when used in v9
36+
ext: string[]
37+
ignorePath?: string
3738
reportUnusedDisableDirectivesSeverity: 'error' | 'off' | 'warn'
3839
resolvePluginsRelativeTo?: string
3940
rulesdir?: string
40-
strict?: boolean
41+
inlineConfig: boolean
42+
maxWarnings: number
4143
}
4244

4345
const eslintOptions = (

packages/next/src/compiled/assert/assert.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/next/src/compiled/babel-packages/packages-bundle.js

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/next/src/compiled/util/util.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/next/src/experimental/testmode/playwright/next-fixture.ts

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export async function applyNextFixture(
7777
const fixture = new NextFixtureImpl(testInfo, nextOptions, nextWorker, page)
7878

7979
await fixture.setup()
80+
// eslint-disable-next-line react-hooks/rules-of-hooks -- not React.use()
8081
await use(fixture)
8182

8283
fixture.teardown()

packages/next/src/experimental/testmode/playwright/next-worker-fixture.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export async function applyNextWorkerFixture(
5454
): Promise<void> {
5555
const fixture = new NextWorkerFixtureImpl()
5656
await fixture.setup()
57+
// eslint-disable-next-line react-hooks/rules-of-hooks -- not React.use()
5758
await use(fixture)
5859
fixture.teardown()
5960
}

packages/next/src/lib/eslint/runLintCheck.ts

+59-3
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,25 @@ async function lint(
135135

136136
const mod = await Promise.resolve(require(deps.resolved.get('eslint')!))
137137

138-
const { ESLint } = mod
138+
const useFlatConfig =
139+
// If V9 config was found, use flat config, or else use legacy.
140+
eslintrcFile?.startsWith('eslint.config.')
141+
142+
let ESLint
143+
// loadESLint is >= 8.57.0
144+
// PR https://github.com/eslint/eslint/pull/18098
145+
// Release https://github.com/eslint/eslint/releases/tag/v8.57.0
146+
if ('loadESLint' in mod) {
147+
// By default, configType is `flat`. If `useFlatConfig` is false, the return value is `LegacyESLint`.
148+
// https://github.com/eslint/eslint/blob/1def4cdfab1f067c5089df8b36242cdf912b0eb6/lib/types/index.d.ts#L1609-L1613
149+
ESLint = await mod.loadESLint({
150+
useFlatConfig,
151+
})
152+
} else {
153+
// eslint < 8.57.0, use legacy ESLint
154+
ESLint = mod.ESLint
155+
}
156+
139157
let eslintVersion = ESLint?.version ?? mod.CLIEngine?.version
140158

141159
if (!eslintVersion || semver.lt(eslintVersion, '7.0.0')) {
@@ -155,6 +173,23 @@ async function lint(
155173
...eslintOptions,
156174
}
157175

176+
if (semver.gte(eslintVersion, '9.0.0') && useFlatConfig) {
177+
for (const option of [
178+
'useEslintrc',
179+
'extensions',
180+
'ignorePath',
181+
'reportUnusedDisableDirectives',
182+
'resolvePluginsRelativeTo',
183+
'rulePaths',
184+
'inlineConfig',
185+
'maxWarnings',
186+
]) {
187+
if (option in options) {
188+
delete options[option]
189+
}
190+
}
191+
}
192+
158193
let eslint = new ESLint(options)
159194

160195
let nextEslintPluginIsEnabled = false
@@ -163,10 +198,20 @@ async function lint(
163198
for (const configFile of [eslintrcFile, pkgJsonPath]) {
164199
if (!configFile) continue
165200

166-
const completeConfig: Config =
201+
const completeConfig: Config | undefined =
167202
await eslint.calculateConfigForFile(configFile)
203+
if (!completeConfig) continue
204+
205+
const plugins = completeConfig.plugins
206+
207+
const hasNextPlugin =
208+
// in ESLint < 9, `plugins` value is string[]
209+
Array.isArray(plugins)
210+
? plugins.includes('@next/next')
211+
: // in ESLint >= 9, `plugins` value is Record<string, unknown>
212+
'@next/next' in plugins
168213

169-
if (completeConfig.plugins?.includes('@next/next')) {
214+
if (hasNextPlugin) {
170215
nextEslintPluginIsEnabled = true
171216
for (const [name, [severity]] of Object.entries(completeConfig.rules)) {
172217
if (!name.startsWith('@next/next/')) {
@@ -309,6 +354,17 @@ export async function runLintCheck(
309354
const eslintrcFile =
310355
(await findUp(
311356
[
357+
// eslint v9
358+
'eslint.config.js',
359+
'eslint.config.mjs',
360+
'eslint.config.cjs',
361+
// TODO(jiwon): Support when it's stable.
362+
// TS extensions are experimental and requires to install another package `jiti`.
363+
// https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files
364+
// 'eslint.config.ts',
365+
// 'eslint.config.mts',
366+
// 'eslint.config.cts',
367+
// eslint <= v8
312368
'.eslintrc.js',
313369
'.eslintrc.cjs',
314370
'.eslintrc.yaml',

packages/next/src/lib/metadata/resolve-metadata.ts

+1
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ function inheritFromMetadata(
603603
}
604604
}
605605

606+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
606607
const commonOgKeys = ['title', 'description', 'images'] as const
607608
function postProcessMetadata(
608609
metadata: ResolvedMetadata,

packages/next/src/trace/report/to-json.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import path from 'path'
55
import { PHASE_DEVELOPMENT_SERVER } from '../../shared/lib/constants'
66
import type { TraceEvent } from '../types'
77

8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
89
const localEndpoint = {
910
serviceName: 'nextjs',
1011
ipv4: '127.0.0.1',

0 commit comments

Comments
 (0)