Chris.luChris.lu header image, displaying an 80s style landscape and sunset

Linting setup using ESLint

Adding linting to a project is something I recommend doing as early as possible, similar to adding CSP to a project

Those are things that, if you postpone them, then you will have a lot more work later, which is why it is best to add linting as early as possible and then fix linting-related problems one by one as soon as they come up

The state of ESLint flat config files

The following ESLint setup uses the ESLint "Classic" configuration files, which is the default for all ESLint versions below 9

In ESLint 9, those configuration files are now deprecated, and it is recommended to use the new flat config files, which are the new default since the release of ESLint 9

ESLint mentions in their documentation:

We are transitioning to a new config system in ESLint v9.0.0. The config system shared on this page is currently the default but will be deprecated in v9.0.0. You can opt-in to the new config system by following the instructions in the documentation.

Also, support for eslintrc (classic) configuration files will be removed in version 10.0.0

Warning

I, however, will use the classic configuration files with overrides in this tutorial, as this is what is currently supported by Next.js

A lot of plugins like typescript-eslint have started working on support for ESLint 9 / flat config files, as you can see in typescript-eslint Issue #8211 but typescript-eslint 8 (with ESLint 9 support) has not been released yet (as of today April 30, 2024), there are also a lot of other plugins that have not completed the transition to eslint 9 / flat config yet, if you are interested in the progress of those plugins then have a look at the Issue in the ESLint repository that keeps track of the flat config rollout for many packages

Even though we won't use them yet (I will update the ESLint chapter when Next.js adds support for flat config), expect the new flat config files to become the new default in the foreseeable future

Answering some questions regarding the linting setup

Note

The next 3 chapters contain a lot of theory:

  • The 1st chapter explains what create-next-app did in regards to linting
  • The 2nd chapter explains what packages related to linting Next.js has
  • The 3rd explains why we do want to modify the current ESLint setup

So if you prefer to get straight to the solution (code) then skip ahead to the "Installing the MDX ESLint plugin and parser" chapter if you have the time and are interested in understanding the "Why"* then I recommend reading on

But didn't Next.js already set up linting?

Yes, Next.js has built-in linting support, this chapter is a recap of what Next.js has done so far

Earlier in this tutorial, we used create-next-app, which has installed ESLint as well as the eslint-config-next package for us (both packages got added to the devDependencies in the package.json)

create-next-app has also added a .eslintrc.json file in the root of the project, in that file, Next.js has added a default configuration that works best for most projects, Next.js has added the .eslintrc.json file so that the linting setup that gets used by the lint command can also be used by your (VSCode) IDE itself

If install ESLint for Next.js manually (without creating lint configuration file manually and do NOT use create next app) but add this lint command "lint": "next lint" to your package.json scripts and then execute it for the first time, then Next.js will detect that there is no .eslintrc.json and it will ask you if you want to use the Base mode or the Strict mode, because we used create-next-app, it did not let us chose if we prefer the Base mode or the Strict mode, that's because when using create-next-app it chooses the strict mode by default, which is why in your .eslintrc.json there is an extends of next/core-web-vitals (which is the strict mode) and not just an extends of next (which is the base mode)

next/core-web-vitals is a set of extra rules that will check your code and inform you about potential optimizations you can do that are related to core web vitals metrics, like rules to improve page loading speed, but next/core-web-vitals will also extend the base next rules

Because create-next-app has added the "lint": "next lint" to your package.json scripts, you can use the command npm run lint, which will execute next lint. next lint is the Next.js CLI command for linting

Next.js has 2 packages that are related to ESLint, one is called eslint-config-next (ESLint Config), and the other one is called eslint-plugin-next (ESLint Plugin)

Package 1: eslint-config-next (ESLint Config) intends to make it easier to get started with ESLint by installing and configuring several plugins for us, some of these plugins are:

Package 2: eslint-plugin-next is the actual ESLint plugin for Nextjs (called @next/eslint-plugin-next on npmjs), it aims to catch common problems in a Next.js application

For a complete list of rules that the Next.js ESLint plugin adds check out the Nextjs "ESLint rules" documentation or have a look at the eslint-plugin-next rules directory on GitHub

Why are we changing the Next.js linting setup?

The Next.js linting setup lints code in .ts and .tsx files using the typescript-eslint parser, however, it does not lint markdown syntax and code in MDX files for which you need to have an MDX parser installed

This is why we are going to add 3 packages to do the linting of markdown content in MDX pages:

