iJS CONFERENCE Blog

Angular <3 Bazel:

A new star is born?

Jan 16, 2020

With Bazel, a new build tool has emerged in the already rich universe of developer tools: A rising star on the horizon that we have all been waiting for. Uh, have we?

Bazel promises a new, better and faster way to build a project. It doesn’t necessarily have to be an Angular or TypeScript project, because Bazel acts independently of the programming language used and is even able to combine different languages in one build. However, these are promises that developers have quite often heard from other tools, so they might just cause a bit of skepticism. After all, which tool out there would not claim to be the best and easiest to use? This is probably the reason why many developers don’t really feel like looking into the topic, quite understandably. Especially in the field of JavaScript there are already lots and lots of tools and aids and development of new ones happens pretty rapidly. Taken together, that might cause a certain fatigue for developers. But with Bazel it’s a bit different and the tool is well worth a look. Let’s start with a short history:

Bazel has been available as open source software since March 2015 and has developed from Google’s internal tool Blaze. Blaze itself has been successfully used internally by Google for over 10 years. In the end, they didn’t want to withhold it from the community anymore and therefore published the essential parts of it. It follows that Bazel is already a very stable and mature tool.

How does Bazel help you?

Anyone who has ever tried to build or extend Angular source code and run some tests afterwards knows how tedious and lengthy this is. Bazel can solve this problem for you. It has reduced the test execution time from one hour to 15 minutes. How does Bazel do that? Having been developed by Google, it is naturally well-adapted to Google’s conditions, meaning it is fit to deal with huge code repositories where every application and its dependencies are built from actual source code. Bazel achieves the promised speed with the help of incremental builds, caching and parallelization of builds. It’s worth to mention that these builds can also be executed remotely in a so-called build cloud, where the load can be distributed across multiple build servers. The result can then be reused by all developers via caching mechanisms.

What’s so special about Bazel?

Getting started with Bazel, the learning curve can be quite steep at first. What is Bazel even? Is it something in the same line of thought as webpacks or maybe a substitute to a Jenkins build server? Well, actually it’s none of that. Bazel neither replaces webpacks nor a build server. Bazel is what we call a build tool. Opposite to it are the dev tools, including webpacks. Figure 1, having been shown by Alex Eagle (Mr. Bazel) during several talks about Bazel, helps enormously in understanding it.


Bazel compared to other tools

For a couple of years so-called dev tools have gained a lot of popularity. For example, attempts have been made to map the entire build in a webpack configuration, rendering tools like Gulp or Grunt less and less important. The intention to save developers some tools is a noble and understandable one, but unfortunately, we are actually generating more complexity than originally intended. Why is that? Figure 2 illustrates the problem.


M-x-N-Plug-in-Matrix

Do you see what’s going on? Each combination of tool and library now requires an own plug-in, so we end up in a plug-in jungle, where each plug-in uses a different set of configurations that now need to be mastered. Do you also have the urge to escape from such a configuration hell? With Bazel, each plug-in only needs to be developed exactly once. Bazel offers support for that by providing a readable language for configuring our builds. Through this unified language, Bazel manages to create uniform configurations that operate independently of the dev tools and programming languages used behind them. The inputs of a configuration and the outputs of a build are thus clearly defined, making it easier for different plug-ins to work together (Fig. 3). This can drastically decrease the number of plug-ins.


Bazel‘s Plug-in-System

Starting a new project with Angular and Bazel

To create a new project with Angular and Bazel, Bazel must first be globally installed via npm. You can use the following command: npm i -g @angular/bazel. After that, use the Angular CLI command to create a new project with Bazel as build tool: ng new –collection=@angular/bazel. Looking at the workspace of the created project, at first glance you can’t see any difference compared to a normal Angular CLI project (Fig. 4), just as intended. This way we can use familiar commands like ng serve, ng build and ng test as we did before switching to Bazel.


Angular Builders API abstracts over Bazel’s usage

Angular Builders, introduced in Angular 8, form an abstraction layer and hide the actual build tool that is used. You need to go to the angular.json file to find a reference to the Bazel command. If we now use ng serve to build our project for the first time, it will take some extra time. But for all of the following builds you won’t notice any difference to the usual build times because of the incremental build.

