TypeScript Monorepos with Yarn
Tags: typescript, yarn, javascript, cicd
Published: 2021-12-23 13:35:14

In a past article in this [monorepo](https://semaphoreci.com/blog/what-is-monorepo) series, we’ve discussed setting up CI/CD for JavaScript packages using [Yarn Workspaces](https://semaphoreci.com/blog/javascript-monorepos-lerna). This time, we will figure out the same for TypeScript. We’ll learn how to build and test TypeScript projects at scale with Yarn and Semaphore.

At the end of the tutorial, we’re going to have a [continuous integration](https://semaphoreci.com/continuous-integration) pipeline that builds only the code that changes.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2b0ki32gj12tu2brqaac.png)

## Uniting Yarn and TypeScript

TypeScript extends JavaScript by adding everything it was missing: types, stricter checks, and a deeper IDE integration. TypeScript code is easier to read and debug, helping us write more robust code.

Compared to JavaScript, however, TypeScript saddles us with one more layer of complexity: code must be compiled first before it can be executed or used as a dependency. For instance, say we have two packages, “child” and “parent”. The child is easy to compile since it has no other dependencies:

``` bash
$ npm install -g typescript
$ cd child
$ tsc
```

Yet, when we try to do the same with the parent that depends on it, we get an error since the local dependency is not found.

``` bash
$ cd parent
$ tsc

src/index.ts:1:20 - error TS2307: Cannot find module 'child' or its corresponding type declarations.

1 import { moduleName } from 'child';

Found 1 error.
```

Without specialized tooling, we have to build and link packages by hand while preserving the correct build order. [Yarn Workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) already solves problems like these in JavaScript. Fortunately, with a bit of tweaking, we can extend it to TypeScript.

## Setting up Workspaces in Yarn

Fork and clone the following GitHub repository, which has a couple of packages to experiment with.

{% github TomFern/semaphore-demo-monorepo-typescript %}

We’re going to build a TypeScript monorepo made of two small packages:

-   **shared**: contains a few utility functions.
-   **sayhi**: the main package provides a “hello, world” program.

Let’s get going. To configure workspaces, switch to the latest Yarn version:

``` bash
$ yarn set version berry
```

Yarn installs on `.yarn/releases` and can be safely checked in the repo.

Then, initialize workspaces. This creates the `packages` folder, a `.gitignore`, and the `package.json` and `yarn.lock`.

``` bash
$ yarn init -w
```

You can add root-level dependencies to build all projects at once with:

``` bash
$ yarn add -D typescript
```

Optionally, you may want to install the [TypeScript plugin](https://yarnpkg.com/package/typescript), which handles types for you. The [foreach plugin](https://yarnpkg.com/cli/workspaces/foreach) is also convenient for running commands in many packages at the same time.

Next, move the code into `packages`.

``` bash
$ git mv sayhi shared packages/
```

To confirm that workspaces have been detected, run:

``` bash
$ yarn workspaces list --json

{"location":".","name":"semaphore-demo-monorepo-typescript"}
{"location":"packages/sayhi","name":"sayhi"}
{"location":"packages/shared","name":"shared"}
```

If this were a JavaScript monorepo, we would be finished. The following section introduces TypeScript builds into the mix.

## TypeScript Workspaces

Our demo packages already come with a working `tsconfig.json`, albeit a straightforward one. Yet, we haven’t done anything to link them up — thus far, they have been completely isolated and don’t reference each other.

We can link TypeScript packages using [project references](https://www.typescriptlang.org/docs/handbook/project-references.html#what-is-a-project-reference). This feature, which was introduced on TypeScript 3.0, allows us to break an application into small pieces and build them piecemeal.

First, we need a root-level `tsconfig.json` with the following contents:

``` bash
{
  "exclude": [
    "packages/**/tests/**",
    "packages/**/dist/**"
  ],
  "references": [
    {
      "path": "./packages/shared"
    },
    {
      "path": "./packages/sayhi"
    }
  ]
}
```

As you can see, we have one `path` item per package in the repo. The paths must point to folders containing package-specific `tsconfig.json`.

The referenced packages also need to have the [composite](https://www.typescriptlang.org/tsconfig#composite) option enabled. Add this line into `packages/shared/tsconfig.json` *and* `packages/sayhi/tsconfig.json`.

``` bash
{
  "compilerOptions": {
     "composite": true

     . . .

  }
}
```

Packages that depend on other ones within the monorepo will need an extra reference. Add a `references` instruction in `packages/sayhi/tsconfig.json` (the parent package). The lines go at the top level of the file, outside `compilerOptions`.

``` bash
{
  "references": [
    {
      "path": "../shared"
    }
  ]

  . . .

}
```

Install and build the combined dependencies with `yarn install`. Since we’re using the latest release of Yarn, it will generate a [zero install](https://yarnpkg.com/getting-started/recipes/#typescript--pnp-quick-start) file that can be checked into the repository.

Now that the configuration is ready, we need to run `tsc` to build everything for the first time.

``` bash
$ yarn tsc --build --force
```

You also can build each project separately with:

``` bash
$ yarn workspace shared build
$ yarn workspace sayhi build
```

And you can try running the main program.

``` bash
$ yarn workspace sayhi node dist/src/sayhi.js
Hi, World
```

At the end of this section, the monorepo structure should look like this:

``` bash
├── package.json
├── packages
│   ├── sayhi
│   │   ├── dist/
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── shared
│       ├── dist/
│       ├── src/
│       ├── package.json
│       └── tsconfig.json
├── tsconfig.json
└── yarn.lock
```

That’s it, Yarn and TypeScript work together. Commit everything into the repository, so we’re ready to begin the next phase: automating testing with CI/CD.

``` bash
$ git add -A
$ git commit -m "Set up TS and Yarn"
$ git push origin master
```

## Building and testing with Semaphore

The demo includes a ready-to-work, change-based pipeline in the `final` branch. But we’ll learn faster by creating it from zero.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/00x9tp0exazm8paaoh5a.png)

If you’ve never used Semaphore before, check out the [getting started guide](https://docs.semaphoreci.com/guided-tour/getting-started/). Once you have added the forked demo repository into Semaphore, come back, and we’ll finish the setup.

We’ll start from scratch and use the starter single job template. Select “Single Job” and click on <u>Customize</u>.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g26jreee9dlqf7xlbnkb.png)

The Workflow Builder opens to let you configure the pipeline.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uu12sfx8bqbjvp95vqk9.png)

## Build Stage

We’ll set up a TypeScript [build stage](https://semaphoreci.com/blog/build-stage). The build stage compiles the code into JavaScript and runs tests such as linting and unit testing.

The first block will build the `shared` package. Add the following commands to the job.

``` bash
sem-version node 14.17.3
checkout
yarn workspace shared build
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sqzaghc1pc1lbxdn3vhf.png)

The details are covered in-depth in the [starter guide](https://docs.semaphoreci.com/guided-tour/getting-started/). But in a few words, [sem-version](https://docs.semaphoreci.com/ci-cd-environment/sem-version-managing-language-versions-on-linux/) switches the active version of Node (so we have version consistency), while [checkout](https://docs.semaphoreci.com/reference/toolbox-reference/#checkout) clones the repository into the CI machine.

Scroll down the right pane until you find **Skip/Run conditions**. Select “Run this block when conditions are met”. In the **When?** field type:

``` text
change_in('/packages/shared/')
```

The [change_in](https://docs.semaphoreci.com/reference/conditions-reference/#change_in) function is an integral part of [monorepo workflows](https://docs.semaphoreci.com/essentials/building-monorepo-projects/). It scans the Git history to find which files have recently changed. In this case, we’re essentially asking Semaphore to skip the block if no files in the `/packages/shared` folders have changed.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1wgs7fb30bho1mg3fd8.png)

Create a new block for testing. We’ll use it to run [ESLint](https://eslint.org/docs/user-guide/getting-started) and unit tests with [Jest](https://jestjs.io/).

In the prologue, type:

``` text
sem-version node 14.17.3
checkout
```

Create two jobs in the block:

-   Lint with the command: `yarn workspace shared lint`
-   Unit testing: `yarn workspace shared test`

![](./public/typescript-monorepos-with-yarn/b2.png)

Again, set the **Skip/Run conditions** and put the same condition as before.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tht1nj9blqas2js2m6hb.png)

## Managing dependencies

We’ll repeat the steps for the `sayhi` package. Here, we only need to replace any instance of `yarn workspace shared <command>` with `yarn workspace sayhi <command>`.

Now, create a building block and uncheck the **Dependencies** section. Removing block dependencies in the pipeline makes blocks run in parallel.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l5kt05j99gmdxujfvmyd.png)

Next, set the **Skip/Run Condition** on the new block to: `change_in('/packages/sayhi/')`.

To finish, add a test block with a lint job and a unit test job. Since this package depends on `shared`, we can add a block-level dependency at this point. When done, you should have a total of four blocks.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xjxja1et8ol89cstfcqx.png)

The **Skip/Run Condition**, in this case, is different because the test block should run if either `sayhi` or `shared` change. Thus, we must supply an array instead of a single path in order to let `change_in` handle all cases correctly:

``` text
change_in(['/packages/sayhi', '/packages/shared'])
```

## Running the Workflow

Click on **Run the Workflow** and then **Start**.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2lzepmdn5eeaxvokscpl.png)

The first time the pipeline runs, all blocks will be executed.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hugnkm4smv9hemg9rj0q.png)

On successive runs, only relevant blocks will start; the rest will be skipped, speeding up the pipeline considerably, especially if we’re dealing with tens or hundreds of packages in the repo.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1yetiwlyiyj03icd8sp4.png)
## Read Next

Adding TypeScript into the mix doesn’t complicate things too much. It’s a small effort that returns gains manifold with higher code readability and fewer errors.

Want to keep learning about monorepos? Check these excellent posts and tutorials:

- [Continuous Integration for Monorepos](https://semaphoreci.com/blog/continuous-integration-monorepos)
- [Monorepo and Micro-Frontends with Jonathan Creamer](https://semaphoreci.com/blog/monorepo-micro-frontends-jonathan-creamer)
- [Monorepo and Building at Scale with Benjy Weinberger](https://semaphoreci.com/blog/monorepo-building-at-scale)
- [JavaScript Monorepos with Lerna](https://semaphoreci.com/blog/javascript-monorepos-lerna)