TypeScript Monorepo with Yarn and Project References

TypeScript Monorepo with Yarn and Project References

Please find an updated and extended version of this post here: The Ultimate Guide to TypeScript Monorepos.


Project references in TypeScript are an amazing feature for building complex TypeScript projects. They enable dividing a large project into smaller, indepedent modules and thereby improving code organisation as well as compile times; since it is easier for TypeScript to identify which parts of the project need to be recompiled when there are changes.

Yarn workspaces in Yarn's new version, often referred to as Yarn Berry or Yarn 2, provide a great way to manage JavaScript projects that consist of more than one package.

Project references and Yarn workspaces in combination enable developing TypeScript projects as scale while keeping code modular and manageable. Unfortunately it is a bit tricky to get everything working for a basic project - it took me a good few hours, even when with the help of a few helpful blog posts (e.g. TypeScript Monorepos with Yarn, An actual complete guide to typescript monorepos).

Thus I put together a quick starter project along with some comments in this blog posts. Find the starter project here:

goldstack / typescript-monorepo-yarn-project-references

Compiling TypeScript

  • Install Yarn with npm install yarn -g
  • Clone the above repo
  • Run yarn
  • Run yarn compile

This will run the following script:

{
 "compile": "yarn node scripts/updateReferences.js && tsc --build"
}

The script updateReferences.js will use the package @monorepo-utils/workspaces-to-typescript-project-references to ensure that all the project references are configured correctly. Specifically this will ensure that when there is a dependency between two packages that the parent package will list the child package in its references in the tsconfig.

For instance, in the sample project we have a package cli-app that imports another package consts (see cli-app package.json). Running yarn tsref will ensure that the correct references are set both in package.json and tsconfig.json.

{
  /* ... */
  "references": [
    {
      "path": "../consts"
    }
  ]
}

updateReferences.js will also ensure that the root tsconfig.json will list all packages in the repository in its "references".

After the project references have been updated, simply running tsc --build in the project root will compile the TypeScript for all modules. TypeScript will do so in a smart way and keep tsconfig.tsbuildinfo files that will help it to speed up subsequent compilations.

tsc --build will validate there are no type errors and also emit commonjs JavaScript output into the respective dist folders for the modules. In this project, it will write into the following dirtectories:

packages/cli-app/dist
packages/consts/dist

Emitted files from tsc --build

TypeScript Configuration

There are a few things to keep in mind when configuring tsconfig.json files within the monorepo. This repository has a tsconfig.base.module.json file that is references by all packages.

{
 "compilerOptions": {
    "composite": true,
    "noEmit": false, /* referenced projects may not disabled emit */
    "rootDir": "./",
    "isolatedModules": true,
    "skipDefaultLibCheck": true,
    "skipLibCheck": true,
    "declaration": true,
 }
}

Notable here is that we set the flag "composite": true. This indicates to the TypeScript compiler that these packages will be part of a project composed of multiple packages.

For each module, we include a tsconfig.json file that inherits from this base configuration. See here the tsconfig.json for the consts module:

{
  "extends": "./../../tsconfig.base.module.json",
  "references": [],
  "include": [
    "src/**/*"
  ],
  "compilerOptions": {
    "outDir": "./dist"
  }
}

Note that we specify the outDir explicitly. This is required so that TypeScript will not compile the code for all packages into the root directory of the monorepo.

Since the cli-app package imports the consts package we also need to set the rootDir in the package-specific configuration, since otherwise output in the dist/ folder will be nested according to the project root folder. See here the tsconfig.json for the cli-app package:

{
  "extends": "./../../tsconfig.base.module.json",
  "compilerOptions": {
    "outDir": "dist", /* Specify an output folder for all emitted files. */
    "rootDir": "."
  },
  "references": [
    {
      "path": "../consts"
    }
  ]
}

Bundling

Since using a monorepo will result in a number of independely compiles commonjs module, we will need to bundle a package along with all it's dependencies if we want to deploy an application. This repository contains a simple example of bundling a command line application using esbuild.

The application can be built as follows:

cd packages/cli-app
yarn build

This will result in a file cli.js to be emitted into the packages/cli-app/dist folder:

Output from esbuild

This file contains the code of this module along with the code of all dependencies, so we can run it as a simple node application:

Output of running the cli application

Since we are using Yarn 2 which dependes on Plug and Play we need to provide a custom configuration for esbuild. Find that configuration here: build.js.

I am currently experimenting with ways to do TypeScript compliation in monorepos since that is one aspect I find not working quite as well as I would like in Goldstack projects:

Goldstack roadmap

I am quite encouraged by this initial setup described in this post and hope that when a few further tests are successful to also use project references by default for all Goldstack templates. If I get to that, will definitely publish another post here.