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

Linting in VSCode using ESLint and MDX extensions

We now have linting set up and a custom lint command 🎉, but we have no linting in VSCode itself (yet)

For that reason we are now going to install 2 VSCode extensions

The first one will add linting messages directly into our code, and the second one will add MDX language support to the VSCode IntelliSense

Having the ESLint extension is great because it allows us to see linting warnings and errors as we code, instead of having to wait for the linting command to run, meaning we can fix the problems one by one as they occur instead of waiting until the last moment and potentially having to fix a lot of issues all at once

ESLint extension

The first extension I recommend installing is the VSCode "ESLint" extension

Adding the ESLint extension to VSCode

If you need help installing the ESLint extension, have a look at my "Installing extensions" chapter in the VSCode post

Open the extensions view, then search for an extension named ESLint (published my Microsoft) and then click on the install button

ESLint extension settings

After installing the ESLint extension, you need to edit the settings of that extension to add things like the .mdx extension to the list of file extensions that it will use

You have several options when editing VSCode settings (spoiler alert: I will use option 2)

Option 1: Open the extensions view (extensions list), if you don't know (yet) how to do this, then I recommend checking out my "VSCode extensions view" chapter in the VSCode post

Then click on the gear icon (⚙️) of the ESLint extension, and then in the menu, select Extension Settings

Option 2: If you have already set custom settings for your VSCode workspace, then you will have a .vscode folder in your project root, if not, create that folder, then inside of that folder, you will have a settings.json file, if that file is not there create it

Note

When you edit settings, you can do it on a User level or Workspace level

In this case, we will do it on a Workspace level by using the settings.json file inside of the .vscode folder (that is in the root of our project), to learn more about how to use the VSCode settings I recommend checking out the VSCode settings in the VSCode post

Now open the .vscode/settings.json file and add the following settings for our ESLint extension:

/.vscode/settings.json
{
    "typescript.tsdk": "node_modules\\typescript\\lib",
    "eslint.debug": true,
    "eslint.options": {
        "extensions": [
            ".js",
            ".jsx",
            ".md",
            ".mdx",
            ".ts",
            ".tsx"
        ]
    },
    "eslint.validate": [
        "markdown",
        "mdx",
        "javascript",
        "javascriptreact",
        "typescript",
        "typescriptreact"
    ]
}

Add a comma at the end of the typescript.tsdk line that is already in the settings.json file, then add our ESLint extension settings below

This will tell the ESLint extension to include files with the ts(x) as well as md(x) extension, and it will make sure that it validates markdown, mdx, javascript, typescript, and also react code

MDX extension

The second VSCode extension I recommend installing is the VSCode MDX (Language support for MDX), when working with MDX content, as this extension will add IntelliSense support for MDX files to VSCode

Note

This extension is still experimental, but I had no problems when using it

Open the extensions view, then search for an extension named MDX (published my unified) and then click on the install button

Unlike the ESLint extension, the MDX extension does not require any custom settings configuration, you are all set

Restarting the ESLint server in VSCode

Every time you make changes to your ESLint setup, for example, after editing either the .eslintrc.js ESLint configuration file or the .remarkrc.mjs remark-lint configuration file, I recommend restarting the ESLint server in VSCode to make sure the changes get applied immediately

To restart the eslint server in VSCode, press ctrl + shift + p to open the command palette, then type ESLint and choose the command called ESLint: Restart ESLint Server

Congratulations 🎉 you just completed the ESLint setup of your project, you now have a linting command for your code as well as your MDX content, you installed 2 extensions in VSCode so that you now also have full ESLint and MDX (markdown) linting inside of our VSCode IDE

Ensure linting in VSCode works as intended (optional)

Now, the final question is: "But does it work?"

To test if everything works as intended, we will create some errors in both the tsx and mdx files, and if we did the setup correctly, we should get linting errors both in VSCode as well as in the command line output

Note

The next few chapters are to verify our linting setup and give you and idea what kind of warning and errors to expect from the linting tools, if you prefer you can skip them and continue with the chapter Disabling rules using comments or come back later to read the chapters you skiped, if you have some time though I recommend checking them out

Testing the MDX file(s) linting process (in VSCode)

We are going to create a test MDX file with some content to see if VSCode displays linting warnings for 3 problems we are going to add to our content

First, let's create a tests folder in the root of our project, and inside that folder, add another folder called eslint, then in that folder, create a file called content.mdx with the following content:

/tests/eslint/content.mdx
# title
 
# Another level 1 headline (it should trigger a linting error)
 
<img src="../../../public/assets/images/chris-octocat-1677036079874.png" />
 
The linting errors that should be shown in this file:
- On line 3: Unexpected duplicate toplevel heading (remark-lint-no-multiple-toplevel-headings)
- On line 5: error no image element use next/image
- On line 10 (this line): should trigger: Unexpected missing final newline character, the last character of this line should have a wavy underline (remark-lint-final-newline)

If your ESLint setup is working as intended, you should now see that several lines are underlined with a green wave, if you hover over the part that is underlined, a modal box should show you details about the warning

For example, the 2nd level 1 heading will display a warning text like this:

Unexpected duplicate toplevel heading, exected a single heading with rank '1' eslint(remark-lint-no-multiple-toplevel-headings)

The first part is the warning message followed by the name of the extension that added the warning (in this case eslint) and then in parenthesis the name of the rule, in this case it is the rule remark-lint-no-multiple-toplevel-headings that got added by remark-lint

This warning alone shows us that ESLint and remark-lint are both working (together)

You should be able to see 2 more warnings in that file:

