Angular 8 - Bazel Walkthrough

Bazel is Google’s open-source part of its internal build tool called Blaze. Learn how and why you should use it.

One of the features that I am really excited about in Angular 8 is Bazel. Bazel is Google’s open-source part of its internal build tool called Blaze. The biggest selling point for this tool is that it is capable of doing incremental builds and tests!

Incremental build means that it will only build what has changed since the last build. Bazel does this by building a task graph based on the inputs and outputs set throughout the application and evaluating which ones need a rebuild. Therefore, the first build will take some time, but the subsequent builds should take much less time. This enables the application to scale without affecting the build times too much.

Bazel supports multiple languages and platforms and makes it possible to build full-stack applications using the same tool.

Bazel also comes with a cool feature called Remote Build Execution.

Remote execution of a Bazel build allows you to distribute build and test actions across multiple machines, such as a datacenter.

This enables faster build and test execution by leveraging the scalability of cores for parallel execution and since this is done remotely, builds can be reused across the team.

Angular 8 provides an opt-in preview mode for Bazel, fingers crossed, we might get it in Angular 9!

Before we start — When building NG apps, it’s better to share your components in a reusable collection, so you won’t have to rewrite them.

Using tools like Bit (GitHub) you can instantly share components (automatically isolated with dependencies) into a shared hub, use and develop them anywhere, sync changes and build multiple apps faster.

Bazelifying your app

Let’s walk through making an existing basic application use Bazel for its build.

You can find the final solution here which uses Bazel for build, for your reference.

1. Install Bazel Dependencies

The first step is to add dependencies needed to build with Bazel.

You can use the below command which will add all the necessary dependencies and also add/modify some files necessary for Bazel build.

yarn ng add @angular/bazel

With this done, whenever we run Angular CLI build commands ( ng build or ng serve ) on the application, it will be using Bazel under the hood.

Let’s have a look at the list of files that are added/changed:

angular-metadata.tsconfig.json

Angular libraries do not come with the NgFactory files such as ngfactory.js, ngsummary.js etc. Since these files are needed for the AOT compilation, we add @angular libraries to the include list in angular-metadata.tsconfig.json , for which the NgFactory files are generated in the postinstall step (see below under package.json).

{

"compilerOptions": {

"lib": [

"dom",

"es2015"

],

"experimentalDecorators": true,

"types": [],

"module": "amd",

"moduleResolution": "node"

},

"include": [

"node_modules/@angular/**/*"

],

"exclude": [

"node_modules/@angular/bazel/**",

"node_modules/@angular/**/schematics/**",

"node_modules/@angular/**/testing/**",

"node_modules/@angular/compiler-cli/**",

"node_modules/@angular/common/upgrade*",

"node_modules/@angular/router/upgrade*"

]

}

You need to add any 3rd party libraries that do not come with the NgFactory files to the include list in angular-metadata.tsconfig.json , otherwise you might end up getting some errors related to the 3rd party library not being available.

angular.json

Changes are made to angular.json to update the builder to be using Bazel and the corresponding build options.

angular.json.bak

A new file called angular.json.bak is created which contains the previous angular.json file contents backed up in it. This is so if you want to opt out from using Bazel, you can restore this file for angular.json .

e2e/protractor.on-prepare.js

This has a script to prepare Protractor for e2e project



const protractor = require('protractor'); const protractorUtils = require(' @angular/bazel /protractor-utils');const protractor = require('protractor');

const portFlag = /prodserver(\.exe)?$/.test(config.server) ? '-p' : '-port';

return protractorUtils.runServer(config.workspace, config.server, portFlag, [])

