Mono-repository with typescript and yarn workspaces

Monoliths have their place in many systems. Usually, they stem from the ease of use, allowing cross-cutting changes to be completed in a single pull request. They also have a lot of downsides, but that’s a whole other can of worms.

Here are the main points we had trouble with in our monolith:

  1. How do you share code between sub-projects?
  2. How do you manage typescript compilation for dependant projects? E.g. if a depends on b and I make a change to a, I want b to be compiled as well with the latest version of a.

This was an example of my repository structure:

projectA/
projectB/
projectC/
packages/

Inside my packages/ directory, I had some core shared libraries, like types / shared utils, etc.

Ok — so how do you get it all to work together?

Since we were already using yarn, it seemed like yarn workspaces would be an ideal fit. If I can get the same thing done with the same tools that's a big win.

  • Add a root tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true,
  },
  "exclude": [
    "**/node_modules",
    "**/.*/",
    "**/jest.config.js",
    "**/.build"
  ],
}
  • In your root package.json add your ‘packages’
{
  ...
  "private": true,
  "workspaces": {
    "packages": [
      "packages/*",
      "projectA",
      "projectB",
      "projectC",
    ]
  },
}
  • in each of your projects + packages, you need to give them the correct name in their respective package.json files e.g. in projectA/package.json
{
  "name": "@company/projectA",
  ...
  "scripts": {
    "watch": "tsc -b -w --preserveWatchOutput",
    ...
  },
  "dependancies": {
    "@company/types",
    "@company/logging",
    "@company/clients"
  }
}
  • Now run yarn install at the root, and then at each package.

  • Now is a very important part. You need to add a tsconfig.json file for each of the projects. This is to assist typescript with the dependency tree. Below is an example of projectA as it depends on 3 packages — clients, types, and logging. This tells typescript to recompile if it senses any changes in the parent package.

{
  "extends": "../tsconfig",
  "compilerOptions": {
    "outDir": ".build"
  },
  "rootDir": ".",
  "references": [
    {
      "path": "../packages/clients"
    },
    {
      "path": "../packages/types"
    },
    {
      "path": "../packages/logging"
    },
  ],
}
  • Now, along with the correct tsc options provided (primarily -b) any changes to projectA will recompile the dependencies if required and also compile the project itself.

Now while running in the context of projectA you will be able to make changes to the packages it depends on e.g. /packages/types and it will recompile and display errors if there are any, along with being able to change things in projectA as normal.

21