Angular 6 has been released few days ago, bringing plenty of great new features, such as Angular Elements or the Angular CDK.

But today I would like to talk about an other feature called Bazel, which been disclosed at Ng-confs and in few blog articles, and which I thought might be part of Angular 6. Since this tool has not been mentioned at all in the official release, I guess it will be announced in Angular 7. Anyways, Bazel is already functional and supported by the framework (the team started to look for early-adopters to test the tool), so we can already have a look atit.

Bazel, build apps like at Google:

Bazel is part of a general effort called ‘ABC’ from Google to improve building and testing on Angular apps. Indeed, ABC stands for:

- (Angular BootCamp)

- Angular with Bazel and Closure (2017)

- Angular Buildtool Convergence (2018) // relying on Bazel and Closure

In addition to being an easy to remember acronym, ABC brings serious innovation to Angular. Originally, the rationale behind Bazel and Closure (which are not restricted to Angular) is to open-source and share some tools and processes used internally at Google: for instance, Bazel is the open-source version of Blaze, a tool developed by Google a few years ago to handle the build of its largest apps. With ABC, Google is extending this effort to the Angular framework.

What does Bazel do ?

So, basically, Bazel optimises your Angular compilation 🥁 Maybe, at first glance, Angular compilation might not be a top issue. But, as soon as your app becomes larger, it turns out to be a real (and irritating) problem. And also, this is a Google tool 🤘

The problem :

To understand what issue Bazel is attempting to solve, let’s inspect what happens when we ng-serve on a basic “hello-world” app:

ng serve — first compilation

As we can see, Angular compiler builds 5 chunks. The most important ones are:

- vendor.js, which bundles all the dependancies

- main.js, which contains the app

Let’s now make a micro-change, such as changing our traditional “Hello world” message:

Hello from Angular !

And then ng-serve again:

What happened ? As we can see, the whole main.js bundle has been rebuilt, even though we changed only1 line of code. This operation took 285ms, and required to:

calling the typescript transpiller to check the type compatibilities (which goes, for each type declaration, through around 20 000 lines of code from the standard typescript library, a lib.d.ts file) and transforms the code into POJ (Plain Old Javascript) by converting or removing all Typescript features, such as classes, interfaces, decorators, etc.

to check the type compatibilities (which goes, for each type declaration, through around from the standard typescript library, a lib.d.ts file) and transforms the code into POJ (Plain Old Javascript) by converting or removing all Typescript features, such as classes, interfaces, decorators, etc. calling the bundler (usually Webpack) to build the main.js file with the Javascript outputs, the html templates and css files

Remark:

The process I shortly described matches a JIT (Just In Time) compilation, which is generally used in dev mode.

In JIT, the main build tool, the Angular compiler, doesn’t intervene: instead, it is bundled in the vendor file, and will do the last compilation step (building ng factories files, which enable the browser to instanciate the component classes) directly in the client’s browser at the run-time.

In production mode, the AOT (Ahead Of Time) compilation will be used. AOT compilation calls the Angular compiler, which builds pre-compiled files (the ‘ng-factories’ files). The Angular compiler is therefore not shipped to the browse, instead the pre-compiled files will be directely included in the main.js bundle.

More precision on this in the next paragraph.

This is of course not a big issue for a hello-world (or a hello-angular) app. But this quickly starts to be irritating when the app grows, and can turn to bring real issues on large apps. For example, according to Google, their own Cloud Console application involves more than 1 500 Angular sources and 6 500 Typescript files: with this scale, rebuilding the full application on each change is absolutely not workable.

Bazel :

The solution found by Google relies on the concept of incrementality, where:

“time to rebuild project is proportional to the changes, not to the size of the project”

- Alex Eagle, Angular Core Team

Brief history :

Actually Bazel is not that new. As I mentioned, Google faced early this problem:

“ A long time ago, Google built its software using large, generated Makefiles. These led to slow and unreliable builds, which began to interfere with our developers’ productivity and the company’s agility. Bazel was a way to solve these problems.”

- https://bazel.build/faq.html

Therefore they started developping Bazel, but under another name, Blaze, which has actually been used now for more than 10 years at Google. Later, Google decided to open-source the building tool, and released a first beta version of Bazel in march 2015, which was primarily focused on C++, Java and Go. Then they gradually expanded it to other languages, including Javascript and Typescript.

