Angular + Bazel: Getting ready!
Tags: angular, bazel
Published: 2019-09-26 20:45:44

---
title: Angular + Bazel: Getting ready!
published: true
description: 
tags: Angular, Bazel
---

In this post, I'll make an introduction to what is Bazel, how to use it with the Angular CLI and why it's a good idea to adopt it.

### What is Bazel?
Bazel is a build tool/system with multi-language capabilities developed by Google. 
It's extensible, meaning that you can add support for other languages and frameworks.
It also knows what and when to rebuild specific parts of your code and caches your build artifacts, making it very fast.

### Concepts
**Workspace**: A workspace is the root directory that contains the source files to build. It should have at least a text file named `WORKSPACE` in it. If a `WORKSPACE` file is found in any sub-directory, it will be ignored by the parent workspace, creating a new workspace.

**Package**: Similar to workspaces, packages are directories beneath a workspace defined by a `BUILD` (or BUILD.bazel) file.  If a subdirectory of a `Package` contains a `BUILD` file it's considered a different `Package`.

**Targets**: The elements of a `Package` are mainly of two types:
* *Files* that can be generated by the build or source files provided by us.
* *Rules* that given an input (source or generated files) will return an output (generated files). Rules outputs always belong to the same package of the rule. 


### Referencing
There are multiple ways of referencing files and outputs.
If you want to reference a Rule inside the same package you'll do it by using a colon (`:`) and the name of the rule.

```
a_rule(
    name = "rule_name"
)

another_rule(
    name = another_rule_name,
    deps = [
        ":rule_name"
    ]
)
```

When referencing another package we will use a double slash (`//`) to get to the root of the workspace and from there get to the desired package.

```
another_package_rule(
    name = "some_name",
    deps = [
        "//foo/bar:a_rule"
    ]
)
```

If the name of the rule matches the package name, then the rule name after the colon can be omitted.

A reference to `//foo/bar:bar` equals `//foo/bar`.

### Bazel and Angular
Bazel is language and framework agnostic, but I would like to start by taking a look at how it can be used with Angular and understand what's happening.
Let's install the required dependencies and start a new project.

```
npm install -g @angular/bazel
ng new --collection=@angular/bazel
```

This command will create a new project with Bazel already set up as the build tool.

If we serve our app (`ng serve`) in our new project some new files will be created. 
* /WORKSPACE
* /BUILD.bazel
* .bazelignore
* .bazelrc
* /src/BUILD.bazel
* /e2e/BUILD.bazel

We can for sure tell that the CLI defined a `workspace`, and three `packages`. Following the Package definition, the package located at the root will contain everything in the workspace except from the `src` and `e2e` folders.
If we stop our server, the files will be removed again.
That was `zero-config` Bazel!

But what can we do if we need to tweak our build because we installed a new library or because we want to optimize our build. We could create the WORKSPACE and BUILD files manually or we can use the --leaveBazelFilesOnDisk flag so the generated files are not removed. 

```bash
ng build --leaveBazelFilesOnDisk
```

After we finish building we'll be able to explore and/or modify the generated files.

Let's start with `/src/BUILD.bazel`. I'll split the file in sections to explain them easily.

```
package(default_visibility = ["//visibility:public"])
```
At the very top, we have the visibility attribute. The visibility attribute will give access or not to the rules in this package from another package. Scopes of visibility can be set. For this example, we'll leave it as public.

```
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")
```

Then all the imports that we'll be using with the load command. This will import a symbol from an extension. 

After that, the rest is rules and its configurations. 

```
sass_binary(
  name = "global_stylesheet",
  src = glob(["styles.css", "styles.scss"])[0],
  output_name = "global_stylesheet.css",
)


multi_sass_binary(
    name = "styles",
    srcs = glob(
      include = ["**/*.scss"],
      exclude = ["styles.scss"],
    ),
)

```
At the start of every rule, there's its name. This will let us reference it from other places and for example, including it as a dependency to another rule.

The first rule is named `global_stylesheet`, this means we can get a reference to its output by calling `:global_stylesheet` (from the same package). The source is either a .css or .scss file depending on our configuration. We also configure the output file.

Our second rule, `styles`, will transform all the .scss files, excluding the global style file.

If you (like me), prefer to use CSS we can get rid of many of this stuff. We'll do that in a moment.

Let's continue with the next rule. `ng_module`.

```
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",
    ],
)
```

First of all, we are naming the rule the same as the directory, this makes it easier later when referencing it. Naming your main rule the same as your package directory is a good practice. We are setting our sources (all .ts files, ignoring what is not part of the module), and our assets (.css and .html files, concatenated with the output of the `styles` rule that converts .scss files). Finally, we declare our dependencies, in this case, located in the `npm` workspace.

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

web_package(
    name = "prodapp",
    assets = [
        # do not sort
        "@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"],
)

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_library(
    name = "test_lib",
    testonly = 1,
    srcs = glob(["**/*.spec.ts"]),
    deps = [
        ":src",
        "@npm//@angular/core",
        "@npm//@angular/router",
        "@npm//@types",
    ],
)

ts_library(
    name = "initialize_testbed",
    testonly = 1,
    srcs = [
        "initialize_testbed.ts",
    ],
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser-dynamic",
        "@npm//@types",
    ],
)

ts_web_test_suite(
    name = "test",
    srcs = [
        "@npm//:node_modules/tslib/tslib.js",
    ],
    runtime_deps = [
        ":initialize_testbed",
    ],
    # do not sort
    bootstrap = [
        "@npm//:node_modules/zone.js/dist/zone-testing-bundle.js",
        "@npm//:node_modules/reflect-metadata/Reflect.js",
    ],
    browsers = [
        "@io_bazel_rules_webtesting//browsers:chromium-local",
    ],
    deps = [
        ":rxjs_umd_modules",
        ":test_lib",
        "@npm//karma-jasmine",
    ],
)
```

The last section will setup and configure our bundler, the development server, the production build, and the tests.

When using Bazel, every dependency is declared, inputs and outputs are known. This will allow us to analyze what's going on and build a directed graph. We can query any package, rule or repository and its dependencies using the query command. The query command is very expressive in what you can request, and you can also get different output types. I recommend taking a look at the [documentation](https://docs.bazel.build/versions/master/query-how-to.html). We'll be also using a tool to convert the query graph results to an image.

```
// get al the packages in the workspace
bazel query --output=graph ... | dot -Tpng > graph.png
```

![workspace output graph](https://thepracticaldev.s3.amazonaws.com/i/neq0oig5pf0qhu8w3c7f.png)

This looks a little too much information. Fortunately, we can select portions of our graph.

```
bazel query "kind(rule, allpaths(//src:devserver, //...))" --output=graph | dot -Tpng > devserver.png
```

![dev server output graph](https://thepracticaldev.s3.amazonaws.com/i/9n9egld3rbwxtn583n63.png)


Let's start optimizing, and customizing our build, with what we have now. First of all, for this project, I decided  that I won't be using any .scss file, so I can get rid of the `styles` rule. Remember to remove any reference to it as well.

I also want the app directory to be its own package, so I'll create a BUILD.bazel file in it.

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

load("@npm_angular_bazel//:index.bzl", "ng_module")

ng_module(
    name = "app",
    srcs = glob(
        include = ["**/*.ts"],
        exclude = ["**/*.spec.ts"],
    ),
    assets = glob([
      "**/*.css",
      "**/*.html",
    ]),
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser",
        "@npm//@angular/router",
        "@npm//@types",
        "@npm//rxjs",
    ],
)

ts_library(
    name = "test_lib",
    testonly = 1,
    srcs = ["app.component.spec.ts"],
    deps = [
        ":app",
        "@npm//@angular/core",
        "@npm//@angular/router",
        "@npm//@types",
    ],
)
```
We've declared a new rule inside this new package inside `src/app` named `app`. We've also created a rule to handle the tests inside this package.
Next, we've removed the references to the `styles` rule, removed it, and instead we're setting the `styles.css` as the source. Also, we added a reference to the tests inside the app package.

```
ng_module(
    name = "src",
    srcs = glob(
        include = ["**/*.ts"],
        exclude = [
            "**/*.spec.ts",
            "main.ts",
            "test.ts",
            "initialize_testbed.ts",
        ],
    ),
    assets = glob([
      "**/*.css",
      "**/*.html",
    ]),
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser",
        "@npm//@angular/router",
        "@npm//@types",
        "@npm//rxjs",
        "//src/app"
    ],
)

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

web_package(
    name = "prodapp",
    assets = [
        # do not sort
        "@npm//:node_modules/zone.js/dist/zone.min.js",
        ":bundle.min.js",
        "styles.css",
    ],
    data = [
        "favicon.ico",
    ],
    index_html = "index.html",
)

history_server(
    name = "prodserver",
    data = [":prodapp"],
    templated_args = ["src/prodapp"],
)

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",
        "styles.css",
    ],
    data = [
        "favicon.ico",
    ],
    index_html = "index.html",
    deps = [":src"],
)

ts_library(
    name = "test_lib",
    testonly = 1,
    srcs = glob(["**/*.spec.ts"]),
    deps = [
        ":src",
        "@npm//@angular/core",
        "@npm//@angular/router",
        "@npm//@types",
    ],
)

ts_library(
    name = "initialize_testbed",
    testonly = 1,
    srcs = [
        "initialize_testbed.ts",
    ],
    deps = [
        "@npm//@angular/core",
        "@npm//@angular/platform-browser-dynamic",
        "@npm//@types",
    ],
)

ts_web_test_suite(
    name = "test",
    srcs = [
        "@npm//:node_modules/tslib/tslib.js",
    ],
    runtime_deps = [
        ":initialize_testbed",
    ],
    # do not sort
    bootstrap = [
        "@npm//:node_modules/zone.js/dist/zone-testing-bundle.js",
        "@npm//:node_modules/reflect-metadata/Reflect.js",
    ],
    browsers = [
        "@io_bazel_rules_webtesting//browsers:chromium-local",
    ]`
    deps = [
        ":rxjs_umd_modules",
        ":test_lib",
        "//src/app:test_lib",
        "@npm//karma-jasmine",
    ],
)
```

If we analyze our dev server again.
![dependencies graph updated](https://thepracticaldev.s3.amazonaws.com/i/ly3ujb6sipzt4cwpmivj.png)

I believe most of the benefits when using Bazel start to be more evident once your workspace starts growing. The pattern becomes roughly the same for each module you start adding.
Here's an example where I added a core module, and a shared module used by other 2 modules (users, repositories). You can easily start exploring the tree of dependencies.

![Extended graph example](https://thepracticaldev.s3.amazonaws.com/i/ij0a0wiwwj618zuptwv5.png)

Because Bazel is always AOT, be aware that when lazy loading a module, we must use the factory module.
For example:
```ts
const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./users/users.ngfactory').then(m => m.UsersModuleNgFactory)
  },
  {
    path: 'repositories',
    loadChildren: () =>
        import('./repositories/repositories.module.ngfactory').then(m => m.RepositoriesModuleNgFactory)
  }
]
```

If you look closely at the `main.dev.ts` and `main.prod.ts` we're also bootstrapping the factory module.

```ts
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
```

Now that we have an idea on how to work with Bazel, let's talk about its benefits.
I think most of them will shine on large codebases with lots of tests. Fortunately, it's adoption can be incremental (One package that does everything) and you can start creating more specific packages and begin declaring it's dependencies.

You can make use also of remote build and caching. Because for a given set of inputs, the output is always the same, you can cache the results and share it. If your CI already run the tests, when you make changes locally you can use the cached results of the unaffected tests and run only the affected ones. The same goes for the builds, reducing build times.

We've started only with an Angular application, but Bazel allows to do a lot more and because it's language agnostic, you can start referencing also the backend (perhaps written in another language), and use Bazel as the build tool.

Even though Bazel extensible, there's been a lot of work on making the needed rules already available. So there might be a low chance of needing to write a custom one. In case you're wondering on how to do it, Rules are written using [Starlark](https://github.com/bazelbuild/starlark) a dialect of Python.

Some of the features presented here are still part of Angular Labs and might change in the future.

###References
* [Angular-Bazel guide] (https://angular.io/guide/bazel)
* [Bazel docs] (https://docs.bazel.build/versions/0.29.1/bazel-overview.html)
* [Angular-Bazel example app] (https://github.com/bazelbuild/rules_nodejs/tree/master/examples/angular)

*Enjoy this article? Head on over to [This Dot Labs](https://www.thisdot.co/labs) and check us out!  We are a tech consultancy that does all things javascript and front end.  We specialize in open source software like, Angular, React and Vue.*