A test of publishing multiple interdependent npm packages from a Bazel monorepo
To install dependencies, run yarn
.
To demonstrate publishing to npm, this demo uses verdaccio, a private npm proxy. To install verdaccio, run:
yarn global add verdaccio
Then, run verdaccio
and look for where its config file is loaded from (should be the first line logged).
Edit the config to allow anyone to publish scoped packages and restart verdaccio:
config.yml
...
packages:
'@*/*':
# scoped packages
access: $all
publish: $anonymous
unpublish: $anonymous
proxy: npmjs
...
These examples mostly assume you're starting from scratch in a new repo (although they can apply to an existing repo given enough refactoring). Check the build_with_both
directory for details on what I think is a good way to convert a package in a JS monorepo to use Bazel.
Bazel is an open-source build toolchain developed by Google. It uses BUILD files to define targets that can be built or run and dependencies between targets.
TypeScript is compiled by the
ts_library
rule.
As an example, take a look at the foo_lib
target in
foo/BUILD.bazel
:
ts_library(
name = "foo_lib",
module_name = "@test_scoped_repo/foo",
visibility = ["//visibility:public"],
srcs = [
"foo.ts",
],
deps = [
"//foo/bla:bla_lib",
],
)
This target defines a TypeScript library named foo_lib
that compiles the foo.ts
source file and depends on the
//foo/bla:bla_lib
TypeScript library. It's publicly visible in the project and has the amd module name @test_scoped_reop/foo
.
To compile this target, run bazel build //foo:foo_lib
(or yarn bazel build //foo:foo_lib
). If you look in the foo/
directory, you'll see that there are no new files created by building the target. That's because Bazel puts
all of its outputs in an output directory
somewhere else and places symlinks to that directory in the root of the project.
In the case of this repo, the symlinks are located in dist/
, but that can be configured by editing
.bazelrc
.
Take a look in dist/bin/foo/
. That directory should contain the results of compiling foo.ts
. It also contains the bla/
subdirectory, which has the results of compiling foo/bla/bla.ts
, which foo.ts
depends on. bla.ts
was compiled because
the foo_lib
target listed bla_lib
in its deps
attribute. For more information on how to declare these dependencies, see the
documentation on Bazel Labels and the
ts_library
documentation.
rules_nodejs provides the
jasmine_node_test
rule for running jasmine tests. As an example,
we'll look at how to test the foo_lib
target.
Since foo_test.ts
is written in TypeScript but jasmine_node_test
requires JavaScript inputs, we first declare a ts_library
target named
foo_test_lib
to compile it. since foo_test_lib
depends on foo.ts
, it needs the :foo_lib
target in its deps
attribute.
It also needs jasmine typings, so we add @npm//@types/jasmine
to its deps (rules_nodejs automatically generates targets for all
installed npm packages. Note that
we're actually using yarn,
but the convention is to use the name npm
for the generated workspace's name).
foo_test_lib
now compiles to a .js
file (dist/bin/foo/foo_test.js
) that jasmine
can run. Instead of running this
file with jasmine, we'll use the jasmine_node_test
rule to run it by creating a new target named foo_test
and passing it
the :foo_test_lib
target as a source.
ts_library(
name = "foo_test_lib",
testonly = True,
srcs = [
"foo_test.ts",
],
deps = [
":foo_lib",
"@npm//@types/jasmine",
],
)
jasmine_node_test(
name = "foo_test",
srcs = [
":foo_test_lib",
],
)
To run the test target, run bazel run //foo:foo_test
, or run bazel test //foo:foo_test
.
By default, bazel test
shows minimal information about the test, just printing whether it passed or failed, and is best used for
running multiple tests at once.
Alex Eagle has a great writeup on how to do this. This section covers how it's done in this repo.
rules_nodejs
provides the pkg_npm
rule for tar
ing and
publishing a package to npm, but there are a few caveats to keep in mind when doing this.
- The package needs its own
package.json
that declares its dependencies and name. - The package name in
package.json
should be the same as themodule_name
in the rootts_library
. All other importablets_library
targets should also setmodule_name
to the corresponding path relative to where they appear in the filetree (take a look atfoo/bla
for an example). This makes sure imports look the same whether the module is loaded from bazel or from npm and is why thebar
package works in bazel and in npm.
To publish foo
and bar
to npm (verdaccio), start verdaccio and run yarn publish-all
in the root of the repo. This will run the following commands:
bazel run //foo:foo_pkg.publish -- --registry http://localhost:4873
bazel run //bar:bar_pkg.publish -- --registry http://localhost:4873
To simulate an external user consuming the packages, cd to external_package
and run yarn install-local
(to use verdaccio). Run yarn tsc
to build and node dist/external_package.js
to run the result.