Mazzarolo MatteoMazzarolo Matteo

Speed up your TypeScript monorepo with esbuild

By Mazzarolo Matteo


TypeScript monorepos are a great way to organize medium-to-big size projects. TypeScript improves the developer experience by adding type-checking and a deep IDE integration. And using a monorepo helps in scaling your project(s).

Compared to plain JavaScript, however, TypeScript adds an additional compilation layer to your project, which may slow down the developer experience. While the native TypeScript compiler is not that slow (IMHO), it's still something you need to take into account if you're planning to build a large codebase. But what if there was a way to speed up the TypeScript compilation by using a different compiler?

Enter esbuild: a fast JavaScript bundler that claims to be >10x faster than similar projects (webpack, rollup + terser, parcel 2). I've been using esbuilt for a couple of TypeScript projects and have been surprised by how well it performs.

🍊 Tangerine monorepo

While learning esbuild, I haven't found many examples of how to integrate it within TypeScript monorepos. So I created my own template: 🍊 tangerine-monorepo, a "minimal" TypeScript-based Node.js monorepo setup fully powered by esbuild.

Feb 3rd 2022 update: Tangerine monorepo now uses Turborepo for an even snappier developer experience 🔥

Features

Workspaces

Tangerine monorepo includes five workspaces:

All the workspaces use esbuild to compile the TypeScript codebase. Be it for building, testing, or running CLI scripts, the compilation is instantaneous compared to the native TypeScript compiler (you can quickly test the difference by temporarily swapping esbuild with tsc).

The tsc CLI is used only to type-check the codebase (without emitting the compiled files — since they're handled by esbuild). I expect people usually use the IDE integration to type-check the code anyway and explicitly invoke the tsc CLI only in specific use cases (such as pre-commit hooks).

Each workspace's package.json is pointing the main and types entry to src/index.ts. Which might look strange at first, given that it's uncompiled code... see "You might not need TypeScript project references" on the Turborepo blog for an explanation. This pattern has been working fine for my use cases so far (especially while using esbuild). Still, you might want to update these entries to suit your needs (e.g., when shipping packages to npm).

.
└── <project-root>/
    └── packages/
        ├── eslint-config/ # eslint-config shared across the workspaces
        ├── is-even/ # simple Node.js module example (with no dependencies)
        ├── is-odd/ # simple Node.js module example (depends on is-even)
        ├── jest-config/ # jest-config shared across the workspaces
        └── server/ # simple Node.js server example (depends on is-even and is-odd)
.
└── <project-root>/
    └── packages/
        ├── eslint-config/ # eslint-config shared across the workspaces
        ├── is-even/ # simple Node.js module example (with no dependencies)
        ├── is-odd/ # simple Node.js module example (depends on is-even)
        ├── jest-config/ # jest-config shared across the workspaces
        └── server/ # simple Node.js server example (depends on is-even and is-odd)

FAQs

Why are you using Yarn Classic instead of Yarn 2+?

Mainly because every time I use Yarn 2+ I encounter tiny issues requiring additional fixes or setup (e.g., Editor SDKs).
If you prefer Yarn 2+, switching to it is as easy as running yarn set version berry 👍.

Why esbuild? Why not swc?

I love swc, but I feel esbuild is still more "mature". I've also noticed that in some cases swc doesn't respect TypeScript's compilerOptions's paths.

Why Turborepo?

Turborepo is specifically built to support monorepos such as this one.
To me, the major benefits of Turborepo are an fast developer experience (mostly because of caching) and its configurability.

Why are you pointing the package.json's main and types entry to uncompiled code?

See "You might not need TypeScript project references" on the Turborepo blog. This pattern has been working fine for my use cases so far (especially while using esbuild). Still, you might want to update these entries to suit your needs.