Configuring Prettier and ESLint

2019-12-20

At my latest job, I've found myself setting up Prettier and ESLint on several different JavaScript projects. I've done it enough times that the process has become somewhat mechanical, so I wanted to document it so that others might be able to use the same process.

In this post I am going to be using ElasticQuill as an example project. You can see the end result in this commit. This process should work for any JavaScript repository with minimal modification, however.

The ElastiQuill project is set up as a monorepo with two components, admin-frontend and backend. At the start the directory structure looks like this:

1
elastiquill
2
├── admin-frontend
3
│  ├── node_modules
4
│  ├── package-lock.json
5
│  ├── package.json
6
│  ├── src
7
│  │  └── (javascript files)
8
│  └── webpack.config.js
9
└── backend
10
   ├── node_modules
11
   ├── package-lock.json
12
   ├── package.json
13
   ├── src
14
   │  └── (javascript files)
15
   └── webpack.config.js

This guide is written against prettier@1.19.1 and eslint@6.7.2.

Return to top

Step 1: Just Use Prettier™

This step comes first because it's the simplest and most mechanical. We simply install prettier and run it on all of the files we want to keep formatted. For the example project, these are everything under src and webpack.config.js, and we run these commands in each of the two project directories (admin-frontend and backend).

1
echo 'trailingComma = "es5"' > .prettierrc.toml
2
cd admin-frontend
3
npm install --save-dev prettier
4
prettier --write 'src/**' webpack.config.js
5
cd ../backend
6
npm install --save-dev prettier
7
prettier --write 'src/**' webpack.config.js
8
cd ..
9
git add .
10
git commit -a -m "Just Use Prettier™"

Notice that the .prettierrc.toml file is in the repository root, so it will be shared between both of the projects. You could install prettier into the repository root if you wanted, but having nested packages in this way can result in some confusing edge cases, so I don't recommend it.

As far as what to put in .prettierrc.toml, I recommend only one non-default setting: trailingComma = "es5" This is because trailing commas lead to cleaner git diff output, and "es5" is supported in all browsers. If you want to go a step further, "all" can also be used, but should only be used if the project is transpiled with babel or something similar. Read more about Prettier options.

Return to top

Step 2: Configure ESLint

Step 2a: Deciding on an ESLint configuration

Unfortunately, ESLint requires much more configuration than Prettier. Since we are using prettier, we can use eslint-prettier to disable all code formatting warnings and have ESLint focus on more important things. I will include the settings that ElastiQuill ended up using and discuss them. ElastiQuill makes use of React, Babel, and Jest.

.eslintrc.base.js
1
module.exports = {
2
   env: { browser: true, node: true, "jest/globals": true, es6: true },
3
   extends: [
4
     "eslint:recommended",
5
     "plugin:react/recommended",
6
     "plugin:prettier/recommended",
7
   ],
8
   plugins: ["jest"],
9
   rules: {
10
     "react/prop-types": "off",
11
     "react/jsx-no-target-blank": "off",
12
     "require-atomic-updates": "off",
13
     "no-unused-vars": ["error", { ignoreRestSiblings: true }],
14
     "no-console": "off",
15
   },
16
   parserOptions: {
17
     ecmaFeatures: {
18
       jsx: true,
19
       modules: true,
20
       legacyDecorators: true,
21
     },
22
   },
23
   parser: "babel-eslint",
24
 };

Let me explain each setting and the motivations for including it:

  • env.browser The admin-frontend project runs in the browser. This setting defines browser globals like window and document.
  • env.node The backend project runs in node. This setting defines node globals like process.
  • env["jest/globals"] The tests run in Jest. This setting defines Jest globals like describe and it.
  • env.es6 The project is written in ES6. This setting defines ES6 globals like Promise.
  • extends "eslint:recommended" This will be the basic defaults we use for ESLint. It provides mostly sane defaults, many of which ESLint can automatically fix.
  • extends "plugin:react/recommended" This plugin adds several useful React-specific style and quality rules.
  • extends "plugin:prettier/recommended" This plugin configures ESLint to defer all style decisions to Prettier.
  • rules["no-unused-vars"] This rule is good, however a common practice in React is to "pull out" props so they don't get passed on to nested components. By setting ignoreRestSiblings, the following unused variable is allowed: const { onSubmit, ...rest}; return <button {...rest} />;
  • parserOptions and parser Everything here is just what is necessary to get ESLint to parse the source code. If ESLint reports syntax errors in valid code, you likely need to check the parserOptions.

The config file for ElastiQuill disables a few of the recommended rules. Here's why:

  • react/prop-types Unfortunately, this project does not use propTypes. It would be nice if it did, but adding them to all components is beyond the scope of simply configuring ESLint.
  • react/jsx-no-target-blank This rule highlights a valid (though minor) security concern that is worth addressing. Rather than spamming rel="noopener" into every target="_blank" link in the project, however, a better solution is to create a custom component for off-site links that open in a new tab. Of course, this is beyond the scope of simply configuring ESLint.
  • require-atomic-updates This rule is fundamentally incompatible with express, so it produces false positives on almost every express middleware you write. This rule also detects only the simplest of race conditions, so I don't bother enabling it at all.
  • no-console Unfortunately, this project has lots of console.error statements in error handlers. It would be nice if it had a better way of handling errors, but fixing all instances of this is beyond the scope of simply configuring ESLint.

