There are more migration steps needed, this guide is still a work-in-progress.
We follow Bazel's LTS policy.
rules_js and the related rules depend on APIs that were introduced in Bazel 5.0.
However we recommend 5.1 because it includes a cache for MerkleTree computations, which makes our copy operations a lot faster.
As explained in the README, rules_js depends on rules_nodejs. We need at least version 5.0.
This also requires you upgrade build_bazel_rules_nodejs to 5.x,
along with @bazel-scoped npm packages like @bazel/typescript.
rules_js is based on the pnpm package manager.
Our implementation is self-contained, so it doesn't matter if Bazel users of your project install pnpm.
However it's typically useful to create or manipulate the lockfile, or to install packages for use outside of Bazel.
You can follow the pnpm install docs.
Alternatively, you can skip the install. All commands in this guide will use npx to run the pnpm tool without any installation.
If you want to use a hermetic, Bazel-managed pnpm and node rather than use whatever is on your machine or is installed by
npx, see the FAQ.
rules_js uses the pnpm lockfile to declare dependency versions as well as a deterministic layout for the node_modules tree.
You can use the npm_package_lock/yarn_lock attributes of npm_translate_lock to keep using those package managers.
When you do, we automatically run pnpm import on that lockfile to create the pnpm-lock.yaml that rules_js requires.
This has the downside that hoisting behavior may result in different results when
developers use npm or yarn locally, while rules_js always uses pnpm.
It also requires passing the package.json file to npm_translate_lock which invalidates that rule
whenever the package.json changes in any way.
As a result, we suggest using this approach only during a migration, and eventually switch developers to pnpm.
If you're ready to switch your repo to pnpm, then you'll use the pnpm_lock attribute of npm_translate_lock. Create a pnpm-lock.yaml file in your project:
- Most migrations should avoid changing two things at the same time,
so we recommend taking care to keep all dependencies the same (including transitive).
Run
npx pnpm importto translate the existing file. See the pnpm import docs - If you don't care about keeping identical versions, or don't have a lockfile,
you could just run
npx pnpm install --lockfile-onlywhich generates a new lockfile.
To make those commands shorter, we rely on the
npxbinary already on your machine. However you could use the Bazel-managed one from rules_nodejs instead, like so:bazel run -- @nodejs_host//:npx_bin pnpm@latest i --lockfile-only
The new pnpm-lock.yaml file needs to be updated by engineers on the team as well,
so when you're ready to switch over to rules_js, you'll have to train them to run pnpm
rather than npm or yarn when changing dependency versions or adding new dependencies.
If needed, you might have both the pnpm lockfile and your legacy one checked into the repo during a migration window. You'll have to avoid version skew between the two files during that time.
A few packages have bugs which rely on "hoisting" behavior in yarn or npm, where undeclared dependencies can be loaded because they happen to be installed in an ancestor folder under node_modules.
In many cases, updating your dependencies will fix issues since maintainers are constantly addressing pnpm bugs.
Another pattern which may break is when a configuration file references an npm package, then a library reads that configuration and tries to require that package. For example, this mocha json config file references the mocha-junit-reporter package, so mocha will try to load that package despite not having a declared dependency on it.
Useful pnpm resources for these patterns:
- https://pnpm.io/package_json#pnpmpackageextensions
- https://pnpm.io/faq#pnpm-does-not-work-with-your-project-here
In our mocha example, the solution is to declare the expected dependency in package.json using the pnpm.packageExtensions key: https://github.com/aspect-build/rules_js/blob/main/package.json.
Another approach is to just give up on pnpm's stricter visibility for npm modules, and hoist packages as needed.
pnpm has flags public-hoist-pattern and shamefully-hoist which can do this, however we don't support those flags in rules_js yet.
Instead we have the public_hoist_packages attribute of npm_translate_lock.
In the future we plan to read these settings from .npmrc like pnpm does; follow aspect-build#239.
As long as you're able to run your build and test under pnpm, we expect the behavior of rules_js should match.
Typically you just add a npm_link_all_packages(name = "node_modules") call to the BUILD file next to each package.json file:
load("@npm//:defs.bzl", "npm_link_all_packages")
npm_link_all_packages(name = "node_modules")This macro will expand to a rule for each npm package, which creates part of the bazel-bin/[path/to/package]/node_modules tree.
The WORKSPACE file contains Bazel module dependency fetching and installation.
Add install steps from a release of rules_js, along with related rulesets you plan to use.
rules_js spawns all Bazel actions in the bazel-bin folder.
- If you use a
chdir.jsworkaround for tools like react-scripts, you can just remove this. - If you use
$(location),$(execpath), or$(rootpath)make variable expansions in an argument to a program, you may need to prefix with../../../to avoid duplicatedbazel-out/[arch]/binpath segments. - If you spawn node programs, you'll need to pass the
BAZEL_BINDIRenvironment variable.- In a
genruleaddBAZEL_BINDIR=$(BINDIR) - ctx.actions.run add
env = { "BAZEL_BINDIR": ctx.bin_dir.path}
- In a
- the load point is now a
binsymbol frompackage_json.bzl - this now produces different rules, which are explicitly referenced from
bin - to run as a tool under
bazel buildyou use [package] which is ajs_run_binary- rename
datatosrcs - rename
templated_argstoargs
- rename
- as a program under
bazel runyou need to add a_binarysuffix, you get ajs_binary - as a test under
bazel testyou get ajs_test
Example, before:
load("@npm//npm-check:index.bzl", "npm_check")
npm_check(
name = "check",
data = [
"//third_party/npm:package.json",
],
templated_args = [
"--no-color",
"--no-emoji",
"--save-exact",
"--skip-unused",
"third_party/npm",
],
)Example, after:
load("@npm//:npm-check/package_json.bzl", "bin")
exports_files(["package.json"])
bin.npm_check(
name = "check",
srcs = [
"//third_party/npm:package.json",
],
args = [
"--no-color",
"--no-emoji",
"--save-exact",
"--skip-unused",
"third_party/npm",
],
)Once everything is migrated, we can remove the legacy rules.
In package.json you can remove usage of the following npm packages which contain Bazel rules, as they don't work with rules_js.
Instead, look under https://github.com/aspect-build/ for replacement rulesets.
@bazel/typescript@bazel/rollup@bazel/esbuild@bazel/create@bazel/cypress@bazel/concatjs@bazel/jasmine@bazel/karma@bazel/terser
Some @bazel-scoped packages are still fine, as they're tools or JS libraries rather than Bazel rules:
@bazel/bazelisk@bazel/buildozer@bazel/buildifier@bazel/ibazel(watch mode)@bazel/runfiles
In addition, rules_js and associated rulesets can manage dependencies for tools they run. For example, rules_esbuild downloads its own esbuild packages. So you can remove these tools from package.json if you intend to run them only under Bazel.
In WORKSPACE you can remove declaration of the following bazel modules:
build_bazel_rules_nodejs
You'll need to remove build_bazel_rules_nodejs load() statements from BUILD files as well.
We suggest using https://docs.aspect.build/ to locate replacements for the rules you use.