Afterwards, the Angular team implemented Bazel to develop the framework (Angular itself). The tool was used internally until Brad Green unveilled, in the ng-conf 2017 keynote, the project to share the tool with the Angular community , by implementing it as the default Angular building tool.

And indeed, now Angular (since version 5 actually) supports the use of Bazel — here is the official demo.

But this is still quite experimental. It seems the team is currently working closely with test-users, so we can probably expect a stable integration for mid 2018, and maybe an integration in the CLI for end of 2018.

General concepts about Bazel:

Language agnocity :

Before tackling the key concepts of how Bazel works, one thing needs to be precised: Bazel is language agnostic, because it has been designed to be able to support different languages, even in the same project.

Therefore, it is fundamentally different from building tools such as Typescript or Angular compilers, which have been developped to compile Typescript or Angular files. For Bazel, we have to teach the software what to do and how to do it.

How to do: the Bazel team created “rules” (we will talk more of this concept later) which have to be imported at the root of the project (in a file called “workspace”), which teaches Bazel how to compile and build the files.

Ex: to enable Bazel to transpile ts into js, we need to import the “”build_bazel_rules_typescript”, which can be imported from the official bazel repository.

Nb: I think this part will probably be probably be automated when Bazel will be integrated into the CLI.

Functionning principle:

The core idea behind Bazel relies on:

splitting a project into “packages” (units which can be compiled independently). A directory is transformed into a package by adding a “build” file at its root level, which holds informations about the package (we will come back on this later).

Nb: a package can contain regular files, but also (sub)packages. Excepted “rules” files, files, directories and packages inside a package are called “targets” .

(units which can be compiled independently). A directory is transformed into a package by adding a file at its root level, which holds informations about the package (we will come back on this later). Nb: a package can contain regular files, but also (sub)packages. Excepted “rules” files, files, directories and packages inside a package are called . drawing an accurate dependancies graph listing which dependancies (which actually are packages) are imported by which packages

=> on rebuild, Bazel knows which packages might need to be recompiled (only the modified packages + those relying on a modified dependance)

listing which dependancies (which actually are packages) are imported by which packages => on rebuild, Bazel knows which packages might need to be recompiled (only the modified packages + those relying on a modified dependance) determining if a change is internal to a package: indeed, depending on if the export API of a package has been modified or not, dependant packages may need or not need to be rebuilt.

For instance, let’s make a change in a library:

- if we add a private helper function somewhere, to make our code more readable, we won’t need to update the packages importing the library

→ this is internal change: we don’t need to rebuild any other package than the library

- if now we change the name of an exported method, we will need to refactor the code of packages importing the method

→ this is a change in the API of the package: any package importing the method needs to be rebuilt

Here is a very clear post to better understand this concept: trimming your (build) tree.

Structure of a Bazel directory:

Basically, the Bazel structure (and the points above) relies on 2 types of files, which we quickly mentioned already:

Workspace file (directory scope):

Located at the very top-level of the directory, there is only one Workspace file per project, which has two purposes:

→ defining, as its name implies, the workspace (by defining the root of the directory)

→ teaching Bazel how to do its job (remember that Bazel is “language agnostic” ), by importing some modules called rules configuring Bazel to do the relevant compilations. The rules are made by the Bazel team, and can be imported directly from Bazel’s Github repository.

Nb: see the next paragraph for an example of rule import

Located at the very top-level of the directory, there is only one Workspace file per project, which has two purposes: → defining, as its name implies, the workspace (by defining the root of the directory) → teaching Bazel how to do its job (remember that Bazel is ), by importing some modules called rules configuring Bazel to do the relevant compilations. The rules are made by the Bazel team, and can be imported directly from Bazel’s Github repository. Nb: see the next paragraph for an example of rule import Build files (package scope) :

Located at the root of packages, build files allow:

→ splitting the project into units: 1 build file at top root of each package)

→ for each unit, teaching Bazel what to do by importing the ‘rules’ -what tells Bazel what to do- from the workspace file (so each unit is, on the compilling side, independant from parent units)

→ listing all the dependancies per package

Anatomy of an Angular-Bazel directory:

Introduction

All the code below is directly extracted for the Github repository of the official demo, by Alex Eagle.

