😽

Prettier の Angular サポートの仕組みと built-in control flow 対応

2023/12/19に公開

この記事は Angular Advent Calendar 2023 の 19 日目です。

Prettier というコードフォーマッタは Angular のテンプレート構文をサポートしています。私は Angular をほとんど使ったことがありませんが、Prettier をメンテする上で Angular のテンプレート構文について調査したり、@angular/compiler の実装を読んだりしています。

この記事では、Prettier による Angular テンプレート構文のサポートの技術的な詳細について説明します。そして、v17 で developer preview となった built-in control flowDeferrable Views という機能による大きな構文の追加による Prettier 側の対応について紹介します。

Prettier の Angular サポート

Prettier は JavaScript で記述されたコードフォーマッタで、Node.js およびブラウザから実行できます(テストはしていませんが Deno や Bun でも動くかもしれません)。

JavaScript を中心とした Web アプリケーション開発に必要な色々な言語をサポートしていることが特徴で、JavaScript や TypeScript の他にも HTML や CSS、GraphQL などをサポートしています。サポート対象の言語は 公式ドキュメントの What is Prettier? ページに列挙されています。

そして Prettier は Angular のテンプレート構文もサポートしています。私がメンテナンスに関わり始めたとき(2019年)にはすでにサポートしていたので、なぜサポートしているのかは知りません。過去の Prettier のメンテナーである ikatyang 氏 によって追加されたようです。

@angular/compiler をフォークした angular-html-parser

Angular には独自のテンプレート構文があります。この構文は、標準的な HTML っぽい構文の中に Angular 独自の構文が組み込まれた形をしています。

parse5 などの HTML パーサーでは Angular のテンプレート構文をパースすることはできません。そこで Prettier は @angular/compiler をフォークした angular-html-parser というパーサーを使っています。@angular/compiler そのままでは、Prettier から使いにくいのでいくつかの変更を加えています。詳細は README の Modifications セクション を参照してください。

@angular/compiler というのは npm パッケージの名前で、リポジトリの実態は https://github.com/angular/angular にあります。つまり Angular 本体のテンプレートのコンパイラです。具体的に @angular/compiler に該当するのは angular/angular リポジトリの /packages/compiler です。

Prettier はこの angular-html-parser を使って、Angular のテンプレート構文のパースだけではなく、通常の HTML や Vue の SFC もパースします。このあたりの処理は Prettier の src/language-html/parse-html.js に実装されています。ただ angular-html-parser を呼び出すだけかと思いきや、実際にはやや難解なコードになってしまっています。

Built-in control flow と Deferrable Views

Angular v17 から Developer Preview として、built-in control flowDeferrable Views という機能が追加されました。この機能によって、以下のような構文を使って条件付きで何かをレンダリングしたり、レンダリングを遅延させたりすることができるようになりました:

<!-- built-in control flow -->
@if (a > b) {
   {{a}} is greater than {{b}}
}

<!-- defferable views -->
@defer {
  <large-component />
}

この機能自体の詳細については公式のガイドや、Angular Advent Calendar 2023 14 日目の noxi515 さんの記事 などを参照してください。

Built-in control flow と Defferable views は機能的には別物なのですが、構文的には同じようなもので、Pretteri 側からの関心としては同じようなものなので、この記事では両方を指して built-in control flow と呼ぶことにします。

Prettier による built-in control flow のサポート

私は普段 Angular を使っているわけではないので、ユーザーから起票してもらった issue でこの機能について知りました。そして、この issue にコメントをくれている皆さんの反応を読んで、どうやらかなり求められている機能のようだということがわかりました。

https://github.com/prettier/prettier/issues/14991

この機能を Prettier がサポートするためには、以下のステップで実装を行う必要があります。

  1. angular-html-parser に対して最新の @angular/compiler の内容をマージする
  2. angular-html-parser に built-in control flow サポートのための修正を施す
  3. Prettier に built-in control flow のフォーマット処理を実装する

ステップ 1 である最新の upstream のマージという作業が結構たいへんでした。というのも、私は Angular のコードベースに詳しいわけではないので、コンフリクトした箇所の解消の方法が全然わからなかったのです。わからないとはいえ基本的にはただのパーサーなので、頑張って読んで気合で修正してテストを通るようにして、なんとかなりました。この作業は https://github.com/prettier/angular-html-parser/pull/30 で行いました。

