Developer diary: Day 1 — Bazel build system with monorepo and typescript
As we grow as a company with an increasing number of developers the pain of maintaining multiple shared libraries grows. In an effort to try to mitigate this I figured I take a stab at trying to combine our shared typescript libraries in a monorepo. This will allow a simpler development flow when working with dependencies between shared libraries by having all code in the same place and stamp new releases of all libraries at the same time.
In addition to merging all libraries, that makes sense, into a single repository or monorepo I also want to add the Bazel build system in to the development flow.
Why Bazel? I like the idea with a language agnostic build system and it’s what is cool now.
First thing is to create a project structure, I ended up with something like this:
packages/
server/
src/
BUILD.bazel
package.tpl.json
event/
src/
BUILD.bazel
package.tpl.jsontest/
server/
BUILD.bazel
event/
BUILD.bazeltools/.bazelrc
BUILD.bazel
package.json
WORKSPACE
In packages
we have two shared libraries server
and event
. Both of theses will be release as separate library. The unit test for each of these resides in test
. In tools
we will add some helper methods/rules Bazel.
When starting with with Bazel it can be really really confusing. Should I use BUILD.bazel
or BUILD
? How do I update versions? How do I debug Bazel rules that fail? And so on… To be honest there’s still some magic happening that I still don’t fully understand.
I’ve take a lot of inspiration from the Angular project and the work of Alex Eagle.
So let’s dive in.
A good place to start is to have a look at the Building JavaScript Outputs guide on the Bazel site. It outlines some of the basics. When Bazel (and ibazel if you want a watcher) is installed we need to add a couple of things in the root folder:
In /.bazelrc
we can setup a few default options for some commands that should be pushed to the repo so it’s shared with everyone that uses it:
In /BUILD.bazel
we expose our tsconfg.json
to Bazel. All files that Bazel needs in it’s rules needs to be exposed.
In WORKSPACE
we defined our root workspace, which rules that should be loaded for all rules:
We need to add our first build tool script. This is used each time Bazel is used with the build
command, in tools/bazel_stamp_vars.sh
we add a environment variable that we can later use to stamp our package with with a release tag:
Now that we have our basic setup in place, let’s add some code in packages/server/src/index.ts
and packages/server/src/calculator/calculator.ts
:
In order to be able to build our package we add the following to packages/server/BUILD.bazel
:
The important thing here is module_name
which is the name it will be publish with on NPM or in your private npm-repository. Note: This property is undocumented for some reason.
Now we can build our code by calling the Bazel package with the path to BUILD.bazel
and the package name, in our case shared-red-server-library
:
bazel build //packages/server:shared-red-server-library
Before we publish our packages we needs some tests, in test/
we mirror our structure from packages
ie:
test/
server/
src/
calculator/
calculator.spec.ts
BUILD.bazel
BUILD.bazel
We add an empty BUILD.bazel
in the root of test
to let Bazel know that there’s packages inside. Then we need add some rules for our test package test/server/BUILD.bazel
:
Note: We use mocha for unit testing. Unfortunately `mocha_node_test` isn’t part of the official package. I’ve created my own based on a PR in github. I will add the code for this in the test repo for this article.
Now we write a unit test for our calculator in test/server/src/calculator/calculator.spec.ts
. In our test we can import our Calculator
form the package (the module_name
we added before) instead from a relative path.
To run the tests we do:
bazel test //test/server:shared-red-server-library_test
Now to the fun part. Releasing the package to the world. In order to do this we add two new rules to our packages/server/BUILD.bazel
so it looks like this:
The new npm_package
creates the npm package. We can create a package as a gzipped file or publish it to npm. We also have a genrule
that uses some jq
magic to set our new version. It picks up the version from a file called volatile-status.txt
that is created with our tools/bazel_stamp_vars.sh
script. This solution is kind of hacky, but it’s what I managed to figure out.
We also need to a packages/server/package.tpl.json
file that we use as template for our npm pakage:
To create a gzipped file with our new package we can do:
bazel run //packages/server:shared-red-server-library_package.pack
And then to publish a new version we create a new tag and then publish the package:
bazel test //test/server:shared-red-server-library_testnpm version patch -m "release: Throwing out v%s to the wolfs"bazel run //packages/server:shared-red-server-library_package.publishgit push --follow-tags
Now we can just redo the same steps for packages/event
as for packages/server
and publish at the same time. Like so:
bazel test //test/server:shared-red-server-library_test
bazel test //test/event:shared-red-event-library_testnpm version patch -m "release: Throwing out v%s to the wolfs"bazel run //packages/server:shared-red-server-library_package.publish
bazel run //packages/event:shared-red-event-library_package.publishgit push --follow-tags
I published all code for this here: https://github.com/RedFlagTeam/developer_diary_day1
..fredrik