Skip to main content

Powered by Turborepo

Why Turborepo? We wanted to expose Crystallize App components in a public design system, speed up build times in the CI and on local machines, and create the foundation for micro frontends.

Powered by Turborepo

Living On the Edge

Nowadays, it can be both good and bad to jump straight away on that new hype, that new framework or tool that promises a lot of improvements and benefits. The good news is that we can always be up to date with what's new in the tech world. The bad is that we are subject to rapid changes and sometimes the sudden end of what we have just started using in our tech stack.

Today we will talk about our experience with Turborepo in our frontend stack, which powers app.crystallize.com, amongst other things.

Turborepo is a high-performance build system for JavaScript and TypeScript codebases.

We won't be explaining what Turborepo is because that is already well explained in this video and their docs. Instead, we’ll talk about our experience with it and how it's going so far.

BTW recently, we migrated our frontend to TypeScript.

Where We Started

Our application is a typical React project, and it includes a bunch of reusable components. Some of those components are public, like @crystallize/react-dialog and @crystallize/react-growl, and are maintained in their repositories. Other components live within the application and are not shared outside the application.

Our typical build time was about two minutes, which is only for the application.

For some time now, we've wanted to open-source a bunch of those reusable components, like buttons, inputs, and other things, without compromising on the developer experience for our app team. We see a need for others to use those, especially when creating their Apps in Crystallize.

We wanted to move away from our current shared component setup, where you need to publish the package to npm, update the app version and install it to get the latest changes. With a monorepo, the shared component package could just be a local dependency of our UI app but still be published separately on the CI when there were changes.

What does this mean in practical terms? It allows us to develop new components in the design system and immediately use them in our UI without having to go through publishing to npm and installing the latest version of the package.

This is where Turborepo comes in.

Introducing Turborepo

We got started by following their getting started guide, which works for any existing project setup. The Crystallize application got a new place to live, apps/web, and its Typescript and Eslint configuration were elevated to two shared packages, ready to be used by any other package or app that we knew we would add later. Then all we needed to do was to install packages, pnpm install in our case, which creates a top-level lockfile, not one per project as we’ve had in the past.

Configuring the CI

We’re using GitHub Actions pipelines, and the consequence of switching to a monorepo is that all apps and packages now share CI configuration under the root level .github/workflows. We altered the configuration for our existing CI configuration, mainly just changing the paths to reflect the app's new location in the repo.

Speeding Up Build Times

Turborepo introduces a concept called “remote cache,” which uploads your build outputs into the cloud, making them available for everyone on the team, including you CI runner. This means that a certain version of the app or a package will only be built once. If you execute a build and Turborepo figures out that it has been built before, it will just download the artifacts of that build, which for us meant reducing two-minute build time to roughly 10 seconds for a cache-hit.

That is good not just for developer experience but could also prove to be a cost slicer for your CI setup if you are running local builds.

Adding a Design System

The app lives under apps/web, and the design system lives under packages/design-system. Whenever you want to use a component from the app, you just do the following:

import { Button } from '@crystallize/design-system'

Wait, how does that work? This is a simple configuration from pnpm, telling it that the design system is a workspace package:

App package.json
{
   dependencies: {
      "@crystallize/design-system": "workspace:*"
   }
}

Killer developer experience right there. We’re importing files like they were published on npm, but in reality, the files are fetched from our local repo and hot-reloaded on the fly if it changes.

Versioning

As we moved every package into a single repository, we could no longer use semantic-release for managing the versioning and releasing to npm, as it relies on the commit message format to determine the next version. We switched to using changeset, which is tailored for monorepos and has a very helpful Github CI bot that makes releasing new versions trivial.

Final Thoughts

Do we believe this configuration is ideal? No, there could possibly be some improvements made.

Like with everything else in web development, ongoing changes and potential improvements are being made both internally and as a result of the advancement of the tools being used.

However, for the time being, everything is going smoothly, and we are delighted with the use of Turborepo to migrate to a monorepo.