この記事はAngular Advent Calendar 2019 15日目の記事です。
Build Angular with Bazel
皆さん、こんにちは。また年末Adventの時期になりましたね。最近Bazelにハマって、今回はBazelでAngularをBuildする仕組みを紹介したいと思います。
私はJia Liと申します、いろいろAngularのアプリの開発をやりましたし、Zone.jsとAngularのContributionもいろいろやりました、今はAngular Collaboratorの形でZone.jsのメンテをやっています。
この記事では下記の内容を説明したいと思います。
- 簡単なBazelの紹介
- @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さんの発表では、下記のように明確な回答が示されました。Bazel はコンパイルやバンドルツールではなく、これらのツールを連携するCoordinateツールです。
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することができます。まずオフィシャルのやり方を説明します。
既存のアプリの場合、
ng add @angular/bazel
新規の場合
npm install -g @angular/bazel
ng new --collection=@angular/bazel
既存のアプリで@angular/bazelを入れたあとで、どこを変更するかを説明します。
- ファイルの変更一覧
-
angular-metada.tsconfig.json: このファイルがaotコンパイルの対象一覧です、何らかの3rd-partyライブラリでngfactory.jsが作成されない場合、ここにいれないといけないです。 -
angular.json.bak: もとのangular.jsonのバックアップです。 -
protractor.on-prepare.js:protractorのbazel ruleでサーバーを起動するとき、ランダムのPortをアサインします、それから、protractor.browser.urlをランタイムで更新しないといけないです。このファイルがprotractorを起動するときのhookのようなものです。 -
initialize_testbed.tsはテストの初期化に必要なものです。 -
main.dev.tsとmain.prod.tsが明確に環境ごとにmain.tsを分けています。分けずにBazelのルールでDev/Prodのmain.tsを作ることもできますが、inputとoutput同じファイル名のmain.tsを触る恐れがありますので、とりあえず自動的に分けるということになりました。 -
rxjs_shims.js: このファイルがrxjs/operatorsとrxjs/testの名前付きUMDバンドルを提供します。もともとこれはwebpackで処理されたらしいですが、今は自分でやらないといけないです。 -
angular.json:angular-cli設定の変更がここです。
ここでは主にangular.jsonの変更を説明します。どこが変更されたかは下記のDiffを見ましょう。
変更点が:
-
builderを@angular/bazel:buildに変更しました。 -
optionsのもとの設定を全部削除し、BazelのTargetとCommandを追加
ng build
を実行したら、angular-cliが@angular/bazelで提供されたbuilderを利用して、実際やっているのは
-
Bazel必要なWORKSPACE、BUILD.bazelなどのファイルを用意する -
bazel build //src:prodappを実行する - 作成された
WORKSPACEなどのファイルを削除する
というのは、@angular/bazelでBuildの仕組みとは:
angular cli -> bazel command -> bazel rule
ということです。
Bazelに関わるファイルをみるためには、下記のCommandが必要です。
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をみたらわかりますが、今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が単体テストにもたらした恩恵
をご参考ください。
ここで説明したルール・構成はBazel angular exampleと若干違うところがありますが、基本の考え方は一緒です。
問題点
いろいろ試しましたが、今Bazel でAngularをBuild・テストすることができることがわかりました。
でもいくつか問題点もあります。
- Angular CLIの連携が足りない、たとえば、
ng generate moduleしたあとで、自動的にBUILD.bazelを作成・更新ができない -
Assetsとの連携が難しい、3rdpartyライブラリとの連携がとっても面倒くさい。
など。 -
rxjs.shim.jsなどのようなAngular CLIですでに対応されている問題を自分で対応しないといけないところがあります。
そして 一番重要なこと
今までの内容を読まなくても、ここの内容だけ読んで頂ければいいと思います。。。。
この記事を書いている最中、bazelconに参加しました。AlexEagleさんとGreg Magolanさんと話したら、今のng_moduleとか@angular/bazelの仕組みを大幅に変更(やり直すと同じぐらい)する必要があることがわかりました。最初に話した
Angular CLI->Bazel->ng_module
の仕組みから
Bazel->Angular CLI
を変更する予定みたいです。また、今のAngular CLIのすべての機能がそのまま利用できます。
そして、ng_moduleを廃棄することもあるかもしれません。
つまり、この記事で書いた内容が半分以上近い将来なくなるということです。。。。。。
最後に
ですが、今bazel for Angularの仕組みを理解できたら、将来新しい仕組みも深く理解できると思います。
ただし、今BazelをAngularプロジェクトに導入するのは、もうちょっと待つのをおすすめします。あるいはNXを利用してください。
以上、どうもありがとうございました!
明日は@ynishimuraさんです!