Adding Bazel to existing projects

Bazel can be added easily to an existing Angular CLI project: ng add @angular/bazel. Use a git status command to take a closer look at all the changes made to the project in this case. However, the specific Bazel configuration files are still hidden from us in this mode and only actually generated at runtime. But since we are most interested in these files when we want to get to know Bazel, we can extract them with the following command: ng build -leaveBazelFilesOnDisk. After that we should find files named WORKSPACE and BUILD.bazel in the root folder, just as we did with src/BUILD.bazel.

However, at the moment we can only successfully build an untapped Angular CLI project. As soon as we add a new dependency to the project, even if it is only an import to the FormsModule, we already have to adjust the Bazel configuration manually. Of course, the Angular CLI will do this at some point in the future, but it isn’t ready to take over these tasks yet. Nevertheless, it does not hurt to learn the basics of Bazel, and the manual changes are not too difficult anyway.

Bazel’s language

In order to be able to really “go bazeling”, we must first clarify a few terms. A Bazel workspace is identified by the WORKSPACE file and located in the main directory of the project, where the files to be built are located and Bazel stores its build results. This workspace can now be further divided by packages. Each directory containing a BUILD file represents a separate package. It makes sense to define several packages, because Bazel only rebuilds the packages where changes have actually been made. The BUILD file itself contains a description of how the package should be built. In Bazel’s language, these are called rules. The rules are first loaded into the BUILD files and then executed. A rule now describes how the desired output files are created from the defined input files. The goal of Bazel is that the BUILD files completely describe the inputs and outputs of the build process, which allows a more detailed analysis of the build artifacts and their changes.

Let us now take a closer look at an example rule located in the generated BUILD file in the src folder of our project and was created by switching to Bazel (Listing 1).

Listing 1: Bazel Rule

// 1. Rules must to be loaded first
load("@npm_angular_bazel//:index.bzl", "ng_module")

// 2. execution of the ng_module rule 
ng_module(       
  name = "src",
  srcs = glob(  
    include = ["**/*.ts"], // Input-Files
    exclude = [
      "**/*.spec.ts",
      "main.ts",
      "test.ts",
      "initialize_testbed.ts",
    ],
  ),
  assets = glob([
    "**/*.css",
    "**/*.html",
  ]) + ([":styles"] if len(glob(["**/*.scss"])) else []),
  deps = [ 
    "@npm//@angular/core",
    "@npm//@angular/platform-browser",
    "@npm//@angular/router",
    "@npm//@types",
    "@npm//rxjs",
    // additional dependencies on libraries can be added here:
    // e.g.: @npm//@angular/forms
  ],
)

What exactly does the ng_module rule from our example do now? It is provided by Angular and is responsible for calling the Angular AOT template compiler. It extends the ts_library rule, which in turn wraps the TypeScript compiler. When calling it, the TypeScript source code is compiled in JavaScript and all Angular compilation steps are executed as well. The glob() function is only a helper function that is used wherever lists of file names are expected. It allows the use of so-called wild card patterns, such as **/*.css. Also noteworthy is that line: ([“:styles”] if len(glob([“**/*.scss”])) else []), referring to the rule created in the same file with the name styles, but only if there are SCSS files that need to be compiled in CSS. This is exactly what the styles rule does.

Summary

Even though Bazel is designed for large monorepos like the ones used by Google, it can make sense to switch to Bazel even for smaller projects in order to reduce build and development times. However, this should only be considered once Bazel is fully integrated into the Angular CLI, thus minimizing the manual configuration effort. For those who want to delve even deeper into the subject, I recommend reviewing the Angular Bazel Example.

Sign up for the iJS newsletter and stay tuned to the latest JavaScript news!

 

BEHIND THE TRACKS OF iJS

JavaScript Practices & Tools

DevOps, Testing, Performance, Toolchain & SEO

Angular

Best-Practises with Angular

General Web Development

Broader web development topics

Node.js

All about Node.js

React

From Basic concepts to unidirectional data flows