Recently, the eslint-config-prettier v8 upgrade broke my ESLint configuration, and I realized I needed a centralized way of managing my ESLint configuration across projects.
This is the outline for how I will solve common configuration across projects going forward. Here are the key features:
- Layer your ESLint rules based on topics: ESLint + Prettier, then TypeScript, then React/Vue.
- Use Lerna to publish scoped packages to npmjs.
- Some helper tools to upgrade your code.
Disclaimer: This is not my original work, but leveraged from other’s work, most notably:
- The ESLint configuration started with ntnyq configs
- The TypeScript idea came from unlikelystudio settings
Layered ESLint
The benefit of this organizational structure is layering your ESLint rules. Some rules apply for TypeScript projects. Some for TypeScript/React projects. What if you add Prettier to the mix?
A picture is worth a 1000 words:
Each rule layers parent rules into it’s rules. For example, our company configuration contains:
eslint-config-prettier-typescript-react
:
extends: [
'@pinnsg/typescript-react',
'@pinnsg/prettier-react',
'@pinnsg/prettier-typescript',
...
which in turn eslint-config-prettier-typescript
:
extends: [
'@pinnsg/typescript',
'@pinnsg/prettier',
...
etc.
Naturally, when you publish your configs, they will reference your scope, not mine .
To package.json or not to package.json
A colleague recently embarked on a move to migrate all their dot-config files into package.json
. Initially I was resistant to this, as I felt like it was adding lots of bloat to package.json
. However, now that I’ve worked with this for a bit, I’m starting to see the advantage of a single configuration file (as much as possible).
However, the level of support for package.json
config depends upon the base library being used. You will see these scenarios:
- Full
package.json
support, including referencing your shared configuration via the package name. -
package.json
support without support for package names - Generic json/js/rc file support
Every package is different, so you will have to check the documentation individually to determine the level of support. If the package uses cosmiconfig, you are golden (#1 above). Other configuration packages are not as complete, and offer less flexibility.
Some config libraries, such as rc-config-loader (used by npm-check-updates) support configuration in package.json
, but not referencing shared configuration. If this is the case, you must use an external .js
configuration file and import your configuration. Here’s how the npm-check-updates
config must use .ncurc.js
such as:
module.exports = require("@pinnsg/config-npm-check-updates");
Instructions
- Clone the template repo
- Update the individual rules per your standards
- Choose your NPM scope.
- If you use your npm username, you can it as your scope (my personal configs are published to
@drmikecrowe
for my use) - If you want a different scope, you must login to npmjs.com and add an organization to your account. For instance, our
@pinnsg
scope is our organizational scope
- If you use your npm username, you can it as your scope (my personal configs are published to
- Globally search/replace all occurrences of my scope (
@drmikecrowe
) and replace with your scope - Globally search/replace all occurrences of my username (
drmikecrowe
) and replace with your username (these are 2 steps in case your scope/username are different) - Rename
packages/mrm-preset-drmikecrowe
to match your scope (without the@
) - Login to NPM using
npm login
- Publish your packages using
lerna publish
NOTE: As you do your initial publish, or after updating, sometimes things go bump in the night during your publish and packages are tagged/pushed but not published. To re-publish you can simply run:
lerna exec -- "npm publish || exit 0"
to publish all packages (you’re receive errors for the packages that succeeded)
MRM
mrm is a fantastic tool for updating projects. Their tagline is:
Codemods for your project config files
I’m a huge fan of this project. It allows me to script intelligent updates to my configs based on set criteria. For example, in the configs
preset (explained below), it does:
const parts = []
if (hasPrettier) parts.push('prettier')
if (hasTypescript) parts.push('typescript')
if (hasVue) parts.push('vue')
else if (hasReact) parts.push('react')
const base = parts.length ? '-' + parts.join('-') : ''
const full = `${configScope}/eslint-config${base}`
const eslintPreset = `${configScope}/${base.slice(1)}`
So, by package.json
inspection, it determines which preset you most likely want, and updates the config to match that preset. Very cool.
MRM Presets
A Preset is a way for you to customize MRM behavior. Included is a custom preset for your own use where you can put your upgrades as you use MRM. In this preset I have 2 tasks:
- configs: Migrate configs of a project to this structure
- typescript: Migrate your
tsconfig.json
to this structure (yes, this should probably be part of theconfigs
preset, but it was easier to simply tweak the existing MRMtypescript
task)
To use these, follow these steps:
- Publish your preset per instructions above
- Install your preset globally with
npm i -g mrm mrm-preset-YOURSCOPE
(without the @ — i.e.mrm-preset-drmikecrowe
) - Change into a project you want to upgrade
- Make sure you have committed all your changes and your git tree is clean
- Run
mrm eslint
- If you use Prettier, run
mrm prettier
- (these two command setup eslint/prettier in a standard way — the next step really needs
.eslintrc.json
instead of a .js file)
- (these two command setup eslint/prettier in a standard way — the next step really needs
- Run
mrm --preset YOURSCOPE config
Once that finishes, you can evaluate the proposed changes and see if you like the results. If they are satisfactory, commit them and enjoy the new config. If they are not, do a
git reset --hard HEAD
and update your preset at
packages/mrm-preset-drmikecrowe/configs/index.js
as needed to modify the configs as you see fit.
Enjoy!
Top comments (1)
Thanks for the excellent post. I tried to follow this pattern with a base config, a typescript config, and a react config. Here is my problem: in my TS config I override a rule from my base config; if I then try create a TS-react config that extends both (in that order), then the overridden rule in in the TS config is wiped out by the React config. I assume that since they both extend the base config, the base config in the React config overrides that in the TS config. Apologies if that is not clear. I don't know if it's just the way I implemented it, or if it is a weakness in the pattern.