arthur.murauskas:~$
← cd ..
Feb 2021 · 9 min read

Configuring TypeScript Monorepo with ESLint, Prettier and WebStorm

typescriptmonorepotooling

Both the advantage and disadvantage of the NodeJS ecosystem is the fact that it is not opinionated. The advantage is that you can be really flexible in how to use it and the disadvantage is... exactly the same.

At the moment there is no widely used standard of managing a monorepo. You can use Lerna, Yarn workspaces and some custom build tools. I will describe one of them in this article.

We are going to be using several tools:

Directory structure

Let's start by creating a directory and initializing the project:

mkdir monorepo && cd monorepo && yarn init .

Now, this will be a monorepo, so let's prepare the directories for our packages. We will use yarn workspaces to organize our worktree, which is going to look like that:

.
├── package.json
└── packages
    ├── client
    └── server

Once we created the directories, let's modify our main package.json file a little bit to be compatible with yarn workspaces:

package.json with workspaces configuration

We did several modifications here:

Once that is done, we can initialize our packages:

cd packages/server && yarn init

Two things that are worth mentioning are:

Once we do the same thing in the packages/client folder, we can add some code and packages. Add empty index.ts files to our packages/**/src directories.

Empty index.ts files in package directories

Let's open WebStorm and write some simple code to show how we can use packages in the monorepo:

WebStorm showing unresolved import

As you can see, we have at least several problems which we have to solve:

We'll start with the basics and configure TypeScript in a monorepo setup first.

TypeScript configuration

To do that, we will create one main tsconfig.json file in the parent directory and additional tsconfig.json files in each package. Here is the content of the parent tsconfig.json file:

{
  "compilerOptions": {
    "incremental": true,
    "outDir": "./dist",
    "baseUrl": ".",
    "moduleResolution": "Node",
    "module": "CommonJS",
    "target": "ES2018",
    "sourceMap": true,
    "lib": ["esnext"],
    "esModuleInterop": true,
    "alwaysStrict": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "resolveJsonModule": true,
    "paths": {
      "@monorepo/*": ["packages/*/src/index.ts"]
    }
  },
  "references": [
    { "path": "packages/client" },
    { "path": "packages/server" }
  ],
  "exclude": ["node_modules", "dist", "*.d.ts"]
}

At least three options are necessary:

Let's now create a tsconfig.json file for our server package:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "rootDir": ".",
    "outDir": "./dist",
    "baseUrl": ".",
    "sourceMap": true,
    "resolveJsonModule": true,
    "composite": true,
    "allowJs": false,
    "declaration": true,
    "declarationMap": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Two options are significant here:

Our packages/client/tsconfig.json will look mostly similar but with some important differences:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "rootDir": ".",
    "outDir": "./dist",
    "baseUrl": ".",
    "sourceMap": true,
    "resolveJsonModule": true,
    "composite": true,
    "allowJs": false,
    "declaration": true,
    "declarationMap": true,
    "allowSyntheticDefaultImports": true,
    "paths": {
      "@monorepo/server": ["../server/src/index.ts"]
    }
  },
  "references": [
    { "path": "../server" }
  ],
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

We add two additional options:

You may ask why we have to specify these paths again if we already specified them in the parent tsconfig.json file and there's no clear answer to this, unfortunately. The good news is that we are not adding new packages multiple times per day, so it has to be done only once.

Prettier and ESLint configuration

Let's add configuration files for Prettier and ESLint and then try to make it all work in WebStorm:

yarn add prettier eslint-config-prettier eslint eslint-config-standard \
  eslint-plugin-import eslint-plugin-node eslint-plugin-promise \
  typescript @types/node @typescript-eslint/eslint-plugin \
  @typescript-eslint/parser -W

Notice the -W flag at the end — it tells Yarn to install the packages at the workspace root.

Let's initialize .eslintrc.json and .prettierrc.json files. Our ESLint config will look like that:

{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "extends": [
    "eslint:recommended",
    "standard",
    "prettier",
    "plugin:@typescript-eslint/recommended",
    "prettier/@typescript-eslint"
  ],
  "plugins": ["@typescript-eslint"],
  "env": {
    "es6": true,
    "node": true
  },
  "ignorePatterns": ["dist", "node_modules", "examples", "scripts"]
}

Feel free to modify it to your liking. There are just a couple of things to keep in mind:

Our .prettierrc.json can be really basic:

{
  "trailingComma": "es5",
  "printWidth": 120,
  "singleQuote": true
}

Final steps

First of all, install the nanoid dependency in our server package:

yarn workspace @monorepo/server add nanoid

Notice how we add a workspace name (which equals to the name in package.json) to install the dependency only in that package.

Once that is done, let's add some useful scripts to our package.json files. Add the following section to your parent package.json file:

{
  "scripts": {
    "build": "tsc -b",
    "start": "yarn workspace @monorepo/client start"
  }
}

The second one is trivial — we are just launching the start script in our client package. However, the first one is much more important as it tells our TypeScript compiler to find all referenced projects (and their tsconfig files), check if they are outdated, and build them in the correct order.

The final touch, modifying our client's package.json:

{
  "scripts": {
    "start": "node dist/src/index"
  },
  "dependencies": {
    "@monorepo/server": "^1.0.0"
  }
}

Here we are adding a start script used in the parent package.json + adding the server as a dependency.

Voila!

Build result

But not exactly... We still have WebStorm to configure.

WebStorm configuration

Let's try to make it work in WebStorm. At the moment, if we type something nasty in our editor, we are not going to see any reaction from ESLint or Prettier:

WebStorm editor without ESLint or Prettier feedback

Open WebStorm preferences (Cmd + ,), search for the Prettier section and make sure that your Prettier package is selected and the "On save" checkbox is ticked.

Prettier settings in WebStorm

Once that is done, you should edit your file again, save it, and see that Prettier will automatically format it. Let's move on to ESLint. First of all, let's add the following rule to your ESLint config to test that the settings are working properly:

{
  "rules": {
    "@typescript-eslint/naming-convention": [
      "error",
      {
        "selector": "variable",
        "format": ["camelCase", "UPPER_CASE"]
      },
      {
        "selector": "function",
        "format": ["camelCase"]
      }
    ]
  }
}

Now we can write a test function to see if ESLint is functioning properly in WebStorm:

ESLint naming convention test

It doesn't. Let's open WebStorm settings again and go to ESLint preferences, and make sure that they are enabled:

ESLint settings in WebStorm

Make sure that your "ESLint package" setting is explicit and your "Working directories" are set to a glob "packages/*".

It might actually work without the modification of these two settings but I had some bizarre and hard to debug problems on big monorepo projects. These two settings seem to fix them.

As soon as we do that we'll get our ESLint in the editor:

ESLint working in WebStorm editor

There is one more thing which we have to do — configure TypeScript itself and add our run/build configurations.

Click on the TypeScript widget in the footer of WebStorm:

TypeScript widget in WebStorm footer

In the TypeScript settings window, check for the following settings:

TypeScript settings in WebStorm

Last but not least, add the Run configuration. I recommend adding tsc -b before each run, which slows down the launch time a little bit, but it's the most reliable way to make sure that you're running the latest compiled version of the code.

Run configuration in WebStorm

And that's it — we can now launch our project in WebStorm!

Final run result

I hope this article will help you get started with monorepos and TypeScript and save some time in the process.