.then(serverSpec => {

const serverUrl = `

protractor.browser.baseUrl = serverUrl;

});

}; module.exports = function(config) {const portFlag = /prodserver(\.exe)?$/.test(config.server) ? '-p' : '-port';return protractorUtils.runServer(config.workspace, config.server, portFlag, []).then(serverSpec => {const serverUrl = ` http://localhost:${serverSpec.port}` protractor.browser.baseUrl = serverUrl;});};

package.json

A post install step is added under scripts which is used to generate the NgFactory files for the libraries that do not ship with them.

"postinstall": "ngc -p ./angular-metadata.tsconfig.json"

Dependencies needed to build using Bazel are added to package.json .

"@angular/bazel"

Provides a builder that allows Angular CLI to use Bazel @angular/bazel:build as the build tool.

"@bazel/bazel"

Is the Bazel build tool.

"@bazel/hide-bazel-files"

Some packages may be shipped with Bazel files with their npm package, @bazel/hide-bazel-files automatically runs a post install script that renames the Bazel files that come with the npm packages. Ideally the npm packages do not ship with Bazel files, but in the interim, this should do the trick.

"@bazel/ibazel"

A file watcher for Bazel to watch for any changes made and automatically trigger build or test run upon save.

"@bazel/karma"

Contains the rules to be able to run karma tests with bazel.

"@bazel/typescript"

Contains the rules to integrate the TypeScript compiler with Bazel.

src/initialize_testbed.ts

This has a script to initialise a TestBed before the tests are run.



import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from ' import {TestBed} from ' @angular/core /testing';import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from ' @angular/platform-browser-dynamic /testing'; TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());

main.dev.ts & main.prod.ts

Two files are created - main.dev.ts and main.prod.ts replacing main.ts file, so it is more explicit and lets you add any different configurations that are specific to the two modes in the respective files. If you want to switch back to @angular-devkit/build-angular , you can just switch to using the main.ts , otherwise if you are never going to switch back, you could just delete the main.ts file.

Also, code splitting is not yet supported in dev mode, so we don’t get nice code split bundles on devserver.

src/rxjs_shims.js provides named UMD modules for `rxjs/operators` and `rxjs/testing` so the application can be bundled with rxjs .

2. Initialise Build Files for Bazel

The next step is to create the initial Bazel configuration files and build using Bazel:

yarn ng build

This will build your application, figures out the dependencies, creates Bazel build files in memory and once it is done, removes all the Bazel files that it creates in memory.

Here, yarn ng build uses Bazel by default because when we did yarn ng add @angular/bazel in the first step, it replaces the Angular CLI build commands ( build , serve , etc.) to be using Bazel under the hood.

3. Run the Application

Let’s run the application!

To run dev server:

yarn serve

To run prod server:

yarn serve --prod

This will build the application using Bazel and spin it up.

4.Customisation

If you want to customise the build further, you can the same command with --leaveBazelFilesOnDisk and this will leave the files created during build process so you can customise the build to suit your needs.

yarn ng build --leaveBazelFilesOnDisk

This creates and leaves behind the below files:

.bazelignore

Excludes directories/sub-directories from WORKSPACE, in this case, we do not want dist and node_modules to be included in the WORKSPACE.

dist

node_modules

.bazelrc

Configuration for Bazel tool for the application, here you can customise the build tool to suit your needs.

Bazel files

Before we go into Bazel files, let’s go through the nomenclature of Bazel files for a better understanding.

Bazel files use Starlark language which is a subset of Python. Most of the configuration needed to build your application using Bazel lies in 2 files called WORKSPACE and BUILD.bazel.

WORKSPACE tells Bazel how to download external dependencies needed to build with Bazel. One workspace file per organization/monorepo containing multiple related applications/libraries inside it.

BAZEL.build tells Bazel about the source code, its dependencies, dev server, prod server and more. Each level that has a BUILD.bazel file is called a package.

In the above example, there are 4 packages: src , app , page-one and page-two .

Build.bazel files are comprised of files andrules. Files could be either files that are checked into the repository (Source files) or files that are generated as a result of the rules (Generated files). Rules specify the steps need to create output based on the input and dependencies. A target is generated by calling a rule. So in the below example, sass_binary is a rule and calling it sass_binary(name=”global_stylesheet”) produces a target.

BUILD.bazel

Bazel sandboxes each package, so unless explicitly specified, they cannot access files or rules not declared in their BUILD.bazel .

package(default_visibility = ["//visibility:public"]) exports_files([

“tsconfig.json”,

])

visibility allows the rules in the current package to be accessed from other packages.

export_files lets files that are not already mentioned in the current packages BUILD.bazel file to be used by other packages. If a file in a package is not mentioned anywhere in its BUILD.bazel file, it can’t be accessed by other packages. In this case, since the BUILD.bazel file does not have anything yet but we still want other packages to access tsconfig.json file located at root, we use export_files to exposed it.

src/BUILD.bazel

Only one BAZEL file is created for the whole application and includes all the files that need to be built in one file (using glob wildcarding). If you want to optimise your build which is more likely to be the case in a real-world application, you can customise it further by adding more fine grained BAZEL files at each level of your application.

Let’s walkthrough the contents of the BUILD.bazel file at src level:

package(default_visibility = ["//visibility:public"])

Sets the visibility of the current package to be public so the other packages can access this package for build.

This is the Starlark way of importing libraries. We will go through the significance of each of the imports below:

sass_binary(

name = "global_stylesheet",

src = glob(["styles.css", "styles.scss"])[0],

output_name = "global_stylesheet.css",

)

sass_binary compiles SASS file into a CSS file by specifying the file that needs to be compiled in src and the name you would like for the output CSS file generated. If you do not specify the output name, it will take the name of the input file specified by default. name field indicates the name for this particular rule.

src = glob([“styles.css”, “styles.scss”])[0] tells the compiler to pick the first file of either styles.css or styles.scss . This is more of a generic rule that will make it work if you use CSS or SCSS in your application. You can just change it to src = “styles.css” if you are using CSS.

If your SASS file has dependencies on other files, you can use the sass_library rule instead.

multi_sass_binary(

name = "styles",

srcs = glob(

include = ["**/*.scss"],

exclude = ["styles.scss"],

),

)

multi_sass_binary lets you build multiple SASS files in one rule. You can specify the list of files that need to be built using glob in the include list. You can also specify the list of files that you don’t want to be included in exclude .

In this case, we are telling it to build all files except the styles.scss since we have already included that in the previous sass_binary rule.

Rule to build the current package with options to specify the TypeScript files, stylesheets and templates that need to be compiled and their dependencies.

For inputs, under src we have a glob that includes all the TypeScript files in this directory and its sub directories. Test related files such as spec.ts test.ts and initialize_testbed.ts are excluded from the compilation as we dont’t want to build them. We also exclude main.ts since we won’t be using it for Bazel build.

Under assets , we have all the css files and html files plus if there are any SCSS files, we include styles rule here as it includes all the SCSS files in it as mentioned above. : preceded by the file or rule name is how we access them.

This way we can have rules as a dependency which would then go to that rule and gets the compiled output for it.

Under dependencies, we have all the libraries that the code in this package depends on. Without this, the package will not know what it needs to load for a particular library during build.

Prod server

Prod server setup consists of 3 things: rollup, web package and a server.



name = "bundle",

entry_point = ":main.prod.ts",

deps = [

"//src",

"

"

],

) rollup_bundle(name = "bundle",entry_point = ":main.prod.ts",deps = ["//src", @npm //@angular/router", @npm //rxjs",],

name = "prodapp",

assets = [

"

":bundle.min.js",

":global_stylesheet",

],

data = [

"favicon.ico",

],

index_html = "index.html",

) web_package(name = "prodapp",assets = [ @npm //:node_modules/zone.js/dist/zone.min.js",":bundle.min.js",":global_stylesheet",],data = ["favicon.ico",],index_html = "index.html", history_server(

name = "prodserver",

data = [":prodapp"],

templated_args = ["src/prodapp"],

)

rollup_bundle generates bundles for the application.

name of the rule is specified under name ,

, the entry point of the application under entry_point which in this case is main.prod.ts

which in this case is the dependencies that are needed to build the application bundles, which is the src folder containing all the application code, @npm//@angular/router & @npm//rxjs (since @npm//@angular/router is already included in src dependencies, we can remove it from here)

web_package assembles the web application from the input files and injects JavaScript and stylesheets into the index.html .

name is the name of the rule

is the name of the rule files that need to be injected into the index.html are specified under assets which are zone.min.js , :bundle.min.js & :global_stylesheet . We are referring to the generated files bundle.min.js and global_stylesheet here which is the output from the corresponding previous rules

are specified under which are , & . We are referring to the generated files and here which is the output from the corresponding previous rules files that need to be loaded but not injected such as favicon.ico (images) are specified under data

(images) are specified under index_html takes the index.html file and is also where the scripts and stylesheets are injected.

history_server is an HTTP server that serves the prod application and takes prodapp as its input.

Dev server



name = "rxjs_umd_modules",

srcs = [

# do not sort

"

":rxjs_shims.js",

],

) filegroup(name = "rxjs_umd_modules",srcs = [# do not sort @npm //:node_modules/rxjs/bundles/rxjs.umd.js",":rxjs_shims.js",],

name = "devserver",

port = 4200,

entry_module = "project/src/main.dev",

serving_path = "/bundle.min.js",

scripts = [

"

":rxjs_umd_modules",

],

static_files = [

"

":global_stylesheet",

],

data = [

"favicon.ico",

],

index_html = "index.html",

deps = [":src"],

) ts_devserver(name = "devserver",port = 4200,entry_module = "project/src/main.dev",serving_path = "/bundle.min.js",scripts = [ @npm //:node_modules/tslib/tslib.js",":rxjs_umd_modules",],static_files = [ @npm //:node_modules/zone.js/dist/zone.min.js",":global_stylesheet",],data = ["favicon.ico",],index_html = "index.html",deps = [":src"],

ts_devserver is a library that runs a local web server and takes the following inputs:

a port

entry_module which is the entry point of the application for dev version

which is the entry point of the application for dev version scripts that need to be included in the bundle after require.js

that need to be included in the bundle after files that need to be injected into index.html under static_files

under files that need to be loaded but not need to be injected under data

index.html under index_html

under the src package which needs to be built first under deps

file_group names a collection of files and can be referenced from other rules. Here, we create a file group for rxjs files called rxjs_umd_modules with rxjs.umd.js and rxjs_shims.js . The reason behind these files is that the main entry point for rxjs is not an UMD file and therefore we specify it explicitly to use the UMD file. rxjs_shims.js provides named UMD modules for rxjs/operators and rxjs/testing so we can bundle the application with rxjs .

WORKSPACE

This file has the external dependencies that Bazel needs to do the build. Once you have set up your application to use Bazel for build, you wont be working on WORKSPACE file as much as you would on BUILD.bazel file.

workspace(

name = "my_wksp",

managed_directories = {"@npm": ["node_modules"]},

)

name - unique name for the workspace

- unique name for the workspace managed_directories tells Bazel that the node_modules directory is special and is managed by the package manager.

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") RULES_NODEJS_VERSION = "0.32.2" RULES_NODEJS_SHA256 = "6d4edbf28ff6720aedf5f97f9b9a7679401bf7fca9d14a0fff80f644a99992b4" http_archive( name = "build_bazel_rules_nodejs", sha256 = RULES_NODEJS_SHA256, url = "https://github.com/bazelbuild/rules_nodejs/releases/download/%s/rules_nodejs-%s.tar.gz" % (RULES_NODEJS_VERSION, RULES_NODEJS_VERSION), )

http_archive downloads a Bazel repository as a compressed archive file, decompresses it, and makes its targets available.

build_bazel_rules_nodejs has Javascript and NodeJS rules for Bazel and has more rules internally that we will be using later on in this file.

RULES_SASS_VERSION = "86ca977cf2a8ed481859f83a286e164d07335116" RULES_SASS_SHA256 = "4f05239080175a3f4efa8982d2b7775892d656bb47e8cf56914d5f9441fb5ea6" http_archive( name = "io_bazel_rules_sass", sha256 = RULES_SASS_SHA256, url = "https://github.com/bazelbuild/rules_sass/archive/%s.zip" % RULES_SASS_VERSION, strip_prefix = "rules_sass-%s" % RULES_SASS_VERSION, )

io_bazel_rules_sass has the rules for compiling sass.

load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories", "yarn_install") check_bazel_version( message = """ ... """, minimum_bazel_version = "0.27.0", )

check_bazel_version verifies that the Bazel version is at least the specified one. This check is in the WORKSPACE file so that the build fails as early as possible.

node_repositories( node_repositories = { "10.16.0-darwin_amd64": ("node-v10.16.0-darwin-x64.tar.gz", "node-v10.16.0-darwin-x64", "6c009df1b724026d84ae9a838c5b382662e30f6c5563a0995532f2bece39fa9c"), "10.16.0-linux_amd64": ("node-v10.16.0-linux-x64.tar.xz", "node-v10.16.0-linux-x64", "1827f5b99084740234de0c506f4dd2202a696ed60f76059696747c34339b9d48"), "10.16.0-windows_amd64": ("node-v10.16.0-win-x64.zip", "node-v10.16.0-win-x64", "aa22cb357f0fb54ccbc06b19b60e37eefea5d7dd9940912675d3ed988bf9a059"), }, node_version = "10.16.0", )

node_repositories is a set of repository rules for setting up hermetic copies of NodeJS and Yarn.

yarn_install( name = "npm", always_hide_bazel_files = True, package_json = "//:package.json", yarn_lock = "//:yarn.lock", )

yarn_install installs npm dependencies into @npm, creates an npm workspace. yarn_install is the preferred rule for setting up Bazel-managed dependencies over npm_install because yarn_install uses the global yarn cache by default improving the build performance and npm has a an isse which can cause missing peer dependencies sometimes.

load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies") install_bazel_dependencies()

install_bazel_dependencies install the rules found in the npm packages such as @bazel/karma , @bazel/typescript , etc. and will install @bazel/karma as a Bazel workspace named npm_bazel_karma and so on.

load("@npm_bazel_karma//:package.bzl", "rules_karma_dependencies") rules_karma_dependencies()

rules_karma_dependencies installs karma dependencies.

load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories") web_test_repositories() load("@npm_bazel_karma//:browser_repositories.bzl", "browser_repositories") browser_repositories()

web_test_repositories allows testing against a browser with WebDriver.

browser_repositories allows us to choose browsers we can test on as below:

browser_repositories(

chromium = True

)

browser_repositories ideally should come from @io_bazel_rules_webtesting but since there is a bug, we use it from @npm_bazel_karma until it is fixed.

load("@npm_bazel_typescript//:index.bzl", "ts_setup_workspace") ts_setup_workspace()

ts_setup_workspace creates some additional Bazel external repositories that are used internally by the TypeScript rules.

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

sass_repositories sets up environment for Sass compiler.

5. Alternate commands

You can also use the below commands to run the dev and prod servers:

To run dev server:

yarn bazel run //src:devserver

To run prod server:

yarn bazel run //src:prodserver

Let’s check out the syntax quickly:

//src:prodserver

// is the equivalent of the project root

is the equivalent of the project root src is the package name with the BUILD.bazel file containing the rules for devserver & prodserver

is the package name with the BUILD.bazel file containing the rules for & devserver & prodserver is the name of the target that will be invoked

Note: yarn bazel run <target> both builds and runs the application.

If you want to build a particular package separately, you can do so using yarn bazel build <target> .

For ex.:

yarn bazel build //src:src

will build the ng_module in src package.

Since the package name and the target name is the same, you can simply do:

yarn bazel build //src

If you want to watch your files and automatically trigger rebuild, you can simple replace bazel in the above command to ibazel and you should be good to go!

yarn ibazel build //src

Tips, some important things and debugging

Start at a bigger level

If your application is not so small and you are feeling demotivated to use Bazel because you need to write multiple Bazel files to optimise the build, start off by having BUILD.bazel files at a project level and for a few high level packages and then fine grain it and add more as you find it necessary. Your application will still build with just one BAZEL.build file for the whole app as we just saw above.

VSCode extension

The Bazel team made a cool extension for Bazel which will give you nice syntax highlighting for your build files.

Everything in Bazel is AOT

You need to use the NgFactory files directly in your references (at least for now until Ivy is out). For example, if you have a lazy loaded import, you need to use the module.ngfactory in the import instead of the module file.

Therefore,

{

path: ‘my-lazy-page’,

loadChildren: () => import(‘./my-lazy-page/my-lazy-page.module’).then(m => m.MyLazyPageModule)

}

becomes

{

path: 'my-lazy-page',

loadChildren: () => import('./ my-lazy-page/ my-lazy-page.module.ngfactory').then(m => m.MyLazyPageModuleNgFactory) }

You will find similar references in main.dev.ts and main.prod.ts as well.

Lazy loading and Bazel

Ever seen this error when trying to build your application using Bazel?

UMD and IIFE output formats are not supported for code-splitting builds.

When rollup_bundle finds a dynamic import (for lazy loading), it thinks that it should do code splitting, but the rollup_bundle rule wasn’t explicitly configured for code splitting, so it to tries build the wrong output.

additional_entry_points attribute for rollup_bundle controls both whether there are extra entry points and also whether there is code splitting.

If you have lazy loading for the routes in your application, until this PR is fixed, you might need to add the lazy loaded module to the additional_entry_points in your roll_up bundle to stop it trying to build an IIFE/UMD output.

3rd party applications

If you are using any 3rd party applications and they do not come with NgFactory files, you need to add them to angular-metadata.tsconfig.json file which will generate the NgFactory files in postinstall step.

Add dependencies explicitly

You need to add dependencies for each package explicitly in the deps section. Otherwise you will face an error like below even if you have added this module in your module.ts file:

Cannot find module '@angular/router'.

Conclusion

Bazel seems like a solid solution for issues around long build times for Angular applications. Angular team claim that build time for Angular has gone down from 60 min to an impressive 7.5 min using Bazel. And with Remote Build Execution, the future looks very exciting in the Angular build world. All of these sound very promising and can’t wait to have Bazel ready in Angular 9!

You can also check out the enterprise level example made by Angular team with build and test using Bazel here.