Angular with Bazel

We recently released rules_js 1.0.0, a faster and more compatible approach to integrating JavaScript tooling under Bazel. Learn more by reading our original post: blog.aspect.dev/rules-js.

Now that ng-conf 2022 is kicking off and all our friends are back in Salt Lake City, it's a perfect time to update how Angular and Bazel work well together using rules_js.

First a bit of background… the Angular CLI

The Angular CLI is the standard developer tool for Angular applications. From dev server to production bundling, the CLI takes an "all-in-one" approach for a good, lowest-effort developer experience. Under the hood the CLI is primarily a thin wrapper over the Angular Architect library. Angular Architect allows various tools to be integrated into the CLI via plugins such as a devserver, webpack bundling, sass integration, and test frameworks, along with the core Angular Compiler (ngc). Architect combines those tools into one for the full Angular CLI experience.

Angular Architect in Bazel

The simplest method of building an Angular application under Bazel is the same as under the Angular CLI: use Angular Architect. Simply invoke the Angular Architect tool from Bazel for the same all-in-one experience as the Angular CLI.

For example, a single Bazel target to compile an Angular application (named my-app, created by the Angular CLI):

load("@npm//:@angular-devkit/architect-cli/package_json.bzl", architect_cli = "bin")

architect_cli.architect(
    name = "my-app",
    args = ["my-app:build"],
    srcs = glob(["**/*.ts", "**/*.sass", "**/*.html"]),
)

See the Angular Architect example for a full example.

But all-in-one is not the idiomatic Bazel style: if anything in the application changes the entire application must recompile. While easier to implement, Angular Architect is not the ideal Bazel experience.

ngc in Bazel

To truly benefit from Bazel the compilation must be split into independent actions that can be individually executed and cached. Each action coordinated by Angular Architect can instead be coordinated by Bazel. When Bazel is coordinating the actions the real benefits of Bazel will be seen such as parallelization, caching, test caching, remote execution/caching and all the other benefits that come with Bazel.

The primary action is compiling Angular TypeScript including component templates, css and the various annotations such as @Injectable. The Angular Compiler (ngc), is a drop-in replacement for the TypeScript compiler (tsc). The ts_project rule can be customized to use ngc as the compiler binary.

Bazel's macros provide a simple way to define your own "syntax sugar". We'll start by declaring the ngc compiler target, and an ng_project macro that makes it easy to declare these.

tools/BUILD.bazel

load("@npm//:@angular/compiler-cli/package_json.bzl", compiler_cli = "bin")
compiler_cli.ngc_binary(name = "ngc")

tools/ng.bzl

load("@aspect_rules_ts//ts:defs.bzl", "ts_project")

# Macro to wrap Angular's ngc compiler
def ng_project(name, **kwargs):
    ts_project(
        name = name,

        # NGC compiler, do not use the standard tsc worker
        tsc = "//tools:ngc",
        supports_workers = False,

        # Any other ts_project() or generic args
        **kwargs
    )

Now Angular code can be fully compiled with the ng_project macro including TypeScript, component HTML and CSS, directives, @Injectables etc.

An example of a ng_project target:

my-app/BUILD.bazel

load("//tools:ng.bzl", "ng_project")

ng_project(
    name = "my-app",
    srcs = glob(["**/*.ts", "**/*.css", "**/*.html"]),
    deps = [
        "//:node_modules/@angular/core",
        ...
    ],
)

This is a single ng_project rule, but an application will most likely be divided into many BUILD files, creating many independently compiled and cached Bazel targets.

Other features of the Angular CLI such as Sass preprocessing, webpack bundling, a devserver, testing etc. will be configured as independent Bazel targets. Under the hood the tools will be the same as Angular Architect but now coordinated by Bazel. Or, you can swap out some of the tools, essentially making your own custom, incremental build system for Angular just with a few lines of Bazel's macros.

For example, bundling the application with rules_webpack

load("@aspect_rules_webpack//webpack:defs.bzl", "webpack_bundle")

webpack_bundle(
    name = "bundle",
    entry_point = "main.js",
    srcs = [":my-app"],
)

For a complete example see the angular-ngc Bazel example: github.com/jbedard/bazel-examples/tree/angu..