Test Angular projects with Bazel
I’m a big fan of mono-repos and one central tool to succeed with a mono-repo is to use a common language agnostic build and test tool like Bazel. I had a lot of issues getting Angular tests up and running with Bazel. This article will share my findings and hopefully, it will help others get their Angular tests up and running with Bazel.
Angular uses Karma and Jasmine for testing but is moving over to Jest for unit testing, as Karma is being decommissioned. Jest has a lot more support in the Bazel community. The first thing we need to do is to convert the Jasmine tests into Jest tests. To configure the jest environment for Jest you can use the jest-preset-angular package.
Unfortunately, we can’t directly use an out-of-the-box rule like ts_project_rule from Aspect, as Angular uses its custom compiler ngc. But it’s possible to change the typescript compiler in the ts_project_rule to the Angular ngc compiler.
load("@npm//:@angular/compiler-cli/package_json.bzl", compiler_cli = "bin")
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@npm//:defs.bzl", "npm_link_all_packages")
npm_link_all_packages(name = "node_modules")
compiler_cli.ngc_binary(
name = "ngc",
)
ts_project(
name = "lib",
srcs = glob(["src/**/*.ts", "src/**/*.css", "src/**/*.html"], exclude = ["src/**/*.spec.ts"]),
deps = [
":node_modules/@angular/core",
":node_modules/rxjs",
":node_modules/tslib",
],
# NGC compiler, do not use the standard tsc worker
tsc = ":ngc",
supports_workers = False,
source_map = True,
declaration = True,
)Once we have compiled the code with the Angular compiler it’s now possible to use the jest_test rule to run the Jest tests. In the spirit of Bazel, we will add a second ts_project target for the test build. This target will be used by our jest_test rule together with the jest configuration and setup files. You can read more about how to set up the jest environment here.
Your final BUILD file should look something like this.
load("@npm//:@angular/compiler-cli/package_json.bzl", compiler_cli = "bin")
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@npm//:defs.bzl", "npm_link_all_packages")
npm_link_all_packages(name = "node_modules")
compiler_cli.ngc_binary(
name = "ngc",
)
ts_project(
name = "lib",
srcs = glob(["src/**/*.ts", "src/**/*.css", "src/**/*.html"], exclude = ["src/**/*.spec.ts"]),
deps = [
":node_modules/@angular/core",
":node_modules/rxjs",
":node_modules/tslib",
],
# NGC compiler, do not use the standard tsc worker
tsc = ":ngc",
supports_workers = False,
source_map = True,
declaration = True,
)
ts_project(
name = "test_ts",
srcs = glob(["src/**/*.spec.ts"]),
# NGC compiler, do not use the standard tsc worker
tsc = ":ngc",
supports_workers = False,
source_map = True,
declaration = True,
deps = [
":lib",
":node_modules/@types/jest",
],
)
jest_test(
name = "test",
config = "jest.config.js",
data = [
":test_ts",
":setup-jest.ts",
":tsconfig.spec.json",
":tsconfig.json",
":node_modules/jest-preset-angular",
":node_modules/jest-environment-jsdom",
":node_modules/@angular/platform-browser-dynamic",
],
node_modules = ":node_modules",
)
To make it easier to use angular in Bazel you can create a macro like this:
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 = ":ngc",
supports_workers = False,
# Any other ts_project() or generic args
**kwargs
)Then you can simplify you BUILD file with this wraper.
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("@npm//:defs.bzl", "npm_link_all_packages")
npm_link_all_packages(name = "node_modules")
ng_project(
name = "lib",
srcs = glob(["src/**/*.ts", "src/**/*.css", "src/**/*.html"], exclude = ["src/**/*.spec.ts"]),
deps = [
":node_modules/@angular/core",
":node_modules/rxjs",
":node_modules/tslib",
],
source_map = True,
declaration = True,
)
ng_project(
name = "test_ts",
srcs = glob(["src/**/*.spec.ts"]),
source_map = True,
declaration = True,
deps = [
":lib",
":node_modules/@types/jest",
],
)
jest_test(
name = "test",
config = "jest.config.js",
data = [
":test_ts",
":setup-jest.ts",
":tsconfig.spec.json",
":tsconfig.json",
":node_modules/jest-preset-angular",
":node_modules/jest-environment-jsdom",
":node_modules/@angular/platform-browser-dynamic",
],
node_modules = ":node_modules",
)This article used rules from Aspect. Mainly rules_js for npm packages, rules_ts for typescript, and rules_jest for the jest test. Follow the links to configure each rule. I used MODULE.bazel for this and you can see my setup here:
bazel_dep(name = "aspect_bazel_lib", version = "1.38.1")
bazel_dep(name = "aspect_rules_jest", version = "0.19.5")
bazel_dep(name = "aspect_rules_js", version = "1.34.0")
bazel_dep(name = "aspect_rules_ts", version = "2.1.1")
####### Node.js version #########
# By default you get the node version from DEFAULT_NODE_VERSION in @rules_nodejs//nodejs:repositories.bzl
# Optionally you can pin a different node version:
bazel_dep(name = "rules_nodejs", version = "6.1.0")
node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(node_version = "18.20.1")
#################################
npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm", dev_dependency = True)
npm.npm_translate_lock(
name = "npm",
npmrc = "//:.npmrc",
pnpm_lock = "//:pnpm-lock.yaml",
verify_node_modules_ignored = "//:.bazelignore",
)
use_repo(npm, "npm")
rules_ts_ext = use_extension(
"@aspect_rules_ts//ts:extensions.bzl",
"ext",
dev_dependency = True,
)
rules_ts_ext.deps()
use_repo(rules_ts_ext, "npm_typescript")