Testing the React component(s) linting (in VSCode)

Now we are going to test if VSCode displays error inside of typescript code by creating a Button component with 3 errors in it

Next, inside the folder /tests/eslint that we created previously, add a new folder called components, and inside that folder, create a new file called Button.tsx with the following content:

/tests/eslint/components/Button.tsx
'use client'
 
import { forwardRef } from 'react'
 
type ButtonRefType = HTMLButtonElement
 
interface PropsInterface {
    clickCallback?: () => void
}
 
const TestButton = forwardRef<ButtonRefType, PropsInterface>((props, buttonRef) => {
 
    const { clickCallback, ...rest } = props
 
    const buttonClickHandler = (/*event: React.MouseEvent<HTMLButtonElement>*/) => {
 
        if (typeof clickCallback === 'function') {
            clickCallback()
        }
 
    }
 
    return (
        <>
            <button
                onClick={buttonClickHandler}
                {...rest}
            >
                I'm a test button
            </button>
        </>
    )
})
 
TestButton.displayName = 'TestButton'
 
export default TestButton
 
/* linting errors I should see in this test component
- line 11:  buttonRef is declared but its value is never read (@typescript-eslint/no-unused-vars)
- line 29: the "'" should be escaped (react/no-unescaped-entities)
- line 35: first you need to comment that line out by adding `//` at the beginning and then you should get: Component definition is missing display name (react/display-name)
*/

This test only shows an error if you chose option 2 when adding the overrides to the eslint configuration: The first thing ESLint should have spotted in this test is the buttonRef variable, it should have a wavy red underline because it is an error if you hover over it you will see in the modal that the @typescript-eslint/no-unused-vars rule got triggered, this rule is recommend by the @typescript-eslint plugin, it extends the base rule eslint/no-unused-vars from the eslint plugin and tells you that you have a variable in your code is unused

Then we have another error, this time, it is the react/no-unescaped-entities rule from the eslint-plugin-react (the React ESLint plugin) that tells us to escape entities

Finally line 37, there is the following line: TestButton.displayName = 'TestButton' that you need to comment out to see the actual error, like this //TestButton.displayName = 'TestButton'

If you comment out line 37 (I left it uncommented as the error adds an underline wave to add the code, making it hard to see the other errors), then there will be another error triggered by the react/display-name rule from the eslint-plugin-react, telling you to set a displayName, because we used forwardRef and because by using a displayName debugging will be easier

So, as you can see, the linting of typescript code is working, too, we got an error from the @typescript-eslint and two from the eslint-plugin-react

Testing the lint command

There is only one final test left, which is testing if the linting command works, too

To test the linting command, open the VSCode terminal and then use the following command:

npm run lint

This should display all the warnings and errors we found in the two previous chapters, similar to this (I removed the plugin names in the output below to ensure it fits inside of the code box):

PATH_TO_PROJECT\tests\eslint\components\Button.tsx
  11:20  error  Component definition is missing display name
  11:70  error  'buttonRef' is defined but never used
  31:18  error  `'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`
 
PATH_TO_PROJECT\tests\eslint\content.mdx
   3:1   warning  Unexpected duplicate toplevel heading, exected a single heading with rank `1`
   5:1   warning  Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
   5:1   warning  img elements must have an alt prop, either with meaningful text, or an empty string for decorative images
  10:99  warning  Unexpected missing final newline character, expected line feed (`\n`) at end of file

Excluding our test files from linting

Before you commit, there is one last thing we need to do, which is to exclude our tests folder from linting by adding it to the exclude block in our .eslintrc.js file, like this:

.eslintrc.js
    ignorePatterns: [
        'node_modules/',
        '.next/',
        '.vscode/',
        'public/',
        // by default we always ignore our tests folder
        // to ensure the tests do NOT trigger errors in
        //staging/production deployments
        // comment out the next line to have eslint check
        // the test files (in development)
        'tests/eslint/',
    ],

By adding 'tests/eslint/' to the ignorePatterns, we ensure that the linting process will not lint those files when we do a deployment and hence prevent our build process

When you want to run the tests again (in development), just comment this line out

Disabling rules using comments

Now that the linting setup is done, you will start seeing a warning in your code, and at some point, you might wonder how you can disable/ignore certain warnings within files

Sometimes, you might encounter warnings that you want to suppress because it is an exception to the rule (but you don't want to disable the rule entirely in your configuration), then you can use comments to disable the rule, for example, for an entire file or just the next line

ESLint "disable" comment

Disabling an eslint rule for the next line (works for plugins too, except remark, see below):

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const foo = 'bar'

To disable a rule for an entire file (and NOT just the next line), you can use:

/* eslint-disable no-console */
console.log('foo')
 
console.log('bar')

To disable more than one rule in a single comment, you need to separate the rule names with a comma

For example, if you use an <img> element with no alt attribute, you will get two warnings

To disable them both, you do it like this:

// eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element
<img src="./foo.gif">

remark-lint disable comments (in MDX)

To disable remark-lint rules we added by using MDX plugin you do NOT specify the rule name:

THIS EXAMPLE IS WRONG
{/* eslint-disable-next-line remark-lint-no-undefined-references) */}
> [Info - 8:41:03 PM] ESLint server is running.

If you do, you will get an error like this:

Error: Definition for rule remark-lint-no-undefined-references was not found

Instead, you need to use mdx/remark for ANY rule you want to disable

In MDX files to add an eslint-disable comment, you need to use JSX comments

So if we do both things, we get something like this:

{/* eslint-disable-next-line mdx/remark */}
> [Info - 8:41:03 PM] ESLint server is running.