A great TypeScript NPM module mono repo setup

Jake Ginnivan
7 min readJun 27, 2020

Over the years I have setup and published a number of open source projects, ensuring they have automated builds/deployments and make it easy for me to accept pull requests then release a new version with ease.

There are a few parts which are often talked about in isolation:

  • CI
  • Versioning
  • Releases
  • Builds / packaging

A complete example setup is available at https://github.com/JakeGinnivan/example-project-structure

The Setup

I prefer not using a tool which tries to do everything, instead use simple but powerful tools to bring everything together.

  • CI — GitHub actions
  • Versioning / releases — ChangeSets
  • Mono repo management— Yarn workspaces
  • Package setup — TypeScript projects
  • Tests — jest with ts-jest

I still use the TypeScript compiler to build my projects, I may change to Babel down the track but for now it works pretty well for me.

Project Structure

TypeScript projects + yarn workspaces make it super easy to have multiple npm packages in a single repo.

Root tsconfig.json

The really important thing to note is the files: [] entry, the root project needs to just reference the other projects. You then run tsc -b to build all projects.

Root tsconfig.settings.json

This is the minimum settings our projects require in my opinion, you need to have composite enabled to enable project references.

I also tend to always set importHelpers to true and set it as a peerDependency for my projects, it doesn’t make sense that any TypeScript helpers end up in each of my NPM packages. What do you do here? I would love to chat through this with someone!

Root package.json

I will cover all of the things in this later in the post, but for now the most important part is the workspaces key which tells yarn where to look for other package.json files.

NOTE: To add dependencies to this root project you need to specify the -W flag when adding the dependency.

yarn add typescript -W

Project tsconfig.json

I also often have a second tsconfig.esm.json per project to create ES Module builds which support tree shaking

Project package.json

The important part is that main points at the commonJS entry point and module points at the ES Module build

Package inter-dependencies

Part of having packages in the same repo is that it should be easy to reference between them. With TypeScript projects this becomes really easy.

There are 2 parts, the first is adding a project reference. This is so TypeScript knows that project 1 needs to be built before project 2.

The second part is setting up a typescript path mapping, this allows you to use the absolute package name in imports from that dependent package. If you accidentally use a relative import between the two projects that will break when the project is published to NPM.

Project structure summary

With all that done, our project looks roughly like this.

After you run yarn tsc -b you will see a dist folder created with both ESM and CommonJS builds, both with source mappings and type definitions.

I am sure it would be possible to share type definitions for the two builds, but for now it’s easier to just generate for both.

CI

In the past I have used AppVeyor, CircleCI, TravisCI, TeamCity and others. While many of these are great and free for open source having CI in the same tool as the code just reduces friction and makes it really easy to get up and running.

You may notice that I am referencing the verify script in my root package.json rather than having to have a more complicated build script I just have a single command which runs everything which the CI build will run. And that is all we need to get Continuous Integration happening on our build.

Versioning

One of my attempts at solving the versioning problems was https://github.com/GitTools/GitVersion, I really enjoyed this project and think it worked pretty well.

The problem it tried to solve was using Git to work out what the next version will be. The way it worked was look for the last git tag, then look commits / merged branch names to calculate the next SemVer. Very similar to the way Semantic Release works, but way more configurable. This has served me well for years, but still felt heavy and complex. Enter Changesets.

It works in a conceptually similar way to GitVersion except it also fixes changelogs / release notes as well. First step is installing it, then initialising the changesets config

yarn add -W @changesets/cli
yarn changeset init

The first thing you will want to do is change the access setting in .changeset/config.json to public .

{
"$schema": "https://unpkg.com/@changesets/config@1.1.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"linked": [],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch"
}

The way changesets work is you now just run yarn changeset when you are making any change.

It will look for packages with changes (using git) and you can just select the package which has changed.

Run through the questions

Letting it know if it’s a major/minor/patch change and giving a summary of the change.

It generates a markdown file in the .changeset folder. As more unreleased changes are merged more markdown files will accrue in that folder.

Pretty happy with that top generated file name too, big-yaks-attack, solid. From this point we can run yarn changeset version to update the package.json versions and the CHANGELOG.md files.

You can see the changes this creates below.

If you wanted to publish from your own machine run yarn changeset publish instead of the version command it will publish the updated packages to NPM, git tag, create a GitHub release and all that jazz. But don’t do that, it is super easy to setup automated released from GitHub, so let’s do that instead.

Setting up automated releases

We will be using the changesets GitHub action to help us with automating the release flow.

First step is to add a release build using GitHub actions. Just check in .github/workflows/release.yml with the following

It simply will run the project build then the changesets version action. When it runs it will open a pull request with any unreleased changes

And as additional changes are merged it will keep this pull request up to date. Next head over to your project settings and add your NPM_TOKEN as a project secret.

Then finally, merge the pull request and changesets will do all the things you would want it to do. It commits the CHANGELOG.md, the package.json version number changes, tags the released commit, creates a GitHub release and of course publishes to NPM.

Testing

I have found with testing it’s far easier to test the repo rather than the projects/packages in that repo. For our libraries our tests are fast enough that this approach works fine.

To set this up we can create a tsconfig for our tests, there are 2 interesting things in this config file. The first is I am targeting es2018, this allows our tests to use more native features in node, hopefully giving your tests a bit of a perf boost.

The second is the path mapping, because we are not leveraging TypeScript project references for our tests we need to explicitly map the import to the root TypeScript source file of the module.

Our Jest config file is pretty standard, I am using ts-jest for the TypeScript support rather than Babel, that way I get Type checking when my tests run without setting up additional compilation checks.

The important line here is the last one, ts-jest comes with a helper which maps TypeScript path mappings to jest path mappings, allowing you to just add the mapping into your TypeScript config then your tests will just work.

Summary

This has been a pretty long blog post, but it breaks down all the things you need to create a mono repo containing NPM modules with CI, changelogs, automated releases, tests, SemVer etc.

If you have any ideas around ways to improve any aspect of this please ping me on Twitter (https://twitter.com/JakeGinnivan/). I am always working on improving this setup!

--

--

Jake Ginnivan

Co-Founder featureboard.app | Principal Consultant arkahna.io | Previously Tech Lead Seven West Media WA | International Speaker | OSS | Mentor