Nb: the Bazel files are written in Bazel’s own language, called Skylark and inpired by Python. You can find more information about the syntax here: link to the documentation.

Workspace file :

It is good practice to start with giving a name to the workspace, with the Bazel native function workspace():

workspace(name = "angular_bazel_example")

Then, for an Angular project, we need to fetch the following modules:

Typescript rules

NodeJS rules, which enables Bazel to import Node packages, needed to:

→ import Angular package

→ import additional packages, such as RxJS for example

→ import Angular package → import additional packages, such as RxJS for example Complementary rules: for example, the official demo also fetches the sass rule

Here is an example of how Bazel fetches a rule module:

####################################

# Fetch and install the Sass rules

git_repository(

name = "io_bazel_rules_sass",

remote = "https://github.com/bazelbuild/rules_sass.git",

tag = "0.0.3",

) load("@io_bazel_rules_sass//sass:sass.bzl", "sass_repositories") sass_repositories()

####################################

# Fetch and install the TypeScript rules

http_archive(

name = "build_bazel_rules_typescript",

url = "https://github.com/bazelbuild/rules_typescript/archive/0.12.3.zip",

strip_prefix = "rules_typescript-0.12.3",

sha256 = "967068c3540f59407716fbeb49949c1600dbf387faeeab3089085784dd21f60c",

) load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace") ts_setup_workspace()

As we can see, fecthing and installing takes place in 3 steps:

fetching the modules: Bazel uses two native rules , http_archive() and git_repository() . The first one downloads a file, decompresses it if needed and makes its targets (which are actually new rules) available to be imported from anywhere in the project.

Nb: the usage of http_archive() is recommended over git_repository() .

, and . The first one downloads a file, decompresses it if needed and makes its targets (which are actually new rules) available to be imported from anywhere in the project. Nb: the usage of is recommended over . importing the installation rules, with an other native rule, load() . The API is quite straightforward: the names of the module to execute (defined previously by parameters in the fetching rule) and of the rule to import are simply passed as argument.

. The API is quite straightforward: the names of the module to execute (defined previously by parameters in the fetching rule) and of the rule to import are simply passed as argument. executing the installation rules, and/or adding external repositories

For information, the fetch of NodeJS rule requires an additional step: informing Bazel of the npm packages to be downloaded (which will, by the way, also be automatically included by Bazel in the package.json), which is done with the native rule local_repository().

####################################

# Fetch and install the NodeJS rules

http_archive(

name = "build_bazel_rules_nodejs",

url = "https://github.com/bazelbuild/rules_nodejs/archive/0.7.0.zip",

strip_prefix = "rules_nodejs-0.7.0",

sha256 = "d0cecf6b149d431ee8349f683d1db6a2a881ee81d8066a66c1b112a4b02748de",

) load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories") node_repositories(package_json = ["//:package.json"])

####################################

# Tell Bazel about some workspaces that were installed from npm.

local_repository(

name = "angular",

path = "node_modules/@angular/bazel",

)

local_repository(

name = "rxjs",

path = "node_modules/rxjs/src",

)

You can find all the native Bazel rules for Workspace here in the official documentation.

Build files:

As we mentionned earlier, build files purpose is to configure the packages. There is one Build file per package (excepted if the package contains nested packages, which will also have their own build files).

Recommended structure of a build file:

