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
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
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
Why does Next.js have two packages related to ESLINT?
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:
- eslint-plugin-react
- eslint-plugin-react-hooks
- eslint-plugin-next
- and some more, if you want the full list of plugins that eslint-config-next installs, check out the eslint-config-next package.json dependencies
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 first one is a remark plugin called remark lint that will lint the markdown syntax we use to format our content in MDX pages
- the second one is an ESLint plugin called eslint-plugin-mdx which contains the configuration and rules for linting MDX
- the third one is a parser called eslint-mdx which will parse the content of MDX files
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:
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:
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:
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:
What this override does:
- files will include any code file that has a ts or tsx extension and any content file that has a md or mdx extension, meaning this override is for both code in our typescript files as well as content in our MDX files
- extends is set so that it will use the rules from the core-web-vitals (Next.js linting strict mode), core-web-vitals will add a few rules related to core web vitals, but also extends the next base rules (so there is no need to add 'next' to the extends too)
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:
- the 1st option: this is what Next.js uses in eslint-config-next WITHOUT any of the @typescript-eslint rule sets enabled
- the 2nd option is more strict version, that will also include the @typescript-eslint/recommended rule set, using it will add a bunch of typescript-related rules, like the no-unnecessary-type-assertion rule that checks if you have type assertions that are not needed; it is possible to get an even stricter version if you also enable the stylistic-type-checked rule set, which will, for example, use prefer-nullish-coalescing in cases where you could have, but did NOT, use the Nullish coalescing operator
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
:
What this override does:
- files is set to ts(x), meaning this override is only for ts and tsx files
- there is no extends defined, as it will already use the extends of the previous override in which we extended next/core-web-vitals (next/core-web-vitals will then extend the base next rule set)
- because this overrides is specifically for typescript files, we set the parser to use the @typescript-eslint parser (instead of the default eslint parser, which only supports parsing javascript)
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 disactivate some of the rules completly)
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:
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)
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:
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):
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:
- files is set to ts(x), meaning this override is only for ts and tsx files
- extends is set to extend the recommended rules of the @typescript-eslint plugin as well as the stylistic rules, if you only want the recommended rules but NOT the stylistic, then comment the
'plugin:@typescript-eslint/stylistic-type-checked',
line out - because this overrides is specifically for typescript files, we set the parser to use the @typescript-eslint parser (instead of the default eslint parser, which only supports parsing javascript)
- plugins is set to typescript-eslint, but this is not the only plugin that will be used for typescript files, in the previous overrides, we already added the Next.js eslint plugin, so there is no need to add it again here (the Next.js eslint config will include a bunch of other plugins like react, react-hooks and some more)
- finally, we have the rules option, here you can add whatever rules you need in your project
- I like to single quotes and not double quotes in my javascript code this is why I set quotes to single (feel free to set it to double if you prefer)
- then I set the rule for semicolons at the end of a javascript line to never because I don't use semicolons at the end of lines in my code (as long as they are optional, in the few cases where they are required, I of course use them, but those cases are very rare)
- next, I also disable the consistent-indexed-object-style rule, I usually like consistency but in this case, I think the rule goes too far, different variants are supported by typescript and as long as there their syntax is right I feel like letting the dev chose which one he prefers, feel free to remove this line if you prefer enforcing the consistency
- finally, I added a configuration for the ban-ts-comment rule to allow the ts-expect-error and ts-ignore comments but only when there is a description as to why those comments got added to the code (by default no comments are allowed)
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:
- files is set to md(x), meaning this overrides is only for md and mdx files
- extends is set to extend the recommended recommended MDX plugin rules
- because this overrides is specifically for MDX (and markdown) files, we set the parser to use the MDX parser (instead of the default eslint parser, which only supports parsing javascript)
- settings is for to do 2 things:
- mdx/code-blocks is set to false to disable the linting of code blocks, you may want to enable this instead, it can be a very nice feature to have eslint MDX use ESLint to lint the code inside of code blocks, I, however, shorten code in code blocks, and this creates a case where rules get triggered just because not all the code is in every code block, like using a variable that has not been created or using a package but the import is missing, this is why for myself I disabled this (I sometimes enable it periodically to lint a new files but then disable it before committing the code for the reasons I just described)
- mdx/remark is set to true because this enables eslint-mdx to use the remark-lint plugin, it will read
.remarkrc.mjs
configuration file and use all the remark-line rules that are listed in that file, this means that when ESLint will now be capable to also lint our markdown content (both in the IDE and when using thenpm run lint
command)
- finally, in the rules, I disable the react eslint plugin react/no-unescaped-entities rule because I don't want to have to escape entities in my markdown files, I understand that the react/no-unescaped-entities is useful in react components but not in markdown content, all other react rules we keep them as is (those rules are enabled because in the very first overrides we added next/core-web-vitals, which extends the next config, which then adds a bunch of eslint packages, each comes with their own set comes, to see which ones it adds look at the Why does Next.js have two packages related to ESLINT? chapter and have a look at Package 1: eslint-config-next)
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