Nadia Makarevich
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.js
to 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 noderequire('ts-node').register();// import our rules from the typescript fileconst rules = require('./index.ts').default;// re-export our rules so that eslint run by node can understand themmodule.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.js
file 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 😊