Skip to content

Commit

Permalink
[BREAKING] Support ESLint v9 in plugin, config and next lint (#71218)
Browse files Browse the repository at this point in the history
> [!WARNING]
> **Breaking Change:** Now uses `eslint-plugin-react-hooks@5.0.0` 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:eslint@9.0.0`. 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 <sebastian.silbermann@vercel.com>
  • Loading branch information
devjiwonchoi and eps1lon authored Oct 14, 2024
1 parent 863168d commit 300bfe5
Show file tree
Hide file tree
Showing 67 changed files with 3,666 additions and 2,332 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ packages/next-swc/docs/assets/**/*
test/lib/amp-validator-wasm.js
test/production/pages-dir/production/fixture/amp-validator-wasm.js
test/e2e/async-modules/amp-validator-wasm.js
test/development/next-lint-eslint-formatter-compact/**/*.js

# turbopack crates
turbopack/crates/*/tests/**
Expand Down
9 changes: 8 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-tslint-comment": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-restricted-types": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-wrapper-object-types": "off",
"@typescript-eslint/class-literal-property-style": "off",
"@typescript-eslint/consistent-generic-constructors": "off",
"@typescript-eslint/consistent-indexed-object-style": "off",
Expand All @@ -77,6 +80,7 @@
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/prefer-for-of": "off",
"@typescript-eslint/prefer-function-type": "off",
Expand Down Expand Up @@ -104,6 +108,7 @@
"args": "none",
"ignoreRestSiblings": true,
"argsIgnorePattern": "^_",
"caughtErrors": "none",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_"
Expand Down Expand Up @@ -178,6 +183,7 @@
{
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "none",
"ignoreRestSiblings": true
}
]
Expand Down Expand Up @@ -308,6 +314,7 @@
"error",
{
"args": "none",
"caughtErrors": "none",
"ignoreRestSiblings": true
}
],
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ dist
target
packages/next/wasm/@next
tarballs/
packages/next/*.tgz
packages/**/*.tgz

# dependencies
node_modules
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"typescript",
"typescriptreact"
],
"eslint.useFlatConfig": false,
// Set Jest runMode to on-demand as otherwise it will start running all tests the first time.
// Equivalent to deprecated option "jest.autoRun": "off"
"jest.runMode": "on-demand",
Expand Down
2 changes: 1 addition & 1 deletion examples/with-supertokens/app/config/frontend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ThirdPartyReact from "supertokens-auth-react/recipe/thirdparty";
import EmailPasswordReact from "supertokens-auth-react/recipe/emailpassword";
import Session from "supertokens-auth-react/recipe/session";
import { appInfo } from "./appInfo";
import { useRouter } from "next/navigation";
import { type useRouter } from "next/navigation";
import { SuperTokensConfig } from "supertokens-auth-react/lib/build/types";
import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui";
import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";
Expand Down
2 changes: 1 addition & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
'*.{js,jsx,mjs,ts,tsx,mts}': [
'prettier --with-node-modules --ignore-path .prettierignore --write',
'eslint --fix',
'cross-env ESLINT_USE_FLAT_CONFIG=false eslint --config .eslintrc.json --fix',
],
'*.{json,md,mdx,css,html,yml,yaml,scss}': [
'prettier --with-node-modules --ignore-path .prettierignore --write',
Expand Down
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"git-clean": "git clean -d -x -e node_modules -e packages -f",
"typescript": "tsc --noEmit",
"lint-typescript": "turbo run typescript",
"lint-eslint": "eslint . --ext js,jsx,ts,tsx --config .eslintrc.cli.json --no-eslintrc",
"lint-eslint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --ext js,jsx,ts,tsx --config .eslintrc.cli.json --no-eslintrc",
"lint-ast-grep": "ast-grep scan",
"lint-no-typescript": "run-p prettier-check lint-eslint lint-language",
"types-and-precompiled": "run-p lint-typescript check-precompiled validate-externals-doc",
Expand Down Expand Up @@ -121,8 +121,8 @@
"@types/relay-runtime": "14.1.13",
"@types/string-hash": "1.1.1",
"@types/trusted-types": "2.0.3",
"@typescript-eslint/eslint-plugin": "7.16.0",
"@typescript-eslint/parser": "7.16.0",
"@typescript-eslint/eslint-plugin": "8.0.0",
"@typescript-eslint/parser": "8.0.0",
"@vercel/devlow-bench": "workspace:*",
"@vercel/fetch": "6.1.1",
"@vercel/og": "0.6.3",
Expand All @@ -144,15 +144,16 @@
"dd-trace": "4.12.0",
"es5-ext": "0.10.53",
"escape-string-regexp": "2.0.0",
"eslint": "8.56.0",
"eslint": "9.12.0",
"eslint-config-next": "workspace:*",
"eslint-formatter-codeframe": "7.32.1",
"eslint-plugin-eslint-plugin": "5.2.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jest": "27.6.3",
"eslint-plugin-jsdoc": "48.0.4",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-v8": "npm:eslint@^8.57.0",
"event-stream": "4.0.1",
"execa": "2.0.3",
"expect-type": "0.14.2",
Expand Down
12 changes: 6 additions & 6 deletions packages/eslint-config-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@
"homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config",
"dependencies": {
"@next/eslint-plugin-next": "15.0.0-canary.190",
"@rushstack/eslint-patch": "^1.3.3",
"@rushstack/eslint-patch": "^1.10.3",
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^5.0.0"
},
"peerDependencies": {
"eslint": "^7.23.0 || ^8.0.0",
"eslint": "^7.23.0 || ^8.0.0 || ^9.0.0",
"typescript": ">=3.3.1"
},
"peerDependenciesMeta": {
Expand Down
12 changes: 7 additions & 5 deletions packages/next/src/cli/next-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,23 @@ export type NextLintOptions = {
config?: string
dir?: string[]
errorOnUnmatchedPattern?: boolean
ext: string[]
file?: string[]
fix?: boolean
fixType?: string
format?: string
ignore: boolean
ignorePath?: string
inlineConfig: boolean
maxWarnings: number
outputFile?: string
quiet?: boolean
strict?: boolean
// TODO(jiwon): ESLint v9 unsupported options
// we currently delete them at `runLintCheck` when used in v9
ext: string[]
ignorePath?: string
reportUnusedDisableDirectivesSeverity: 'error' | 'off' | 'warn'
resolvePluginsRelativeTo?: string
rulesdir?: string
strict?: boolean
inlineConfig: boolean
maxWarnings: number
}

const eslintOptions = (
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/compiled/assert/assert.js

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions packages/next/src/compiled/babel-packages/packages-bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/next/src/compiled/util/util.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export async function applyNextFixture(
const fixture = new NextFixtureImpl(testInfo, nextOptions, nextWorker, page)

await fixture.setup()
// eslint-disable-next-line react-hooks/rules-of-hooks -- not React.use()
await use(fixture)

fixture.teardown()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export async function applyNextWorkerFixture(
): Promise<void> {
const fixture = new NextWorkerFixtureImpl()
await fixture.setup()
// eslint-disable-next-line react-hooks/rules-of-hooks -- not React.use()
await use(fixture)
fixture.teardown()
}
62 changes: 59 additions & 3 deletions packages/next/src/lib/eslint/runLintCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,25 @@ async function lint(

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

const { ESLint } = mod
const useFlatConfig =
// If V9 config was found, use flat config, or else use legacy.
eslintrcFile?.startsWith('eslint.config.')

let ESLint
// loadESLint is >= 8.57.0
// PR https://github.com/eslint/eslint/pull/18098
// Release https://github.com/eslint/eslint/releases/tag/v8.57.0
if ('loadESLint' in mod) {
// By default, configType is `flat`. If `useFlatConfig` is false, the return value is `LegacyESLint`.
// https://github.com/eslint/eslint/blob/1def4cdfab1f067c5089df8b36242cdf912b0eb6/lib/types/index.d.ts#L1609-L1613
ESLint = await mod.loadESLint({
useFlatConfig,
})
} else {
// eslint < 8.57.0, use legacy ESLint
ESLint = mod.ESLint
}

let eslintVersion = ESLint?.version ?? mod.CLIEngine?.version

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

if (semver.gte(eslintVersion, '9.0.0') && useFlatConfig) {
for (const option of [
'useEslintrc',
'extensions',
'ignorePath',
'reportUnusedDisableDirectives',
'resolvePluginsRelativeTo',
'rulePaths',
'inlineConfig',
'maxWarnings',
]) {
if (option in options) {
delete options[option]
}
}
}

let eslint = new ESLint(options)

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

const completeConfig: Config =
const completeConfig: Config | undefined =
await eslint.calculateConfigForFile(configFile)
if (!completeConfig) continue

const plugins = completeConfig.plugins

const hasNextPlugin =
// in ESLint < 9, `plugins` value is string[]
Array.isArray(plugins)
? plugins.includes('@next/next')
: // in ESLint >= 9, `plugins` value is Record<string, unknown>
'@next/next' in plugins

if (completeConfig.plugins?.includes('@next/next')) {
if (hasNextPlugin) {
nextEslintPluginIsEnabled = true
for (const [name, [severity]] of Object.entries(completeConfig.rules)) {
if (!name.startsWith('@next/next/')) {
Expand Down Expand Up @@ -309,6 +354,17 @@ export async function runLintCheck(
const eslintrcFile =
(await findUp(
[
// eslint v9
'eslint.config.js',
'eslint.config.mjs',
'eslint.config.cjs',
// TODO(jiwon): Support when it's stable.
// TS extensions are experimental and requires to install another package `jiti`.
// https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files
// 'eslint.config.ts',
// 'eslint.config.mts',
// 'eslint.config.cts',
// eslint <= v8
'.eslintrc.js',
'.eslintrc.cjs',
'.eslintrc.yaml',
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ function inheritFromMetadata(
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const commonOgKeys = ['title', 'description', 'images'] as const
function postProcessMetadata(
metadata: ResolvedMetadata,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/trace/report/to-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'path'
import { PHASE_DEVELOPMENT_SERVER } from '../../shared/lib/constants'
import type { TraceEvent } from '../types'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const localEndpoint = {
serviceName: 'nextjs',
ipv4: '127.0.0.1',
Expand Down
Loading

0 comments on commit 300bfe5

Please sign in to comment.