Getting started with Bazel for web developers
Check the complete version of the monorepo code here .
In this post, I want to share my story of how I started experimenting with Bazel and eventually fell in love with it.
To understand why Bazel exists, letβs go back to the basics. As a self-taught developer, I have often struggled with the abstractions introduced by new frameworks. This made it difficult for me to grasp fundamental software engineering concepts, despite having a solid introduction to software and hardware design during my engineering degree.
A C++ programmer would have no trouble understanding why Bazel is useful and how to use it. C++ developers typically have a mindset of understanding how things work under the hood. However, modern web developers, especially those confined to frameworks, often lack this understanding due to the numerous layers of abstraction added on top of the actual software workings.
Let me remind you how any software works:
source code > build process > executable
If you donβt understand how your application goes from source code to an executable, you wonβt be able to effectively use Bazel to organize your development process for that application. Unfortunately, the majority of React developers donβt even know what npm run build
does under the hood.
Now, letβs explore why you would ever need to use Bazel. Bazel is particularly useful for orchestrating multiple projects with different languages in a monorepository. In an ideal scenario, you would have:
- One hermetic toolchain and dependency setup for every language, regardless of the platform.
- Build, test, run, and deploy configurations set up once, and then you can use
bazel build/test/run //path:target
to execute them.
Letβs consider a JavaScript project (this applies to all its variants). A typical React application has the following structure:
my-react-app/
βββ node_modules/ # Dependencies (auto-generated)
βββ public/
β βββ index.html # Main HTML file
β βββ favicon.ico # Favicon (optional)
βββ src/
β βββ index.js # Entry point
β βββ App.js # Main application component
β βββ components/ # Folder for reusable components
β β βββ Header.js # Example component
β β βββ Footer.js # Example component
β βββ styles/ # Folder for CSS or SCSS files (optional)
β β βββ App.css # Styles for App.js
β βββ assets/ # Folder for static assets like images (optional)
β β βββ logo.png # Example image
βββ package.json # Project dependencies and scripts
To integrate this into a monorepo, we need to make it use the Node.js version of the repository and the repositoryβs dependency module. Then, we need to tell Bazel how to build the app.
By the way, do you know how a React app is built? π§
First, the source files are transpiled to a basic JavaScript version using tools like Babel. Then, the source needs to be bundled into a format that can be executed on the intended platform, such as a browser. The executable for the browser is an HTML file with JavaScript code. So, our bundle will simply consist of:
build/
βββ index.html # Main HTML file
βββ main.js # Minified and bundled JavaScript file
The index.html
file will look like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My React App</title>
</head>
<body>
<div id="root"></div>
<script src="main.js"></script>
</body>
</html>
To achieve this, we use bundlers like Webpack, ESBuild, or Rust.
Now, letβs configure Bazel to build our app while keeping a few things in mind:
- Take advantage of what Bazel offers.
- Avoid reinventing the wheel.
First, letβs register the global, constant, hermetic toolchain in :
####### Node.js version #########
# By default, you get the node version from DEFAULT_NODE_VERSION
# in @rules_nodejs//nodejs:repositories.bzl
# Optionally, you can pin a different node version:
bazel_dep(name = "rules_nodejs", version = "5.8.2")
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(node_version = "16.14.2")
#################################
To test if Bazel is using the given version, we can create a hello.mjs
file and run it with js_binary
like this:
// hello.mjs
console.log(process.version);
js_binary(
name = "hello",
entry_point = "hello.mjs",
)
Great! Now letβs configure the dependency manager:
npm = use_extension("@aspect_rules_js//npm:extensions.bzl",
"npm", dev_dependency = True)
npm.npm_translate_lock(
name = "npm",
bins = {
"react-scripts": [
"react-scripts=./bin/react-scripts.js",
],
},
data = [
"//:package.json",
"//:pnpm-workspace.yaml",
"//:packages/my-app/package.json",
],
npmrc = "//:.npmrc",
pnpm_lock = "//:pnpm-lock.yaml",
verify_node_modules_ignored = "//:.bazelignore",
update_pnpm_lock = 1,
)
use_repo(npm, "npm")
rules_js
relies on pnpm-lock.yaml
.
Run this commende to generate the pnpm lock file:
bazel run -- @pnpm//:pnpm --dir $PWD install --lockfile-only
So, now we have the Node.js version and the dependencies configured. how and where do we define the build rules for Bazel?
for every packages or app in order to build it ,we need to define the BUILD file in the root of this app with the right bazel rule to build
for CRA app βaspectβ team already has writen a rule that wrap βreact-scriptβ to build a CRA
it is loaded from load("@npm//:react-scripts/package_json.bzl", cra_bin = "bin")
:
cra_bin.react_scripts(
# Note: If you want to change the name make sure you update BUILD_PATH below accordingly
# https://create-react-app.dev/docs/advanced-configuration/
name = "build",
srcs = CRA_DEPS,
args = ["build"],
chdir = package_name(),
env = {"BUILD_PATH": "./build"},
out_dirs = ["build"],
)
@npm//:react-scripts/package_json.bzl
this is called a Bazel labels.
one think to note is that this bazel rule use pnpm
so in package.json
need to fix peer dependency:
"pnpm": {
"//packageExtensions": "Fix missing dependencies in npm
packages, see https://pnpm.io/package_json#pnpmpackageextensions",
"packageExtensions": {
"postcss-loader": {
"peerDependencies": {
"postcss-flexbugs-fixes": "*",
"postcss-preset-env": "*",
"postcss-normalize": "*"
}
}
}
}
Feel free to comment below if you have any thoughts or questions. Your input is highly appreciated!