Loading statements : loads determined rules from the modules fetched in workspace. Here is the syntax of a load statement: load(“@nameOfModule//path/to/file:target”, “nameOfRule”)

Nb: in Skylark, Bazel’s language, a target is:

→ any file in a package

→ a package group (which we will see it later)

load(“@angular//:index.bzl”,“ng_module”),

# loading the 'ng_module' rule from the index.bzl file located at the root of Angular node package load(“@build_bazel_rules_typescript//:defs.bzl”, “ts_library”, “ts_web_test”)

# loading the 'ts_library', 'ts_web_test' rules from the defs.bzl file located at the root of the Typescript module

Package() function : package() adds default meta-data to the rules declared in the package:

package(default_visibility = ["//visibility:public"]) # Means that the rules declared in this Build file can be used by

# package (inside or outside the current package)

Configuration of the rules:

# After having imported the ng_module rule: ng_module(

name = "src",

srcs = glob(["*.ts"]),

# glob() fetches all files matching the expression

# passed as argument

tsconfig = ":tsconfig.json",

# tsconfig.json has been globally exported

# we will see this in a moment

deps = ["//src/hello-world"],

)

Example : overview of the demo app build files structure

In this section I listed the build files of the official demo app, with their main functionnalities explained:

root/BUILD.bazel : mainly declares package groups of targets with the native function filesgroup(). Package grouping assembles some package targets under a single label, which can be used later in nested packages.

Nb: I think at least part of the files-groupings done here might probably be automised when Bazel will be integrated into the Angular CLI.

filegroup(

name = "angular_bootstrap_scripts",

# do not sort

srcs = [

"//:node_modules/zone.js/dist/zone.min.js",

"//:node_modules/zone.js/dist/async-test.js",

"//:node_modules/zone.js/dist/sync-test.js",

"//:node_modules/zone.js/dist/proxy.min.js",

"//:node_modules/zone.js/dist/jasmine-patch.js",

],

)

root/src/BUILD.bazel : configures the ng_module rule of this package, and other rules needed for the compilation of the project, such as the dev-server or buldles.

Nb: You can notice the use of some group-files labels declared in the previous build file, which can be reached with the standard target syntax: “//:angular_bundles”



load("

ng_module(

name = "src",

srcs = glob(["*.ts"]),

tsconfig = ":tsconfig.json",

deps = ["//src/hello-world"],

) # ng_module configurationload(" @angular //:index.bzl", "ng_module")ng_module(name = "src",srcs = glob(["*.ts"]),tsconfig = ":tsconfig.json",deps = ["//src/hello-world"],

load("

ts_devserver(

name = "devserver",

entry_module = "angular_bazel_example/src/main",

scripts = ["//:angular_bundles"],

serving_path = "/bundle.min.js",

static_files = [

":zone.js",

"index.html",

],

deps = ["//src"],

) # dev server configurationload(" @build_bazel_rules_ty pescript//:defs.bzl", "ts_devserver")ts_devserver(name = "devserver",entry_module = "angular_bazel_example/src/main",scripts = ["//:angular_bundles"],serving_path = "/bundle.min.js",static_files = [":zone.js","index.html",],deps = ["//src"],

root/src/hello-world/BUILD.bazel :

- ng_module rule

- additionnal rules (sass) / librairies (lib.ts)

Nb: Notice that we don’t need to redeclare the compilling rules configured in the previous Build files, since we are using the same rules.

Nb (2) : since the Module has been defining as the compiling unity, don’t forget that for Bazel to work, we need to add a new module in each new package, here: src/hello/hello.module.ts.



load("

load(" load(" @angular //:index.bzl", "ng_module")load(" @io_bazel_rules_sass //sass:sass.bzl", "sass_binary")load(" @build_bazel_rules_ty pescript//:defs.bzl", "ts_library", "ts_web_test") sass_binary(

name = "hello-world-styles",

src = "hello-world.component.scss",

deps = [

"//src/shared:colors",

"//src/shared:fonts",

],

)

name = "hello-world",

srcs = [

"hello-world.component.ts",

"hello-world.module.ts",

],

assets = [":hello-world-styles"],

tsconfig = "//src:tsconfig.json",

deps = [

"//src/lib",

"

],

) ng_module(name = "hello-world",srcs = ["hello-world.component.ts","hello-world.module.ts",],assets = [":hello-world-styles"],tsconfig = "//src:tsconfig.json",deps = ["//src/lib", @rxjs ",], ts_library(

name = "test_lib",

testonly = 1,

srcs = glob(["*.spec.ts"]),

deps = [":hello-world"],

)

Conclusion:

I hope this article could give you the basis to get started with Bazel. As a conclusion, I would like to put Bazel into perspective with the other features recently released with Angular 6. Quoting the official announcement,

This is a major release focused less on the underlying framework, and more on the toolchain and on making it easier to move quickly with Angular in the future. As a part of this release, we are synchronizing the major versions going forward for the framework packages ( @angular/core , @angular/common , @angular/compiler , etc), the Angular CLI, and Angular Material + CDK.

The emphasis on the toolchain, implemented by the new functionalities of the CLI, shows, in my opinion, the intention of the Angular team to give the framework serious advantages in efficiency and time-saving during development, especially for large apps, which is perfectly consistent with the implementation of Bazel.