ステップ 2 は簡単でした。https://github.com/prettier/angular-html-parser/pull/32https://github.com/prettier/angular-html-parser/pull/35 です。

ステップ 3 は難しかったです。angular-html-parser が返す AST が中途半端だったので、Prettier 側でもう少しいい感じの AST を組み立ててからプリントする必要がありました。当初私がそれを実装していたのですが、ダラダラしている間に Angular v17 がリリースされてしましました。

そのままダラダラし続けていたところ、別のコントリビューターがシンプルで筋の良い実装の PRを作ってくれました。私の実装では、明示的な preprocess ステップ内で AST の組み換えを行っていましたが、この実装ではもっとシンプルにプリントしながら構造を解釈していく方法を取っていました。こっちの方がシンプルだったし、バグが少なくなりそうだったので、ありがたくこの PR をマージさせてもらいました。オープンソースソフトウェア、最高。

その後すぐに Prettier 3.1 としてリリースしました

Angular チームとの関わり

この built-in control flow のサポートについては、Angular チームの方から連絡をいただいて、話を聞きながら実装をしていました(最終的には他の方の PR をマージしたのですが)。

色々とやりとりをしていたのもあって、Angular v17 のリリースブログ内で名指しで Prettier との連携について言及されてしまいました(もちろん、事前に私から許可を出しています)。

https://blog.angular.io/introducing-angular-v17-4d7033312e4b

We’re also in contact with Sosuke Suzuki from Prettier to ensure proper formatting of Angular templates.

まさかここまで個人名が出ると思っていなかったので、やや緊張感がありました。

ICU 式が壊れた

Buitl-in control flow をサポートした Prettier 3.1 をリリースしたら、今度は ICU 式がフォーマットできなくなったというバグが報告されました:

https://github.com/prettier/prettier/issues/15650

ICU 式 というのは、ある値の状況に応じてレンダリングする内容を切り替える機能です。

たとえば、以下の例では minits という変数が 0 であれば just now、1 であれば one minite ago、それ以外であれば {{minites}} minites ago をレンダリングします。plural はキーワードのようなもので、どのように分岐させるかを決定するようです。

<span i18n>Updated: {minutes, plural,
	=0 {just now}
	=1 {one minute ago}
	other {{{minutes}} minutes ago}
}</span>

plural 以外にも select というのがあります:

<span i18n>The author is {gender, select,
	male {male}
	female {female}
	other {other}
}</span>

この ICU 式が壊れる問題はどうやらパーサー側の問題でこうなってしまうらしく、適切なオプションを渡さないとシンタックスエラーになってしまうようでした。

3.1 以前の built-in control flow をサポートしていないときは、angular-html-parser は ICU 式を文字列として読み飛ばしていました(ICU 式の構文木を作らず、単に文字列として解釈する)。しかし、built-in control flow がサポートされたあとは、built-in control flow のブロックの構文と衝突してしまうようになりました。なので、適切なオプションを渡して ICU 式の中身までちゃんとパースするように修正する必要がありました。

angular-html-parser にオプションを付与する PR はコントリビューターの方が作ってくれました。

同じ方が Prettier 側の対応 PR も作成してくれました。ですが、この PR では AST を見てちゃんとプリントするのではなく、ICU 式の構文木のノードの開始位置から終了位置までもとの文字列を切り取って返すだけの実装になっていました。これはあんまり望ましくないので、ICU 式の構文木をちゃんと見てプリントする実装する PR を作成しました。

こちらはすでにマージ済みですがまだリリースできていません。3.1.1 としてリリースできると良いのですが、すでに main ブランチに minor 相当の機能がマージされてしまっているのでちょっとめんどくさくて放置してしまっています...。

今後

angular-html-parser をフォークとしてメンテし続けるのはキツそうなので、Angular の upstream に組み込めないかと考えています。まだ具体的には何も動いていないのですが、Angular チームの協力も得られそうなので頑張っていきたいと思います。フォークのメンテは本当に心が苦しくなるから。

Discussion