Skip to content

Commit c57fe25

Browse files
authored
feat: add a codemod to migrate from the deprecated "next lint" command (#82685)
## Add codemod to migrate from `next lint` to ESLint CLI This PR introduces a new codemod `next-lint-to-eslint-cli` that helps users migrate away from the deprecated `next lint` command to using ESLint directly, in preparation for Next.js 16 where `next lint` will be removed. ### What it does The codemod automates the migration process by: 1. **Updating package.json scripts** - Replaces all `next lint` commands with `eslint` equivalents - Maps Next.js-specific flags (`--strict` → `--max-warnings 0`, `--dir`/`--file` → paths) - Preserves other ESLint-compatible flags - Handles complex scripts with pipes, redirects, and multiple commands 2. **Managing ESLint configuration** - Creates a new flat config (`eslint.config.mjs`) if none exists - Updates existing flat configs to include Next.js rules - Provides guidance for legacy configs that need manual migration 3. **Installing dependencies automatically** - Adds required packages: `eslint`, `eslint-config-next`, `@eslint/eslintrc` - Detects and uses the project's package manager (npm/yarn/pnpm/bun) - Falls back to manual instructions if installation fails ### Usage ```bash npx @next/codemod@latest next-lint-to-eslint-cli . ``` ### Example transformations ```json // Before { "scripts": { "lint": "next lint", "lint:fix": "next lint --fix", "lint:strict": "next lint --strict --dir src" } } // After { "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", "lint:strict": "eslint --max-warnings 0 src" } } ``` ### Why this change? - `next lint` is being deprecated and will be removed in Next.js 16 - ESLint v9+ flat config is becoming the standard - Direct ESLint usage provides more flexibility and better ecosystem compatibility - Reduces Next.js maintenance burden of wrapping ESLint functionality The codemod ensures a smooth transition path for existing Next.js projects while following ESLint best practices.
1 parent ad48b8f commit c57fe25

20 files changed

+1681
-3
lines changed

docs/01-app/02-guides/upgrading/codemods.mdx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,73 @@ Replacing `<transform>` and `<path>` with appropriate values.
2424

2525
## Codemods
2626

27+
### 16.0
28+
29+
#### Migrate from `next lint` to ESLint CLI
30+
31+
##### `next-lint-to-eslint-cli`
32+
33+
```bash filename="Terminal"
34+
npx @next/codemod@canary next-lint-to-eslint-cli .
35+
```
36+
37+
This codemod migrates projects from using `next lint` to using the ESLint CLI with your local ESLint config. It:
38+
39+
- Creates an `eslint.config.mjs` file with Next.js recommended configurations
40+
- Updates `package.json` scripts to use `eslint .` instead of `next lint`
41+
- Adds necessary ESLint dependencies to `package.json`
42+
- Preserves existing ESLint configurations when found
43+
44+
For example:
45+
46+
```json filename="package.json"
47+
{
48+
"scripts": {
49+
"lint": "next lint"
50+
}
51+
}
52+
```
53+
54+
Becomes:
55+
56+
```json filename="package.json"
57+
{
58+
"scripts": {
59+
"lint": "eslint ."
60+
}
61+
}
62+
```
63+
64+
And creates:
65+
66+
```js filename="eslint.config.mjs"
67+
import { dirname } from 'path'
68+
import { fileURLToPath } from 'url'
69+
import { FlatCompat } from '@eslint/eslintrc'
70+
71+
const __filename = fileURLToPath(import.meta.url)
72+
const __dirname = dirname(__filename)
73+
74+
const compat = new FlatCompat({
75+
baseDirectory: __dirname,
76+
})
77+
78+
const eslintConfig = [
79+
...compat.extends('next/core-web-vitals', 'next/typescript'),
80+
{
81+
ignores: [
82+
'node_modules/**',
83+
'.next/**',
84+
'out/**',
85+
'build/**',
86+
'next-env.d.ts',
87+
],
88+
},
89+
]
90+
91+
export default eslintConfig
92+
```
93+
2794
### 15.0
2895
2996
#### Transform App Router Route Segment Config `runtime` value from `experimental-edge` to `edge`

packages/next-codemod/bin/transform.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ export async function runTransform(
113113
return require(transformerPath).default(filesExpanded, options)
114114
}
115115

116+
if (transformer === 'next-lint-to-eslint-cli') {
117+
// next-lint-to-eslint-cli transform doesn't use jscodeshift directly
118+
return require(transformerPath).default(filesExpanded, options)
119+
}
120+
116121
let args = []
117122

118123
const { dry, print, runInBand, jscodeshift, verbose } = options

packages/next-codemod/lib/handle-package.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export function getPkgManager(baseDir: string): PackageManager {
3131
return 'npm'
3232
}
3333
}
34+
// No lock file found, default to npm
35+
return 'npm'
3436
} catch {
3537
return 'npm'
3638
}

