Updated on: Sat Mar 20 2021
Hey yo! Here are some notes on how to add code linting via eslint and auto formatting via prettier to your monorepo. These notes I took trying to achieve those results implementing it here on yaraoncode.me. Few words here. yaraoncode.me is basically a monorepo consisting of two packages:
nextjs
project running frontendsanity.io
headless cms frontend powering web by contentYou may ask why I did it as monorepo and this is amazing question. I don't know exactly. web and studio packages do not share functionality between each other. But at the same moment I constantly make changes to both of them and it's easy to do single deploy to vercel
via one push to main
branch.
So. First of all today I knew more or less nothing on how to add linting and prettyfing to codebase from scratch. I just used already something which was done before me by someone more smart on my daily job. And I will not write in depth manual here. Just a reference for myself to use it when need in the future. Go.
Previously I was confused how eslint and prettier are connected to each other and what does it mean "make prettier to work with eslint". So, as far as I get prettier is tool to format your code according to some rules written in .prettierrc and eslint is extremely powerful thing to check you code according to some rulesets. "Make them work together" - is to make them work separately and then connect in a way to avoid conflicts between them.
Everything starts from eslint first. As mentioned in the title this is typescript based monorepo, so we will hassle with all the typescript magic which you can imagine. It was a hard day, believe me. I started to do it wrong and was stucked at some point with things were just not working and finally get to the state which I'm more or less happy with.
I started adding eslint/prettier to the root and then thought it is wrong. Spoiler: it is correct. I switched adding those things to each package separately. A lot of pain, tears and blood - eslint finally started working for me but prettier not. Why I switched from "add to root" approach is because yarn/lerna started to tell me that installing eslint/prettier to monorepo root is probably not what I want. Literally. Those tools told me so. Who am I to argue with software. I thought I'm wrong. So I moved in a direction to make those tools happy but the only thing I achieved - I made sad myself.
Have those tools installed in the root appeared to be correct way to go. So let me finally describe what the steps need to be taken.
Go to project's root and install needed packages:
copied bash
yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y eslint-import-resolver-custom-alias
Couple of words here. @typescript-eslint/eslint-plugin and @typescript-esling/paser are two guys who do main job here. Others are more or less just rules. I decided to stick with config from airbnb guys because they deserved a lot of respect in a community and why not. Basically eslint-config-airbnb should already include it's peer dependencies eslint-plugin-react and eslint-plugin-import but for some reason it is not and I had to install them manually.
After that we are ready to touch .eslintrc.js in the root of our monorepo:
copied javascript
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'airbnb'
]
};
Do not forget to make .eslintignore to not waste your cpu:
copied bash
node_modules
.next
dist
And we even can try to run eslint but nothing will happen or may be something will but I already do not remember what. But let's try it first. Add this to scripts section of package.json root.
copied
"lint:web": "eslint packages/web --ext .tsx,.ts --quiet",
"lint:web:fix": "eslint packages/web --ext .tsx,.ts --quiet --fix",
"lint": "eslint packages/web --ext .tsx,.ts"
lint:web will be our main yarn command here. --quiet option needed to calm down all warnings because it's to many of them. Too many.
So after that we'll face with a lot of errors. Let's resolve them.
We may face with this error right inside of our .eslintrc.js:
copied
Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: .eslintrc.js.
The file must be included in at least one of the projects provided.
To handle this error we have to add this line to compilerOptions of .eslintrc.js:
copied javascript
ignorePatterns: ['.eslintrc.js']
JSX not allowed in files with extension '.tsx' - airbnb doesn't add tsx support by default so we have to add this information to .eslintrc.js rules section:
copied javascript
'react/jsx-filename-extension': [1, { extensions: ['.tsx'] }]
Next error - children is missing in props. Add this to rules section:
copied javascript
'react/prop-types': [2, { ignore: ['children'] }]
If we use nextjs (like we are at the moment) we'll definitely meet error pointing that href is required in a tag. To fix this we need to add this to rules
copied javascript
'jsx-a11y/anchor-is-valid': [
2,
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
We will definitely face problem with global window object. First of all we have to specify some stuff in tsconfig for typescript to understand which env compile javascript will run in:
copied javascript
{
env: {
browser: true,
node: true,
}
}
I like to avoid relative paths like ../../../../path/to/module. To achieve this in context of nextjs application we'll do this to compilerOptions in tsconfig.json (this needs to exist in tsconfig.json of a specific package):
copied javascript
"baseUrl": ".",
"paths": {
"@/": ["src/"]
},
Let's force this to work fine. The trickiest part here is that we'll have root .eslintrc.js and we'll override it in specific packages. Main thing to mention here is parserOptions section. It should properly mention root directory and should list all tsconfig.json files in working tree.
copied javascript
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.root.json', './packages/*/tsconfig.json'],
ecmaFeatures: {
jsx: true,
}
}
Also at the same time we have to add new setting section to .eslintrc.js:
copied javascript
settings: {
'import/resolver': {
'eslint-import-resolver-custom-alias': {
alias: {
'@': './src',
},
extensions: ['.ts', '.tsx'],
packages: ['packages/*'],
}
}
}
What to pay attention here is packages: ['packages/*'] - this is what does the trick. And purpose of the trick is to connect root .eslintrc.js with tsconfig from some specific package.
Last step we have to install and configure prettier. To do this let's install needed packages first:
copied bash
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
To make prettier work we need some configuration in .prettierrc
copied json
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80
}
And also we need to connect it with eslint via plugin and config:
copied javascript
extends: [
...,
'prettier',
],
plugins: [
...,
'prettier'
],
rules: {
...,
'prettier/prettier': 2
}
This additions make possible prettier configuration to work with eslint and do not conflict with it's rules.
Final .eslintrc.js
copied javascript
module.exports = {
env: {
browser: true,
node: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.root.json', './packages/*/tsconfig.json'],
ecmaFeatures: {
jsx: true,
},
},
ignorePatterns: ['.eslintrc.js'],
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'airbnb',
'plugin:import/typescript',
'prettier',
],
settings: {
'import/resolver': {
'eslint-import-resolver-custom-alias': {
alias: {
'@': './src',
},
extensions: ['.ts', '.tsx'],
packages: ['packages/*'],
},
node: {
extensions: ['.ts', '.tsx'],
},
},
},
rules: {
'import/extensions': 'off',
'react/react-in-jsx-scope': 'off',
'react/jsx-filename-extension': [2, { extensions: ['.tsx'] }],
'react/jsx-props-no-spreading': 'off',
'react/prop-types': [2, { ignore: ['children'] }],
'no-underscore-dangle': [2, { allow: ['_updatedAt', '_id'] }],
'jsx-a11y/anchor-is-valid': [
2,
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
'consistent-return': 'off',
'react/jsx-one-expression-per-line': 'off',
'prettier/prettier': 2,
},
};
Final tsconfig.root.json and ./packages/web/tsconfig.json:
copied json
// tsconfig.root.json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"noEmit": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "preserve",
"baseUrl": ".",
"types": [
"node"
],
"isolatedModules": true
},
"exclude": [
"node_modules"
]
}
// ./packages/web/tsconfig.json
{
"extends": "../../tsconfig.root.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
"types": [
"node"
],
"isolatedModules": true
},
"include": [
"next-env.d.ts",
"./**/*.ts",
"./**/*.tsx"
]
}
Useful links: