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.
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.
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 protractorUtils = require('@angular/bazel/protractor-utils');
const protractor = require('protractor');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;
});
};
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 {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 [@](http://twitter.com/angular?source=post_page---------------------------)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
.
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.
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.
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
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](https://docs.bazel.build/versions/master/be/common-definitions.html?source=post_page---------------------------#common-attributes)
allows the rules in the current package to be accessed from other packages.
[export_files](https://docs.bazel.build/versions/master/be/functions.html?source=post_page---------------------------#exports_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.
load("@npm_angular_bazel//:index.bzl", "ng_module")
load("@npm_bazel_karma//:index.bzl", "ts_web_test_suite")
load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle", "history_server")
load("@build_bazel_rules_nodejs//internal/web_package:web_package.bzl", "web_package")
load("@npm_bazel_typescript//:index.bzl", "ts_devserver", "ts_library")
load("@io_bazel_rules_sass//:defs.bzl", "multi_sass_binary", "sass_binary")
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](https://github.com/bazelbuild/rules_sass?source=post_page---------------------------#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](https://github.com/bazelbuild/rules_sass?source=post_page---------------------------#sass_library)
rule instead.
multi_sass_binary(
name = "styles",
srcs = glob(
include = ["**/*.scss"],
exclude = ["styles.scss"],
),
)
[multi_sass_binary](https://github.com/bazelbuild/rules_sass/issues/22?source=post_page---------------------------)
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.
ng_module(
name = "src",
srcs = glob(
include = ["**/*.ts"],
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",
],
)
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 setup consists of 3 things: rollup, web package and a server.
rollup_bundle(
name = "bundle",
entry_point = ":main.prod.ts",
deps = [
"//src",
"@npm//@angular/router",
"@npm//rxjs",
],
)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
,entry_point
which in this case is main.prod.ts
src
folder containing all the application code, @npm//@angular/router
& @npm//rxjs
(since [@npm](http://twitter.com/npm?source=post_page---------------------------)//@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 ruleindex.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 rulesfavicon.ico
(images) are specified under **data**
**index_html**
takes the index.html file and is also where the scripts and stylesheets are injected.[history_server](https://www.npmjs.com/package/history-server?source=post_page---------------------------)
is an HTTP server that serves the prod application and takes prodapp
as its input.
Dev server
filegroup(
name = "rxjs_umd_modules",
srcs = [
# do not sort
"@npm//:node_modules/rxjs/bundles/rxjs.umd.js",
":rxjs_shims.js",
],
)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:
**port**
**entry_module**
which is the entry point of the application for dev version**scripts**
that need to be included in the bundle after require.js
index.html
under **static_files**
**data**
index.html
under **index_html**
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 workspacemanaged_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.
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 rootsrc
is the package name with the BUILD.bazel file containing the rules for devserver
& prodserver
devserver
& prodserver
is the name of the target that will be invokedNote: yarn bazel run
both builds and runs the application.
If you want to build a particular package separately, you can do so using yarn bazel build
.
For ex.:
yarn bazel build //src:src
will build the ng_module
insrc
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 //srcTips, some important things and debuggingStart 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.
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.
#angular #web-development