Build Angular with Bazel

この記事は[Angular Advent Calendar 2019](https://qiita.com/advent-calendar/2019/angular) 15日目の記事です。

# Build Angular with Bazel

皆さん、こんにちは。また年末Adventの時期になりましたね。最近Bazelにハマって、今回はBazelでAngularをBuildする仕組みを紹介したいと思います。

私はJia Liと申します、いろいろAngularのアプリの開発をやりましたし、Zone.jsとAngularのContributionもいろいろやりました、今はAngular Collaboratorの形でZone.jsのメンテをやっています。

この記事では下記の内容を説明したいと思います。

1. 簡単なBazelの紹介
2. @angular/bazelでAngularをBuildする仕組み

になります。

## Bazelとは

Bazelの細かい説明は別の記事でしたいので、この記事では簡単な紹介だけさせていただきます。
Bazelは速い、拡張性がいい、インクリメント、そしてユニバーサル（言語・フレームワークに限らない）のBuildツールで、大規模のmono repoに一番向いています。

### Bazelの位置づけ

今世の中にはBuildツールがいっぱいあります。たとえば、
- CI tools: Jenkins/CircleCI
- Compile tools: tsc/sass
- Bundle tools: webpack/rollup
- Coordinate tools: make/grunt/gulp
など、Bazelの位置づけはどこでしょうか？

今年のngconfの[AlexEagleさん](https://twitter.com/Jakeherringbone)の発表では、下記のように明確な回答が示されました。Bazel はコンパイルやバンドルツールではなく、これらのツールを連携するCoordinateツールです。

![Screen_Shot_2019-05-28_at_8.26.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109004/1eb12095-d6ee-7547-119f-c7f35deceefc.png)

### Bazelの素晴らしいところ
- 正確性
    - BazelはSandBoxでIsolateな環境でBuildを実行する
    - Bazel RuleがInputだけアクセスできる
- 速い
    - BazelはDeclarative、そしてBazel RuleはPure Functionなので、Bazelが依頼関係を分析できます。そして、変更があったら、影響する部分だけ再Buildすることができます。
    - BazelがPure Functionで、アウトプットがインプットによってのみ決定されるので、インプットによって、Cacheが簡単にできます。そして、並列実行もできます。
- ユニバーサル
    - Bazelがどの言語・フレームワークでもBuildできます。サーバーでMaven、クライアントでWebpack/Gulp、モバイルがGradleのような別々なBuildツールがBazelで統一することができるようになります。

## @angular/bazel でAngularをBuildする

今はすでにBazelでAngularをBuildすることができます。まずオフィシャルのやり方を説明します。
既存のアプリの場合、

```sh
ng add @angular/bazel
```
新規の場合

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

既存のアプリで`@angular/bazel`を入れたあとで、どこを変更するかを説明します。

- ファイルの変更一覧

![Screenshot from 2019-12-09 20-34-33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109004/ca3fc99d-5f2b-5e13-176d-aa42a2e6dff2.png)

1. `angular-metada.tsconfig.json`: このファイルが`aot`コンパイルの対象一覧です、何らかの3rd-partyライブラリで`ngfactory.js`が作成されない場合、ここにいれないといけないです。
2. `angular.json.bak`: もとの`angular.json`のバックアップです。
3. `protractor.on-prepare.js`: `protractor`のbazel ruleでサーバーを起動するとき、ランダムのPortをアサインします、それから、`protractor.browser.url`をランタイムで更新しないといけないです。このファイルが`protractor`を起動するときの`hook`のようなものです。
4. `initialize_testbed.ts`はテストの初期化に必要なものです。
5. `main.dev.ts`と`main.prod.ts`が明確に環境ごとに`main.ts`を分けています。分けずにBazelのルールでDev/Prodのmain.tsを作ることもできますが、`input`と`output`同じファイル名の`main.ts`を触る恐れがありますので、とりあえず自動的に分けるということになりました。
6. `rxjs_shims.js`: このファイルが`rxjs/operators`と`rxjs/test`の名前付きUMDバンドルを提供します。もともとこれはwebpackで処理されたらしいですが、今は自分でやらないといけないです。
7. `angular.json`: `angular-cli`設定の変更がここです。

ここでは主に`angular.json`の変更を説明します。どこが変更されたかは下記のDiffを見ましょう。

![Screenshot from 2019-12-09 20-18-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109004/cd2dc01d-cf3b-fa60-b079-24539179e245.png)

変更点が：
1. `builder`を`@angular/bazel:build`に変更しました。
2. `options`のもとの設定を全部削除し、`Bazel`のTargetとCommandを追加

```sh
ng build
```

を実行したら、`angular-cli`が`@angular/bazel`で提供された`builder`を利用して、実際やっているのは

1. `Bazel`必要な`WORKSPACE`、`BUILD.bazel`などのファイルを用意する
2. `bazel build //src:prodapp` を実行する
3. 作成された`WORKSPACE`などのファイルを削除する

というのは、`@angular/bazel`でBuildの仕組みとは：
`angular cli` -> `bazel command` -> `bazel rule`
ということです。

`Bazel`に関わるファイルをみるためには、下記のCommandが必要です。

```sh
ng build --leaveBazelFilesOnDisk
```

それらのファイルのなかに、代表的なファイルを説明します。

- WORKSPACE: 環境の設定はここです。

```
workspace(
    name = "bazel-sample",
    managed_directories = {"@npm": ["node_modules"]},
)

```
まず名前と`@npm`の特別のDirectoryの設定をします。

```
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "build_bazel_rules_nodejs",
    sha256 = "9901bc17138a79135048fb0c107ee7a56e91815ec6594c08cb9a17b80276d62b",
    url = "https://github.com/bazelbuild/rules_nodejs/releases/download/0.40.0/rules_nodejs-%s.tar.gz" % (RULES_NODEJS_VERSION, RULES_NODEJS_VERSION),
)
```

`nodejs`の`bazel rule`をダウンロードします。`typescript`、`karma`、`protractor`などの基本ルールが入っています。

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

同じく、`SASS`のルールをダウンロードします。

```
load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install")

node_repositories(...)
yarn_install(...)
```

ここで`node`をIsolate環境でインストールして、`yarn`インストールをします。このステップでnodeのバージョンなど`package.json`ファイルも指定できます。

```
install_bazel_dependencies()
npm_bazel_protractor_dependencies()
npm_bazel_karma_dependencies()
web_test_repositories()
browser_repositories(
    chromium = True,
    firefox = True,
)
ts_setup_workspace()
sass_repositories()
```
それぞれのツールのセットアップを実施します。

ちなみに、このファイルは比較的一番問題が出やすいところです、原因としてはWORKSPACEの中にいろいろなライブラリ等を導入しますが、Dependency 管理の仕組み等がないためです。ここではいろいろとおかしな問題がよくでます。今Bazelチームがより良い方法を設計しているらしいです。

## BUILD.bazel、ルートフォルダーのBuildファイル

```
exports_files([
    "tsconfig.json",
])
```

メインの役割はルートフォルダーの設定ファイルをサブモジュールに共用することです。

## src/BUILD.bazel、これが一番重要なBuildファイルです

```
sass_binary(
    name = "styles",
    src = "styles.scss",
)
```

このRuleが`scss`ファイルをコンパイルします。

```
NG_FACTORY_ADDED_IMPORTS = []
ng_module(
    name = "src",
    srcs = [
        "main.ts",
    ],
    tsconfig = ":tsconfig.json",
    deps = NG_FACTORY_ADDED_IMPORTS + [
        "//src/app",
        "@npm//@angular/core",
        "@npm//@angular/platform-browser",
        "@npm//@angular/router",
    ],
)
```

ここがAOTコンパイルすることを実施します。`ng_module`ルールが`ts_library`を継承しますので、裏では`ngc`を利用してコンパイルを実施します。
`NG_FACTORY_ADDED_IMPORTS`はライブラリの`ngfactory`コードを導入するところです。例えば、`    "@npm//@angular/cdk",`など。これらのコードは`postinstall`するとき`ngcc`でコンパイルされた結果です。

そして、[オフィシャルのbazel angular sample](https://github.com/bazelbuild/rules_nodejs/tree/master/examples/angular)をみたらわかりますが、今`ng_module`を利用するときの推奨されるやり方は`NgModule`ごとに`BUILD.bazel`を作ることになります。

```
_ASSETS = [
    # This label references an output of the "styles" sass_binary above.
    ":styles.css",

    # We load zone.js outside the bundle. That's because it's a "pollyfill"
    # which speculates that such features might be available in a browser.
    # Also it's tricky to configure dead code elimination to understand that
    # zone.js is used, given that we don't have any import statement that
    # imports from it.
    "@npm//:node_modules/zone.js/dist/zone.min.js",
]
```

StaticのAssetsをここに定義します。

```
rollup_bundle(
    name = "bundle",
    config_file = "rollup.config.js",
    entry_point = ":main.prod.ts",
    deps = [
        "//src",
        "@npm//rollup-plugin-commonjs",
        "@npm//rollup-plugin-node-resolve",
    ],
)
```

このルールが`rollup`でバンドルを作成します（TreeShakingなども実施します）。

```
terser_minified(
    name = "bundle.min",
    src = ":bundle",
)
```
このルールが`terser`でバンドルをUglifyにします。

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

このルールが作成された`assets`と`scripts`を`index.html`にいれて、動作可能なHTMLパッケージを作成します。

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

このルールでは単純に`Http サーバー`を立ち上げて、パッケージをサーブしています。

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

```
開発するとき、`ibazel`でこのルールを実施して、`watch mode`で開発することができます、`ng serve`がこのルールを実行します。この`ts_devserver`はwebpack_devserverではなく、もっとライトウェイトなGoで実装されたDevServerです(livereloadを利用)。

```
karma_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",
    ],
    tags = ["native"],
    deps = [
        ":rxjs_umd_modules",
        ":test_lib",
        "@npm//karma-jasmine",
    ],
)
```

`ng test`のときはこのルールを実施して、`karma`テストを実行します。このルールも問題が出やすいところです。特に`bootstrap`/`runtime_deps`/`data`/`deps`の分け方をいろいろ設計しないといけないところがあります。ほかの記事でまた今度説明させていただきます。

テストコードも`AOT`で実施するため、ViewEngineの場合、`aotSummuries`を利用する必要があります。詳細は前日@Quramyさんの記事[Ivyが単体テストにもたらした恩恵
](https://qiita.com/Quramy/items/9650155576bbb43f41bf)をご参考ください。


ここで説明したルール・構成は[Bazel angular example](https://github.com/bazelbuild/rules_nodejs/tree/master/examples/angular)と若干違うところがありますが、基本の考え方は一緒です。

## 問題点

いろいろ試しましたが、今Bazel でAngularをBuild・テストすることができることがわかりました。
でもいくつか問題点もあります。
- Angular CLIの連携が足りない、たとえば、`ng generate module`したあとで、自動的に`BUILD.bazel`を作成・更新ができない
- `Assets`との連携が難しい、`3rdparty`ライブラリとの連携がとっても面倒くさい。
など。
- `rxjs.shim.js`などのようなAngular CLIですでに対応されている問題を自分で対応しないといけないところがあります。

## そして　一番重要なこと

今までの内容を読まなくても、ここの内容だけ読んで頂ければいいと思います。。。。

この記事を書いている最中、[bazelcon](https://conf.bazel.build/2019)に参加しました。[AlexEagleさん](https://twitter.com/Jakeherringbone)と[Greg Magolanさん](https://twitter.com/gregmagolan)と話したら、今の`ng_module`とか`@angular/bazel`の仕組みを大幅に変更（やり直すと同じぐらい）する必要があることがわかりました。最初に話した
`Angular CLI->Bazel->ng_module`
の仕組みから
`Bazel->Angular CLI`
を変更する予定みたいです。また、今の`Angular CLI`のすべての機能がそのまま利用できます。
そして、`ng_module`を廃棄することもあるかもしれません。
つまり、この記事で書いた内容が半分以上近い将来なくなるということです。。。。。。

## 最後に

ですが、今bazel for Angularの仕組みを理解できたら、将来新しい仕組みも深く理解できると思います。
ただし、今BazelをAngularプロジェクトに導入するのは、もうちょっと待つのをおすすめします。あるいは[NX](https://github.com/nrwl/nx)を利用してください。

以上、どうもありがとうございました！

明日は@ynishimuraさんです！