この記事は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を見ましょう。
変更点が:
1. builder
を@angular/bazel:build
に変更しました。
2. 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さんです!