packages/next-codemod/lib/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,9 @@ export const TRANSFORMER_INQUIRER_CHOICES = [
121121
value: 'next-experimental-turbo-to-turbopack',
122122
version: '10.0.0',
123123
},
124+
{
125+
title: 'Migrate from `next lint` to the ESLint CLI',
126+
value: 'next-lint-to-eslint-cli',
127+
version: '16.0.0',
128+
},
124129
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "my-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint"
10+
},
11+
"dependencies": {
12+
"react": "^18.3.0",
13+
"react-dom": "^18.3.0",
14+
"next": "15.0.0"
15+
},
16+
"devDependencies": {
17+
"typescript": "^5",
18+
"@types/node": "^20",
19+
"@types/react": "^19",
20+
"@types/react-dom": "^19"
21+
}
22+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "my-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "eslint ."
10+
},
11+
"dependencies": {
12+
"react": "^18.3.0",
13+
"react-dom": "^18.3.0",
14+
"next": "15.0.0"
15+
},
16+
"devDependencies": {
17+
"typescript": "^5",
18+
"@types/node": "^20",
19+
"@types/react": "^19",
20+
"@types/react-dom": "^19",
21+
"eslint": "^9",
22+
"eslint-config-next": "15.0.0",
23+
"@eslint/eslintrc": "^3"
24+
}
25+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = [
2+
{
3+
rules: {
4+
"quotes": ["error", "double"],
5+
"indent": ["error", 2]
6+
}
7+
}
8+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "complex-scripts-app",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"lint": "next lint --fix --dir src --dir pages",
6+
"lint:strict": "next lint --strict",
7+
"lint:ci": "next lint --quiet --output-file lint-results.json",
8+
"precommit": "next lint --fix && npm test",
9+
"test": "jest && next lint",
10+
"complex": "npm run build && next lint --dir . --ext .js,.jsx,.ts,.tsx 2>/dev/null",
11+
"pipe": "next lint | tee lint.log",
12+
"redirect": "next lint > output.txt 2>&1",
13+
"multi": "next lint; next build; next lint --fix"
14+
},
15+
"dependencies": {
16+
"next": "15.0.0"
17+
}
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "complex-scripts-app",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"lint": "eslint --fix src pages",
6+
"lint:strict": "eslint --max-warnings 0 .",
7+
"lint:ci": "eslint --quiet --output-file lint-results.json .",
8+
"precommit": "eslint --fix . && npm test",
9+
"test": "jest && eslint .",
10+
"complex": "npm run build && eslint . 2>/dev/null",
11+
"pipe": "eslint . | tee lint.log",
12+
"redirect": "eslint . > output.txt 2>&1",
13+
"multi": "eslint .; next build; eslint --fix ."
14+
},
15+
"dependencies": {
16+
"next": "15.0.0"
17+
},
18+
"devDependencies": {
19+
"eslint": "^9",
20+
"eslint-config-next": "15.0.0",
21+
"@eslint/eslintrc": "^3"
22+
}
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "my-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint"
10+
},
11+
"dependencies": {
12+
"react": "^18.3.0",
13+
"react-dom": "^18.3.0",
14+
"next": "15.0.0"
15+
},
16+
"devDependencies": {
17+
"eslint": "^8.0.0",
18+
"eslint-config-next": "15.0.0"
19+
}
20+
}

0 commit comments

Comments
 (0)