Bundling packages in a Monorepo
Unlike internal packages, external packages can be deployed to npm (opens in a new tab) and used locally. In this guide, we'll bundle a package to both ECMAScript modules (opens in a new tab) (esm
) and CommonJS modules (opens in a new tab) (cjs
), the most commonly used formats on npm.
Setting up a build script
Let's start with a package created using our internal packages tutorial.
There, we created a math-helpers
package which contained a few helper functions for adding and subtracting. We've decided that this package is good enough for npm, so we're going to bundle it.
At the end of that tutorial, we had a package set up under /packages
, which looked like this:
├── apps
│ └── web
│ └── package.json
├── packages
│ └── math-helpers
│ ├── src
│ │ └── index.ts
│ ├── tsconfig.json
│ └── package.json
├── package.json
└── turbo.json
We're going to add a build
script to math-helpers
, using a bundler. If you're unsure which one to choose, we recommend tsup
(opens in a new tab).
First install, tsup
inside packages/math-helpers
using your package manager.
{
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts"
}
}
tsup
outputs files to the dist
directory by default, so you should:
- Add
dist
to your.gitignore
files to make sure they aren't committed bygit
. - Add
dist
to the outputs ofbuild
in yourturbo.json
.
{
"pipeline": {
"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
:
{
"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 (opens in a new tab).
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-helpers
needs to run before the build
of apps/web
.
Fortunately, we can use dependsOn
to easily configure this.
{
"pipeline": {
"build": {
"dependsOn": [
// Run builds in workspaces I depend on first
"^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 math-helpers
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:
{
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "npm run build -- --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.
Summary
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.