I recommend starting with the defaults for all rules and choosing to disable specific rules later on in step 2b.

We save this base configuration as .eslintrc.base.js in the repository root. Then, in each of the project directories we reference it:

admin-frontend/.eslintrc.js
1
const baseConfig = require("../.eslintrc.base");
2
module.exports = { ...baseConfig, ignorePatterns: ["src/lib/**/*"] };
backend/.eslintrc.js
1
const baseConfig = require("../.eslintrc.base");
2
module.exports = { ...baseConfig, ignorePatterns: ["src/views/**/*"] };

We have to ignore some paths in each of the projects. For admin-frontend, there is a lot of vendor code that we don't need to bother linting. For backend, there's a lot of non-react, non-babel code which would require a different eslintrc. This is beyond the scope of this post.

Return to top

Step 2b: Running ESLint

With our configuration decided, it's time to run eslint and get an error list:

1
cd admin-frontend
2
npm install --save-dev \
3
	eslint \
4
	eslint-config-prettier \
5
	eslint-config-react \
6
	eslint-plugin-prettier \
7
	eslint-plugin-jest \
8
	babel-eslint
9
eslint --cache --fix --ext js src

This will give us a big error list. Start by going through and cleaning up anything trivial, like unused variable errors or missing key props. If fixing an error requires more than 15 seconds of reading the code, don't fix it at this stage.

Tip: You can have vim load the eslint errors into your quickfix window with this command, and use :cnext or ]q from vim-unimpaired.

1
:cexpr system('node_modules/.bin/eslint --cache --format unix --ext js src')

I find it's useful to commit early and often at this stage, and make sure to test the application before every commit to make sure that everything is working still.

After fixing all of the trivial errors, start working on the rest. For these, it's useful to time box yourself to no more than a minute or so per lint error. If fixing a specific error would take longer than a minute, then use an ESLint comment directive to suppress the lint error for the specific location. Example:

1
/* eslint-disable no-alert */
2
alert('foo');
3
/* eslint-enable no-alert */

If you end up leaving lots of lint violations (by using these disable directives), it may be useful to add eslint-plugin-eslint-comments to the configuration.

We follow a similar process for the backend directory. At the end of this step, eslint should report no errors for either project.

Return to top

Step 3: Adding to CI

Now that the code base is formatted and lint-free, we want to keep it that way. This is simply a matter of writing some scripts to run prettier and eslint, and telling the CI to run them.

The first script will be prettier-check, which verifies that prettier has been run on all files. Add some scripts to the script section of the package.json files in each project directory:

admin-frontend/package.json
1
{
2
  "scripts": {
3
    "lint": "eslint --cache --ext js src",
4
    "prettier-check": "prettier --check src/** webpack.config.js"
5
  }
6
}

Then add scripts/prettier-check.sh to the repository root:

scripts/prettier-check.sh
1
#!/bin/bash
2
3
 COMBINED_STATUS=0
4
 DIRS="admin-frontend backend"
5
 for dir in $DIRS; do
6
   echo
7
   echo "Checking $dir source"
8
   (cd $dir && npm run prettier-check)
9
   DIR_STATUS=$?
10
   [ $DIR_STATUS -ne 0 ] && COMBINED_STATUS=$DIR_STATUS
11
 done
12
13
  [ $DIR_STATUS -ne 0 ] && exit $DIR_STATUS
14
 exit 0

This script should be straightforward, the only tricky thing is we want to run prettier on each project, but keep the build running and only fail at the end if there were errors. Add scripts/lint.sh as well, which is the same script but has npm run prettier-check replaced by npm run lint.

If the project is hosted on GitLab, then a .gitlab-ci.yml to run these scripts might look like this:

.gitlab-ci.yml
1
stages:
2
 - test
3
 
4
static-analysis:
5
  stage: test
6
  image: node
7
  cache:
8
    key: ${CI_JOB_NAME}
9
    paths:
10
      - admin-frontend/node_modules
11
      - backend/node_modules
12
  script:
13
    # These are in () to run in a subshell and not modify the real pwd
14
    - (cd admin-frontend && npm install)
15
    - (cd backend && npm install)
16
    - npm run prettier-check
17
    - npm run lint

Return to top

Conclusion

And that's it. Altogether, configuring and running eslint and prettier on ElastiQuill took me 2 hours.

When I look back on this project, I can think of a few things that could be improved. Here's are some of the ideas:

  • Since eslint-prettier is enforcing Prettier styles, the prettier-check script may actually be redundant, but I'm not sure that eslint-prettier performs the same checks as prettier --check.
  • It would be nice if the "longer-term" lint errors like react/prop-types and no-console were set to "warn" instead of "error" and accompanied by a tool to produce a report of these (like Go Report Card).