The recommended way to add eslint-plugin-mdx as described in their README is to use the overrides feature of ESLint (if you want to know more about the parsing issues you might have if NOT using overrides to check out the eslint-plugin-mdx GitHub issue #251)

One problem we are facing is that, even though Next.js has created a .eslintrc.json for us that lets us do some fine-tuning of rules, adding a new overrides for markdown will not work due to a limitation how the next lint CLI works (there is open discussion next lint command doesn't support overrides #35228 where the limitation gets discussed), next lint doesn't use the .eslintrc.json that create-next-app added to the root of project, it just added that file so that our IDE (VSCode) can do linting in files using the same setup as Next.js

As the next lint command ignores custom overrides that are in your .eslintrc.json, we will NOT be able to use the next lint CLI. Instead, we will create a custom lint command in our package.json scripts, by using our command we ensure that the eslint configuration file in the root of our project gets used to configure the linting process. This solution will be used for linting in the IDE, that happens while we are coding as well as the linting that will happen when using the npm run lint command

Finally, there is yet another problem, the Next.js CLI build command does not use the package.json lint script but uses the Next.js lint CLI directly, this means that we will need to tell the build CLI NOT to do linting during builds (using the default Next.js CLI linting) and then we will manually re-add linting for builds by changing the package.json build script so that it uses our package.json lint script before doing an actual build

Installing the MDX ESLint plugin and parser

First, we need to make sure the MDX eslint plugin (and parser) are installed by using the following command:

npm i eslint-plugin-mdx@latest --save-exact --save-dev

The ESLint MDX plugin has the ESLint MDX parser (called eslint-mdx) listed as a dependency so we don't need to explicitly list it as it will get installed too, alongside other packages like the eslint-plugin-markdown and a few others

ESLint configuration step 1: Basic ESLint configuration file

create next app has added a .eslintrc.json in the root of our project, as we will add our own custom eslint configuration, we can get rid of that file

The first thing we need to do is delete the current .eslintrc.json

create next app should have installed all dependencies needed, so at this point we do NOT need to install any new packages

If you didn't use create-next-app as we did in this tutorial, then I recommend using this command to ensure all packages are installed:

npm i eslint@latest eslint-config-next@latest --save-exact --save-dev
Warning

As the new ESLint 9 did get released, you might encounter backward compatibility problems when using the latest version in combination with the next config eslint or other plugins, in that case, I recommend using the latest version of the 8.x branch until the flat config is more widely supported, as of now this is ESLint 8.57.0

Then create a new .eslintrc.js (in the root of your project) and add the following content:

.eslintrc.js
module.exports = {
    root: true,
    parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
    },
    'env': {
        browser: true,
        es2021: true,
        node: true,
    },
    ignorePatterns: [
        'node_modules/',
        '.next/',
        '.vscode/',
        'public/',
    ],
    reportUnusedDisableDirectives: true,
    overrides: [
    ],
}
 
Note

You may have noticed that I chose javascript for the eslint configuration file and not JSON. I usually use javascript over json as it allows me to add comments

However, if you prefer json, feel free to create a .eslintrc.json instead of a .eslintrc.js or use one of the many other ESLint configuration file formats that are supported

The above basic setup is inspired by what you get if you use the eslint init script to setup ESLint in a new project, using eslint init in a sandbox folder is a good way to see what kind of basic setup the ESLint team recommends

I added root: true to make sure eslint stops at the root of my project and does not attempt to check for other eslint configuration files in parent directories

I also added some entries in the ignorePatterns to make sure ESLint is not going to lint anything in those folders. You might want to add other folders to this list over time if you want ESLint to exclude those folders from linting

I then enabled the option reportUnusedDisableDirectives to make sure ESLint will trigger a warning if it finds unused disable eslint comments, which can happen when code gets deleted or moved around and suddenly an // eslint-disable-next-line comment becomes useless

ESLint configuration step 2: ESLint ts(x) and md(x) files override

The 1st override we add to the array is fairly short, as its only purpose is to tell ESLint which rule sets we want to use, no matter if it is mdx / markdown content in md(x) files or typescript code in ts(x) files

We do this, because of a rule from the Next.js ESLint plugin that will recommend that you use next/image instead of a regular <img> element, this rule can be useful in both MDX files and also for Typescript code in React components as well as in Next.js pages

Add the following object to the overrides array:

.eslintrc.js
overrides: [
    {
        files: ['**/*.ts?(x)', '**/*.md?(x)'],
        extends: [
            'next/core-web-vitals',
        ],
    },
],

What this override does:

ESLint configuration step 3: ESLint ts(x) files only override

The 2nd override is specifically for typescript code, its primary purpose is to tell ESLint to use the @typescript-eslint/parser parser to parse ts(x) files

For this override, I have 2 options you can chose from:

Option 1: typescript only parser + Next.js config (which includes react, react-hooks, ...)

If you chose option 1 (the less strict version), then add the following override into your .eslintrc.js:

.eslintrc.js
overrides: [
    {
        files: ['**/*.ts?(x)', '**/*.md?(x)'],
        extends: [
            'next/core-web-vitals',
        ],
    },
    {
        files: ['**/*.ts?(x)'],
        parser: '@typescript-eslint/parser',
        parserOptions: {
            sourceType: 'module',
            ecmaFeatures: {
                jsx: true,
            },
            warnOnUnsupportedTypeScriptVersion: true,
        },
        rules: {
            quotes: [
                'error',
                'single',
                { "allowTemplateLiterals": true },
            ],
            semi: [
                'error',
                'never',
            ],
        },
    },
],

What this override does:

Option 2: everything that is in option 1 + typescript code rules

I like option 2 best as it adds a lot of good rules that will check your typescript code and give you feedback if needed. If you too decide to use this option, but it finds too many problems in your code, then maybe set the rules to warn instead of error until you have time to fix problems that get reported. Later you can switch back to error to enforce the rules (or you might want to deactivate some of the rules completely)

If you chose option 2 (the stricter version with more typescript-related rules), we first need to install the additional @typescript-eslint/eslint-plugin package, but to ensure that this package uses the same version as the @typescript-eslint/parser package, I recommend installing both, like so:

npm i @typescript-eslint/parser@7.18.0 @typescript-eslint/eslint-plugin@7.18.0 --save-exact --save-dev

We install version 7.18.0 for both plugins as this is (as of now July 2024) the latest version of the 7.x branch (v8.x are for ESLint 9)

Warning

If you get an NPM error because the version of the installed typescript-eslint/parser and the typescript-eslint/plugin version doesn't match:

npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
Conflicting peer dependency: @typescript-eslint/parser

Then use the following command to remove the parser:

npm remove @typescript-eslint/parser  

Then delete your package-lock.json (NOT the package.json file) file in the root in the repository

Finally, try out the installation command (above) again

Next, we put the following override into our overrides array (instead of the one in option 1):

.eslintrc.js
overrides: [
    {
        files: ['**/*.ts?(x)', '**/*.md?(x)'],
        extends: [
            'next/core-web-vitals',
        ],
    },
    {
        files: ['**/*.ts?(x)'],
        extends: [
            'plugin:@typescript-eslint/recommended-type-checked',
            'plugin:@typescript-eslint/stylistic-type-checked',
        ],
        parser: '@typescript-eslint/parser',
        parserOptions: {
            sourceType: 'module',
            ecmaFeatures: {
                jsx: true,
            },
            warnOnUnsupportedTypeScriptVersion: true,
            project: './tsconfig.json',
        },
        plugins: [
            '@typescript-eslint',
        ],
        rules: {
            quotes: [
                'error',
                'single',
                { "allowTemplateLiterals": true },
            ],
            semi: [
                'error',
                'never',
            ],
            '@typescript-eslint/consistent-indexed-object-style': 'off',
            '@typescript-eslint/ban-ts-comment': [
                'error',
                {
                    'ts-expect-error': 'allow-with-description',
                    'ts-ignore': 'allow-with-description',
                    'ts-nocheck': false,
                    'ts-check': false,
                    minimumDescriptionLength: 3,
                },
            ],
        },
    },
],
Warning

This overrides is not in addition to the overrides in option 1, it is a full replacement

So you chose to either add option 1 or option 2, but NOT BOTH

What this override does:

ESLint configuration step 4: ESLint md(x) files only overrides

The 3rd and final overrides is specifically for markdown / mdx content, its primary purpose is to tell ESLint to use the eslint-mdx parser to parse md(x) files, insert this into youroverrides: [] array at the end:

.eslintrc.js
    {
        files: ['**/*.md?(x)'],
        extends: [
            'plugin:mdx/recommended',
        ],
        parser: 'eslint-mdx',
        parserOptions: {
            markdownExtensions: ['*.md, *.mdx'],
        },
        settings: {
            'mdx/code-blocks': false,
            'mdx/remark': true,
        },
        rules: {
            'react/no-unescaped-entities': 0,
        }
        // markdown rules get configured in remarkrc.mjs
    },

This was the last overrides, which means our configuration file is now complete, you can now save the .eslintrc.js file

Congratulations 🎉 if you made it this far, we had to make a lot of changes, but you now know a lot more about how ESLint works, and we are now ready to start adding linting for MDX content in the next part of this tutorial

If you liked this post, please consider making a donation ❤️ as it will help me create more content and keep it free for everyone