I am working full-time on a suite of libraries for building games with Three.js and React; this suite is called the Composer Suite, and its growing number of libraries and applications all live in a single monorepo. In this post, I will describe the tools I use to manage this repository, and to make both the development as well as the releasing of new packages and package versions as painless as possible.
Please read this list as a snapshot of what I currently use. I rely on good tooling to stay productive (and sane), so sometimes I add one thing and remove another. I will review and update this post over time.
I use pnpm for package management. I am not super-religious about it, having used yarn up until recently, but so far, I've been really enjoying its benefits.
pnpm is primarily known for how, instead of duplicating the same packages into every
node_modules, it will hard-link them to a central repository, vastly cutting down on both disk space used for dependency installations, but also speeding up the install process significantly.
I'm also enjoying its overall developer experience; it has an exceptionally well-designed CLI, very clear to understand logging output, and I even find its lock file to be easier to make sense of in the rare occasions where I need to find out more about pnpm's current view of my repository.
pnpm can also link packages to dependencies within the same monorepo that have not yet been published to NPM, which is great for bootstrapping new packages, which happens surprisingly often in my case. :D
For actually building my packages, I use the excellent Preconstruct. Besides being a very reliable build tool, it enforces an opinionated structure to your packages that just makes sense and helps you avoid a lot of the pitfalls that come with both monorepos and publishing packages to NPM.
But its absolute killer feature is
preconstruct dev, a command that will link dependent packages within the monorepo in a way that lets packages and apps authored in TypeScript consume their dependencies' original TypeScript code directly.
I write all my libraries with TypeScript, and I've always hated firing up build watchers. With
preconstruct dev, I can fire up an application and just make changes to the code of any of the packages it depends on, and those changes will be reflected immediately, including full support for HMR, without having to spawn any kind of build watchers. It's a huge time-saver that I've come to rely on heavily.
Publishing new versions of packages typically involves multiple steps:
- Bump the version number in the package's
- Update the changelog for the package
- Make sure the package is actually built 🤡
- Actually publish to NPM
- Create a Release entry on GitHub
This can be a lot of work — particularly maintaining a useful changelog. Which is why I use Changesets, which — together with its great GitHub Action — does pretty much all of this work for me.
Whenever I make a change, fix a bug or add a new feature that warrants a new release, I use the Changesets CLI to create a new changeset file, which is just a Markdown file describing the change, and including some meta information about which packages are affected, and should be version-bumped, and what kind of version bump they should get (patch, minor, or major.)
These changeset files are the committed to the repository alongside the change they describe. Then, when I'm ready to publish a new version of my packages, the
changeset version command will vacuum up all the accumulated changesets, transfer their contents to the relevant
CHANGELOG.md files, and bump the version numbers in the
package.json files of the affected packages. I can then run
changeset publish to have it automatically publish the newly bumped versions to NPM.
And since I'm lazy, and lazy is good, I actually use the excellent Changesets GitHub Action, which will do all of this for me, by way of a Pull Request it will automatically create (you will often find them in the Pull Requests tab of the Composer Suite monorepo.) I can just merge that Pull Request, and a couple of minutes later, my packages will be published to NPM, and GitHub Release entries are created for them, too!
I find this entire approach much better than generating changelogs from commit messages, because changeset files are much more expressive, and can be edited to add more information, or to fix typos, and so on. They also make it much easier to review the changes that are being published, and to make sure that the changelog entries are actually useful.
Speaking of GitHub Actions, they're just incredible, and you should use them. In the Composer Suite monorepo, I have one action for running tests on all branches and Pull Requests, and the Changesets action described above. If I ever get serious about linting, I'll probably add that, too. The key point here is: automation is good! Go forth and automate!
The latest addition to the Composer Suite monorepo, Turborepo optimizes monorepo workflows by caching build artifacts. This may sound a little abstract and boring, but what this actually means is that when you build something within your monorepo, Turborepo will make sure only the things that it depends on are rebuilt; everything else will be retrieved from a cache that either lives on your local computer, or a remote cache server. This can make building your entire monorepo very fast, or even no-op:
Adding Turborepo to the Composer Suite monorepo pretty much halved CI build times, but it was also a way to teach Vercel, which I use for hosting the various example apps in the repo, to only actually deploy the ones that have changed since their last deployment. And that's really cool!
I do all of my development in Visual Studio Code, and love the official GitHub Pull Requests and Issues extension. It essentially integrates the entire Pull Request flow directly into the editor; you can directly check out Pull Requests from its UI, browse around in the code, post and reply to comments, make further changes, open or close the Pull Request for review, and of course also eventually merge and close it. It's such an incredible time saver for interacting with incoming (and also your own) PRs, and I heavily recommend it.
These are my favorite monorepo things right now. Without them, I would most definitely be a lot less productive. I hope you find these recommendations useful. If you have any questions, or if you have any other tools you'd like to recommend, please let me know in the comments!