Engineering

Migrating to Turborepo – What to Expect

We recently rewrote Dub's codebase to use a monorepo setup via Turborepo. Here's what we learned.

Migrating to Turborepo – What to Expect

We recently rewrote Dub's codebase to use a monorepo setup via Turborepo.

In the process, we learned a lot about monorepo management – here are some of our learnings.

What is Turborepo?

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

It's built by the team at Vercel – who is also behind the Next.js framework.

Why rewrite to Turborepo?

As an open-source project, we want to make our local development setup as easy as possible – both for self-hosting and for contributing to the project.

However, our existing setup was a bit of a mess. We had both our marketing site and our main app in the same codebase, and we had to run both of them in order to develop locally.

This led to a lot of confusion for new contributors since they would had to set up dependencies and environment variables that were only relevant to the marketing site – just to contribute to the main app.

Rewriting to a monorepo setup allowed us to decouple the two codebases, and improve the local development experience.

The migration process

The migration process was relatively straightforward. We followed the official Turborepo + TailwindCSS example and made some tweaks to fit our needs.

There are plenty of amazing guides on this topic (e.g. this fantastic one from Lee Robinson) so we won't dive too deep into it, but rather focus on some of the gotchas that we ran into in the process.

Gotcha #1: Why are my Tailwind styles not working with Turborepo?

If you're using Tailwind with Turborepo, you might run into an issue where your Tailwind styles are not working properly.

To fix that, all you need to do is make sure you configure the content option in your tailwind.config.js file to include the paths to your UI package.

For example, if you are using your local UI package in one of the apps in your monorepo, you would need to add the following:

content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    // target the `ui` package in your monorepo
    "../../packages/ui/src/**/*{.js,.ts,.jsx,.tsx}",
],
...

If you are using your UI package as a third-party import in a different codebase, you should do this instead:

content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    // target the `@dub/ui` node module
    "./node_modules/@dub/ui/**/*.{mjs,js,ts,jsx,tsx}",
],
...

Shoutout to this blog post by Will Liu for the pro-tip!

Gotcha #2: Differences between dependencies and devDependencies

When you're working with a monorepo, you might run into issues with inter-package dependencies.

For example, in our Dub codebase, we had a @dub/ui package that depended on a @dub/utils package.

├── apps
│   ├── web
├── packages
│   ├── utils
│   └── ui <-- depends on `utils`

Naturally, we would add the @dub/utils package under the dependencies field in the package.json file of the @dub/ui package.

{
  "name": "@dub/ui",
  "dependencies": {
    "@dub/utils": "workspace:*"
  }
}

While this would work fine in the monorepo, this would cause issues when we publish the @dub/ui package to npm. When you install the @dub/ui package from npm, it would throw an error saying that it could not find the @dub/utils package with the workspace:* tag.

To fix this, we had to move the @dub/utils package from the dependencies field to the devDependencies field.

{
  "name": "@dub/ui",
  "devDependencies": {
    "@dub/utils": "workspace:*"
  }
}

This would ensure that @dub/uitls won't block the installation of the @dub/ui package from npm – and since you are also importing the @dub/utils package, it would still be available in your local development environment.

Gotcha #3: Latest versions of tsup doesn't export .d.ts files

If you're using tsup to compile your Typescript packages, you might run into an issue where your .d.ts files are not being exported properly, causing your Typescript imports to fail.

To fix this, we had to downgrade to tsup@6.1.3:

pnpm install tsup@6.1.3 --save-dev

This issue might be fixed in the future, so keep an eye out on the tsup changelog to see if you can upgrade to a newer version.

Pro-tip: Use prettier-plugin-organize-import for organizing imports

When migrating to a monorepo setup, you might need to bulk update your imports to use the new package names.

This is where the prettier-plugin-organize-import plugin comes in handy.

It helps you organize your imports in a consistent manner, as well as auto-combining duplicate imports. Watch it in action below:

Underrated: @PrettierCode. Specifically the "prettier-plugin-organize-import" plugin. Being able to automatically sort and combine duplicate imports is such a superpower for developer productivity.

Build Faster with Turborepo

We're excited for what this migration to Turborepo unlocks for Dub.

Not only are we now able to build our code faster with Turborepo's high-performance build system, we were also able to decouple our codebases for a better local development experience.

This means that we will be able to improve our self-hosting experience, as well as make it easier for contributors to make changes to the codebase.

Our codebase is fully open-source, so feel free to check it out and learn more about our Turborepo setup.

Supercharge
your marketing efforts

See why Dub.co is the link management infrastructure of choice for modern marketing teams.