Configuring Prettier and ESLint
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:
elastiquill
├── admin-frontend
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ └── (javascript files)
│ └── webpack.config.js
└── backend
├── node_modules
├── package-lock.json
├── package.json
├── src
│ └── (javascript files)
└── webpack.config.js
This guide is written against prettier@1.19.1
and eslint@6.7.2
.
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
).
echo 'trailingComma = "es5"' > .prettierrc.toml
cd admin-frontend
npm install --save-dev prettier
prettier --write 'src/**' webpack.config.js
cd ../backend
npm install --save-dev prettier
prettier --write 'src/**' webpack.config.js
cd ..
git add .
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.
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.
module.exports = {
env: { browser: true, node: true, "jest/globals": true, es6: true },
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:prettier/recommended",
],
plugins: ["jest"],
rules: {
"react/prop-types": "off",
"react/jsx-no-target-blank": "off",
"require-atomic-updates": "off",
"no-unused-vars": ["error", { ignoreRestSiblings: true }],
"no-console": "off",
},
parserOptions: {
ecmaFeatures: {
jsx: true,
modules: true,
legacyDecorators: true,
},
},
parser: "babel-eslint",
};
Let me explain each setting and the motivations for including it:
env.browser
Theadmin-frontend
project runs in the browser. This setting defines browser globals likewindow
anddocument
.env.node
Thebackend
project runs in node. This setting defines node globals likeprocess
.env["jest/globals"]
The tests run in Jest. This setting defines Jest globals likedescribe
andit
.env.es6
The project is written in ES6. This setting defines ES6 globals likePromise
.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 settingignoreRestSiblings
, the following unused variable is allowed:const { onSubmit, ...rest}; return <button {...rest} />
;parserOptions
andparser
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 theparserOptions
.
The config file for ElastiQuill disables a few of the recommended rules. Here's why:
react/prop-types
Unfortunately, this project does not usepropTypes
. 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 spammingrel="noopener"
into everytarget="_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 ofconsole.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:
const baseConfig = require("../.eslintrc.base");
module.exports = { ...baseConfig, ignorePatterns: ["src/lib/**/*"] };
const baseConfig = require("../.eslintrc.base");
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.
Step 2b: Running ESLint
With our configuration decided, it's time to run eslint and get an error list:
cd admin-frontend
npm install --save-dev \
eslint \
eslint-config-prettier \
eslint-config-react \
eslint-plugin-prettier \
eslint-plugin-jest \
babel-eslint
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.
: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:
/* eslint-disable no-alert */
alert('foo');
/* 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.
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:
{
"scripts": {
"lint": "eslint --cache --ext js src",
"prettier-check": "prettier --check src/** webpack.config.js"
}
}
Then add scripts/prettier-check.sh
to the repository root:
#!/bin/bash
COMBINED_STATUS=0
DIRS="admin-frontend backend"
for dir in $DIRS; do
echo
echo "Checking $dir source"
(cd $dir && npm run prettier-check)
DIR_STATUS=$?
[ $DIR_STATUS -ne 0 ] && COMBINED_STATUS=$DIR_STATUS
done
[ $DIR_STATUS -ne 0 ] && exit $DIR_STATUS
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:
stages:
- test
static-analysis:
stage: test
image: node
cache:
key: ${CI_JOB_NAME}
paths:
- admin-frontend/node_modules
- backend/node_modules
script:
# These are in () to run in a subshell and not modify the real pwd
- (cd admin-frontend && npm install)
- (cd backend && npm install)
- npm run prettier-check
- npm run lint
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, theprettier-check
script may actually be redundant, but I'm not sure thateslint-prettier
performs the same checks asprettier --check
. - It would be nice if the "longer-term" lint errors like
react/prop-types
andno-console
were set to"warn"
instead of"error"
and accompanied by a tool to produce a report of these (like Go Report Card).