Nadia Makarevich

Custom eslint rules + typescript monorepo = ❤️

Custom eslint rules + typescript monorepo = ❤️

Do you love writing eslint rules as I do? There is something magical and powerful in writing a tiny piece of software that enforces a vision on how the code should look like and even fixes this vision by itself.

Do you love writing strongly typed code as I do? Code autocompletion, bugs are caught before you even save the file, input and output of functions are obvious from their types… Perrrfect! After a while, writing pure javascript feels like descending into the dark ages.

And here is a bummer: it’s actually quite hard to make those two get along! Eslint doesn’t know about typescript existence and doesn’t support rules written in any other language other than javascript. To solve this, people usually resort to one of those solutions:

  • they either write eslint rules with just javascript and don’t get the benefits of types at all (like for example next.js repo does)
  • or they compile eslint rules to javascript before using them (like for example typescript-eslint repo does)
  • or even extract eslint rule into their own repository, package it via npm and then consume in the main repo as an external eslint plugin (any other repo that consumes any external eslint plugins)

All of the solutions will work, but my little inner perfectionist gets an eye twitch when it sees raw javascript code in the otherwise perfectly typed repository, or cries “dev experience 😭” when it sees other steps between the writing of the code and its consumption.

Hence this little magic trick that solves this problem and allows you to get the benefits of both worlds and make the dev experience shine. It assumes monorepo setup (yarn workspaces, lerna, pnpm, etc), but the general approach can be used outside of monorepos as well.

Jump straight to the example implementation if you’d rather read code than words: https://github.com/adevnadia/custom-eslint-in-typescript

Step 1: eslint plugin package

Create a new package esint-plugin-example for the future eslint plugin, with the following structure (or any structure that you prefer to use for your packages and rules of course):

package.json should have the following fields:

where name should follow eslint naming conventions and main should point to index.js file (the only js file we’d ever need in this setup).

index.ts — typescript entry point to the plugin, exports the rules

import myFirstRule from './rules/my-first-rule';
const rules = {
'my-first-rule': myFirstRule
};
export default rules;

rules/my-first-rule.ts — the actual rule implemented for the plugin. In typescript 😍! Make sure to install @types/eslint.

import { Rule } from 'eslint';
const rule: Rule.RuleModule = {
create: (context: Rule.RuleContext) => {
return {
// rule code
}
},
};
export default rule;
  • index.js — the “bridge” file that makes the whole setup work (see step 3)

Step two: eslint config

Run yarn install if you’re on workspaces (or any other linking step that your repo uses), now the package is linked and ready to be consumed.

Add it to the list of eslint plugins and to the list of enabled rules in eslint config file

module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['example'],
rules: {
'example/my-first-rule': 'error',
}
};

Step three: the bridge between javascript and typescript

Final, and most important step — teach Node.jsto use typescript when running eslint. To do that, add this to our index.js file in the eslint-plugin-example package:

// This registers Typescript compiler instance onto node.js.
// Now it is possible to just require typescript files without any compilation steps in the environment run by node
require('ts-node').register();
// import our rules from the typescript file
const rules = require('./index.ts').default;
// re-export our rules so that eslint run by node can understand them
module.exports = {
rules: rules
};

That’s it! Now when you do yarn run eslint in your repo, the beautiful strongly typed rule will be run directly, using the typescript compiler.

Try it out: https://github.com/adevnadia/custom-eslint-in-typescript

Little caveat

Of course, all the good things have their cost. In this case, the cost is “publishing” — if you’re publishing packages from the repo, you probably raised an eyebrow on the solution above. Unfortunately, since main field in package.json now points to the bridge index.js file, not the compiled code in dist folder, the published package will point to this code as well. There are ways to deal with it: replace main field in the CI before publishing, make use of environment variables in the index.jsfile or just exclude eslint plugin from publishing at all. The solution depends entirely on what is acceptable in your repo and how much you want to modify the publishing process.

That’s it, hope this little trick saved you some time and made your inner perfectionist as happy as mine 😊