Skip to content

Commit 47d09ec

Browse files
committed
feat: update linting in Next.js
1 parent 8c11037 commit 47d09ec

Some content is hidden

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

50 files changed

+1259
-36
lines changed

docs/01-app/03-api-reference/06-cli/create-next-app.mdx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ The following options are available:
2121
| --------------------------------------- | --------------------------------------------------------------- |
2222
| `-h` or `--help` | Show all available options |
2323
| `-v` or `--version` | Output the version number |
24-
| `--no-*` | Negate default options. E.g. `--no-eslint` |
24+
| `--no-*` | Negate default options. E.g. `--no-ts` |
2525
| `--ts` or `--typescript` | Initialize as a TypeScript project (default) |
2626
| `--js` or `--javascript` | Initialize as a JavaScript project |
2727
| `--tailwind` | Initialize with Tailwind CSS config (default) |
2828
| `--eslint` | Initialize with ESLint config |
29+
| `--biome` | Initialize with Biome config |
30+
| `--no-linter` | Skip linter configuration |
2931
| `--app` | Initialize as an App Router project |
3032
| `--api` | Initialize a project with only route handlers |
3133
| `--src-dir` | Initialize inside a `src/` directory |
@@ -58,14 +60,24 @@ You will then be asked the following prompts:
5860
```txt filename="Terminal"
5961
What is your project named? my-app
6062
Would you like to use TypeScript? No / Yes
61-
Would you like to use ESLint? No / Yes
63+
Which linter would you like to use? ESLint / Biome / None
6264
Would you like to use Tailwind CSS? No / Yes
6365
Would you like your code inside a `src/` directory? No / Yes
6466
Would you like to use App Router? (recommended) No / Yes
6567
Would you like to use Turbopack? (recommended) No / Yes
6668
Would you like to customize the import alias (`@/*` by default)? No / Yes
6769
```
6870

71+
### Linter Options
72+
73+
**ESLint**: The traditional and most popular JavaScript linter. Includes Next.js-specific rules from `eslint-plugin-next`.
74+
75+
**Biome**: A fast, modern linter and formatter that combines the functionality of ESLint and Prettier. Includes built-in Next.js and React domain support for optimal performance.
76+
77+
**None**: Skip linter configuration entirely. You can always add a linter later.
78+
79+
> **Note**: `next lint` command will be deprecated in Next.js 16. New projects should use the chosen linter directly (e.g., `biome check` or `eslint`).
80+
6981
Once you've answered the prompts, a new project will be created with your chosen configuration.
7082

7183
### With an official Next.js example

packages/create-next-app/create-app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export async function createApp({
3131
typescript,
3232
tailwind,
3333
eslint,
34+
biome,
3435
app,
3536
srcDir,
3637
importAlias,
@@ -48,6 +49,7 @@ export async function createApp({
4849
typescript: boolean
4950
tailwind: boolean
5051
eslint: boolean
52+
biome: boolean
5153
app: boolean
5254
srcDir: boolean
5355
importAlias: string
@@ -235,6 +237,7 @@ export async function createApp({
235237
isOnline,
236238
tailwind,
237239
eslint,
240+
biome,
238241
srcDir,
239242
importAlias,
240243
skipInstall,

packages/create-next-app/index.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const program = new Command(packageJson.name)
5050
.option('--js, --javascript', 'Initialize as a JavaScript project.')
5151
.option('--tailwind', 'Initialize with Tailwind CSS config. (default)')
5252
.option('--eslint', 'Initialize with ESLint config.')
53+
.option('--biome', 'Initialize with Biome config.')
5354
.option('--app', 'Initialize as an App Router project.')
5455
.option('--src-dir', "Initialize inside a 'src/' directory.")
5556
.option('--turbopack', 'Enable Turbopack by default for development.')
@@ -229,6 +230,7 @@ async function run(): Promise<void> {
229230
const defaults: typeof preferences = {
230231
typescript: true,
231232
eslint: false,
233+
linter: 'none',
232234
tailwind: true,
233235
app: true,
234236
srcDir: false,
@@ -277,23 +279,63 @@ async function run(): Promise<void> {
277279
}
278280
}
279281

280-
if (!opts.eslint && !args.includes('--no-eslint') && !opts.api) {
282+
// Determine linter choice if not specified via CLI flags
283+
// Support both --no-linter (new) and --no-eslint (legacy) for backward compatibility
284+
const noLinter =
285+
args.includes('--no-linter') || args.includes('--no-eslint')
286+
287+
if (!opts.eslint && !opts.biome && !noLinter && !opts.api) {
281288
if (skipPrompt) {
282-
opts.eslint = getPrefOrDefault('eslint')
289+
const preferredLinter = getPrefOrDefault('linter')
290+
opts.eslint = preferredLinter === 'eslint'
291+
opts.biome = preferredLinter === 'biome'
292+
// No need to set noLinter flag since we check args at runtime
283293
} else {
284-
const styledEslint = blue('ESLint')
285-
const { eslint } = await prompts({
294+
const { linter } = await prompts({
286295
onState: onPromptState,
287-
type: 'toggle',
288-
name: 'eslint',
289-
message: `Would you like to use ${styledEslint}?`,
290-
initial: getPrefOrDefault('eslint'),
291-
active: 'Yes',
292-
inactive: 'No',
296+
type: 'select',
297+
name: 'linter',
298+
message: 'Which linter would you like to use?',
299+
choices: [
300+
{
301+
title: 'ESLint',
302+
value: 'eslint',
303+
description: 'More comprehensive lint rules',
304+
},
305+
{
306+
title: 'Biome',
307+
value: 'biome',
308+
description: 'Fast formatter and linter (fewer rules)',
309+
},
310+
{
311+
title: 'None',
312+
value: 'none',
313+
description: 'Skip linter configuration',
314+
},
315+
],
316+
initial: 0,
293317
})
294-
opts.eslint = Boolean(eslint)
295-
preferences.eslint = Boolean(eslint)
318+
319+
opts.eslint = linter === 'eslint'
320+
opts.biome = linter === 'biome'
321+
preferences.linter = linter
322+
323+
// Keep backwards compatibility with old eslint preference
324+
preferences.eslint = linter === 'eslint'
296325
}
326+
} else if (opts.eslint) {
327+
opts.biome = false
328+
preferences.linter = 'eslint'
329+
preferences.eslint = true
330+
} else if (opts.biome) {
331+
opts.eslint = false
332+
preferences.linter = 'biome'
333+
preferences.eslint = false
334+
} else if (noLinter) {
335+
opts.eslint = false
336+
opts.biome = false
337+
preferences.linter = 'none'
338+
preferences.eslint = false
297339
}
298340

299341
if (!opts.tailwind && !args.includes('--no-tailwind') && !opts.api) {
@@ -426,6 +468,7 @@ async function run(): Promise<void> {
426468
typescript: opts.typescript,
427469
tailwind: opts.tailwind,
428470
eslint: opts.eslint,
471+
biome: opts.biome,
429472
app: opts.app,
430473
srcDir: opts.srcDir,
431474
importAlias: opts.importAlias,
@@ -459,6 +502,7 @@ async function run(): Promise<void> {
459502
packageManager,
460503
typescript: opts.typescript,
461504
eslint: opts.eslint,
505+
biome: opts.biome,
462506
tailwind: opts.tailwind,
463507
app: opts.app,
464508
srcDir: opts.srcDir,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"files": {
9+
"ignoreUnknown": true,
10+
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"indentStyle": "space",
15+
"indentWidth": 2
16+
},
17+
"linter": {
18+
"enabled": true,
19+
"rules": {
20+
"recommended": true
21+
},
22+
"domains": {
23+
"next": "recommended",
24+
"react": "recommended"
25+
}
26+
},
27+
"assist": {
28+
"actions": {
29+
"source": {
30+
"organizeImports": "on"
31+
}
32+
}
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"files": {
9+
"ignoreUnknown": true,
10+
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"indentStyle": "space",
15+
"indentWidth": 2
16+
},
17+
"linter": {
18+
"enabled": true,
19+
"rules": {
20+
"recommended": true
21+
},
22+
"domains": {
23+
"next": "recommended",
24+
"react": "recommended"
25+
}
26+
},
27+
"assist": {
28+
"actions": {
29+
"source": {
30+
"organizeImports": "on"
31+
}
32+
}
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"files": {
9+
"ignoreUnknown": true,
10+
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"indentStyle": "space",
15+
"indentWidth": 2
16+
},
17+
"linter": {
18+
"enabled": true,
19+
"rules": {
20+
"recommended": true
21+
},
22+
"domains": {
23+
"next": "recommended",
24+
"react": "recommended"
25+
}
26+
},
27+
"assist": {
28+
"actions": {
29+
"source": {
30+
"organizeImports": "on"
31+
}
32+
}
33+
}
34+
}

packages/create-next-app/templates/app-empty/js/eslint.config.mjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ const compat = new FlatCompat({
99
baseDirectory: __dirname,
1010
});
1111

12-
const eslintConfig = [...compat.extends("next/core-web-vitals")];
12+
const eslintConfig = [
13+
...compat.extends("next/core-web-vitals"),
14+
{
15+
ignores: [
16+
"node_modules/**",
17+
".next/**",
18+
"out/**",
19+
"build/**",
20+
"next-env.d.ts",
21+
],
22+
},
23+
];
1324

1425
export default eslintConfig;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"files": {
9+
"ignoreUnknown": true,
10+
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"indentStyle": "space",
15+
"indentWidth": 2
16+
},
17+
"linter": {
18+
"enabled": true,
19+
"rules": {
20+
"recommended": true
21+
},
22+
"domains": {
23+
"next": "recommended",
24+
"react": "recommended"
25+
}
26+
},
27+
"assist": {
28+
"actions": {
29+
"source": {
30+
"organizeImports": "on"
31+
}
32+
}
33+
}
34+
}

packages/create-next-app/templates/app-empty/ts/eslint.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ const compat = new FlatCompat({
1111

1212
const eslintConfig = [
1313
...compat.extends("next/core-web-vitals", "next/typescript"),
14+
{
15+
ignores: [
16+
"node_modules/**",
17+
".next/**",
18+
"out/**",
19+
"build/**",
20+
"next-env.d.ts",
21+
],
22+
},
1423
];
1524

1625
export default eslintConfig;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"files": {
9+
"ignoreUnknown": true,
10+
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"indentStyle": "space",
15+
"indentWidth": 2
16+
},
17+
"linter": {
18+
"enabled": true,
19+
"rules": {
20+
"recommended": true
21+
},
22+
"domains": {
23+
"next": "recommended",
24+
"react": "recommended"
25+
}
26+
},
27+
"assist": {
28+
"actions": {
29+
"source": {
30+
"organizeImports": "on"
31+
}
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)