Publishing libraries

Publishing a package to the npm registry from a monorepo can be a smooth experience, with the right tools.

While this guide cannot solve for every possible compiling, bundling, and publishing configuration needed for robust packages, it will explain some of the basics.

You should follow this setup if you want to publish some of your monorepo's packages to npm. If you don't need to publish to npm, you should use an Internal Package instead. They're much easier to set up and use.

Bundling

Unlike Internal Packages, external packages can be deployed to npm and used locally. In this guide, we'll bundle a package to both ECMAScript modules (esm) and CommonJS modules (cjs), the most commonly used formats on npm.

Setting up a build script

Let's start with a package created using the Internal Packages tutorial.

There, we created a @repo/math package which contained a few helper functions for adding and subtracting numbers. We've decided that this package is good enough for npm, so we're going to bundle it.

We're going to add a build script to @repo/math, using a bundler. If you're unsure which one to choose, we recommend tsup.

Install tsup inside the ./packages/math package using your package manager and then create a build script for it:

./packages/math/package.json
{
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts"
  }
}

tsup outputs files to the dist directory by default, so you should:

  1. Add dist to your .gitignore files to make sure they aren't committed to source control.
  2. Add dist to the outputs of build in your turbo.json.
./turbo.json
{
  "tasks": {
    "build": {
      "outputs": ["dist/**"]
    }
  }
}

That way, when tsup is run the outputs can be cached by Turborepo.

Finally, we should update our package entrypoints. Inside package.json, change main to point at ./dist/index.js for clients using CommonJS modules (cjs), module to point at ./dist/index.mjs for clients using ECMAScript modules (esm), and types to the type definition file - ./dist/index.d.ts:

./packages/math/package.json
{
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts"
}

It is not required to bundle to both cjs and esm. However, it is recommended, as it allows your package to be used in a wider variety of environments.

If you run into errors by using main, module and types, take a look at the tsup docs.

Bundling is a complicated topic, and we don't have space here to cover everything!

Building our package before our app

Before we can run turbo run build, there's one thing we need to consider. We've just added a task dependency into our monorepo. The build of packages/math needs to run before the build of apps/web.

Fortunately, we can use dependsOn to easily configure this.

./turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"]
    }
  }
}

Now, we can run turbo run build, and it'll automatically build our packages before it builds our app.

Setting up a dev script

There's a small issue with our setup. We are building our package just fine, but it's not working great in dev. Changes that we make to our @repo/math package aren't being reflected in our app.

That's because we don't have a dev script to rebuild our packages while we're working. We can add one easily:

./packages/math/package.json
{
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
  }
}

This passes the --watch flag to tsup, meaning it will watch for file changes.

If we've already set up dev scripts in our turbo.json, running turbo run dev will run our packages/math dev task in parallel with our apps/web dev task.

Our package is now in a spot where we can consider deploying to npm. In our versioning and publishing section, we'll do just that.

Versioning and publishing

Manually versioning and publishing packages in a monorepo can be tiresome. Luckily, there's a tool that makes things easy - the Changesets CLI.

We recommend Changesets because it's intuitive to use, and - just like Turborepo - fits with the monorepo tools you're already used to.

Some alternatives are:

Publishing

Once your package has been bundled, you can then publish it to the npm registry.

We recommend taking a look at the Changesets docs. Here's our recommended reading order:

  1. Why use changesets? - an intro that takes you through the fundamentals.
  2. Installation instructions
  3. If you're using GitHub, consider using the Changeset GitHub bot - a bot to nudge you to add changesets to PR's.
  4. You should also consider adding the Changesets GitHub action - a tool which makes publishing extremely easy.

Using Changesets with Turborepo

Once you've started using Changesets, you'll gain access to three useful commands:

Terminal
# Add a new changeset
changeset
 
# Create new versions of packages
changeset version
 
# Publish all changed packages to npm
changeset publish

Linking your publishing flow into Turborepo can make organizing your deploy a lot simpler and faster.

Our recommendation is to add a publish-packages script into your root package.json:

./package.json
{
  "scripts": {
    // Include build, lint, test - all the things you need to run
    // before publishing
    "publish-packages": "turbo run build lint test && changeset version && changeset publish"
  }
}

We recommend publish-packages so that it doesn't conflict with npm's built-in publish script.

This means that when you run publish-packages, your monorepo gets built, linted, tested and published - and you benefit from all of Turborepo's speedups.

hours

Total Compute Saved
Get started with
Remote Caching →

On this page