GitHub Actionsについて調べていたら、ひょんなことからworkflow commandsの存在とそれを使ってPull RequestのDiff Viewでメッセージが表示できることを知った。 (GitHub ActionsでESLintを動かした時にエラー表示してくれるアレ)
良い機会なのでTS Compiler APIの勉強も兼ねて型検査の結果をコード上に表示するためのGitHub Actionsを自作しようと思う。
できたもの
Embedded content: https://github.com/marketplace/actions/typescript-error-reporter
使い方
お手持ちのGitHub Actions workflowに2行追加するだけ
- name: TypeScript Error Reporter
uses: andoshin11/typescript-error-reporter-action@v1.0.0
キャッシュも含めてフルで書くとこんな感じ
name: main
on: [push]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.13.0]
steps:
- uses: actions/checkout@v1
- name: Prepare repository
run: git checkout "${GITHUB_REF:11}"
- name: Setting up Node.js v${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Restore dependencies
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: |
yarn install
- name: TypeScript Error Reporter
uses: andoshin11/typescript-error-reporter-action@v1.0.0
env:
CI: true
上記のように設定すると、Pull Request作成のタイミングで自動でCI上で型チェックが走るはず。
型エラーがあればJobがfailする
エラー部分はDiff viewでエラー内容がコメントされる。
手元のTypeScriptプロジェクト3つほどで試しに動かしてみましたが問題なさそうでした。 不具合があれば下記のRepositoryまでIssueください。
Embedded content: https://github.com/andoshin11/typescript-error-reporter-action
How it works?
本ツールがやっていることは主に3つ
- Step 0: コンパイラの初期化
- Step 1: コンパイル結果(
diagnostics
)の取得 - Step 2: Step 1の結果の整形およびGitHubへのレポート
Step 0: コンパイラの初期化
初期化処理としてTypeScriptの Language Service
およびその内部で利用する Language Service Host
を作成する。
TSの Language Service
が受け取ることのできるHostは非常に柔軟性が高く、TS Programが行うファイルアクセス処理(ディレクトリ取得、ファイル読み取り等)に対してかなりアグレッシブな介入を行うことができるのが特徴だ。
世のTS Compiler APIを利用したツールでよく見られる介入処理としては、メモリ上の仮想ファイルを媒介にしたRead高速化や本来TSが解釈できないファイルフォーマット( .vue
等)へのresolverの提供、 appendSuffix
処理等があり、エンジニアとしてはかなり創造性が刺激される仕組みの一つである。
Step 1: コンパイル結果( diagnostics
)の取得
先のStepで作成した Language Service
からTS Programの実態を取り出し静的解析を実行する。
このTS Programからは本ツールで利用している Semantic Diagnostics
の他に Global Diagnostics
, Syntactic Diagnostics
といった多様な解析結果を取得できるだけでなく、 AST
の取得やAST Nodeから型情報を取り出せる Type Checker
の取得といった様々な便利な情報を利用できる。もちろんEmitコマンドを実行すればコンパイル後の d.ts
ファイルや .js
ファイルの取得も可能だ。
また、TS Programの解析結果( diagnostics
)に含まれる内容はエラーや警告文の他に Suggestion
というカテゴリも存在する。
今回の本旨からは逸れるが、この Suggestion Diagnostics
からASTに対するCodeFix処理を自動適用可能なツールも着手中。柔軟にユーザー入力を受け取ったりDiagnosticsからの変換処理を行ってCodeFixを簡単にできないかなーと夢想している。
Step 2: 実行結果の整形およびGitHubへのレポート
GitHub Actions内からはworkflow commandsという仕組みを利用することができ、これらを用いて後続Actionへの値の受け渡しやUI上へのレポーティングといったリッチな処理を行うことができる。
Commandはリテラルの命令文で発行することも可能だが、公式の @actions/coreが提供する issueCommandを利用するのが個人的にオススメ。
TS Programの提供する diagnostics
にはコード上の位置情報も含まれるため、今回はそれをGitHub向けのcode location formatに変換することで対応した。
こだわりポイント
異なるTSバージョンへの対応
本ツールの実装には記事執筆時点で最新である typescript@3.8.3
を利用しているが、CI向けのツールとして提供する以上は各プロジェクトで利用されているバージョンのTypeScriptに解析処理は委ねたいところである。
ならば実行コンテキストで node_modules/typescript/lib/typescript.js
を探索して require
すれば良いかというと、それはそれでユーザーに事前の $ npm install
を強制してしまうため少々悩ましい。汎用的なGitHub Actionsとして公開する性質上、外部のコンテキストに依存することなく単独での実行を可能にするのがベストだ。
そのような理由を踏まえて今回はlockファイルから解析したバージョンの typescript.js
をCDN上から取得することにした。ネットワーク経由で取得したJSファイルをevalするというのは若干の気持ち悪さがあるが、一応の妥協点とする。
lib.d.tsの読み込み
上記のようにTypeScriptの本体は実行環境に合わせてCDN上から取得することとした。これによって一般的なJavaScriptランタイム上でCompiler APIを実行できるようになったわけである。めでたしめでたし。
・
・
・
・
・
とまぁそうは問屋が卸さないわけで、次に問題となるのが node_modules/typescript/lib
配下にある lib.*.d.ts
といったファイルたちだ。
一般的なECMAScriptの型定義(Promise, Array#map, Object#entries, etc..)はこれらのファイル内に定義されているためコンパイラの実行時にそれらが存在しなければ、 "Cannot find global type 'Promise'"というようなエラーが膨大に表示されてしまうのである。
コンパイラの実行ファイル自体はCDNで取得できるが、適切な実行結果を得るにはnpmを経由してこれらの lib.*.d.ts
filesを取得しなければならないという奇妙な状況は、まさにTypeScriptならではの問題だ。
解決策
そこで今回は苦肉の策として lib.*.d.ts
自体は手元の typescript@3.8.3
環境のものをGitHub Actionsバンドルに含めることにした。Compiler本体はバージョン差異が大きいことが予想されるが、ECMAScript Globalの型定義ならばバージョン差異が悪い方向に働くことはないだろうという楽観的判断によるものである。
実装の話をさせてもらうと、本ツールはWebpackでビルド & トランスパイルしたJSファイルをNodeで動かしている都合上そのままでは d.ts
ファイルを取り回すことはできないため、あらかじめ lib.*.d.ts
の記述内容およびType Referenceの依存ツリーを解析してJSON形式で出力するようなスクリプトを用意した。
Languaga Serviceからなんらかの lib.*.d.ts
ファイルにアクセスが発生する場合は、ファイル読み取り処理に介入してJSON上の値を返却するよう先述のLanguage Service Hostに手を加えることで対応している。
なかなか 泥くs 味わい深いロジックが散らばっているのでそのあたりの実装に興味がある数奇者な御仁は本ツールのソースコードを参照されたい。
端的に言って、二度とやりたくない。
キャッシュによるRead高速化
この手の解析ツールを実装する際にパフォーマンスのボトルネックとなるのはやはりIOである。 npmエコシステムの特殊さも合間って往往にしてプロジェクトのファイル依存ツリーは膨大なサイズとなるため、都度ファイルシステムにアクセスされてしまう設計ではなかなかにしんどい。
そのため今回はメモリ上の Map
オブジェクトをキャッシュレジストリとすることで、Read処理の高速化を図った。
比較的導入が簡単で即効性があるためCompiler APIを触る際にはほぼ必須なプラクティスであるが、ソフトウェアの性質によりASTに破壊的変更が発生する場合などはSnapshotのrevision管理がシビアなので要注意。
まとめ
近頃、自分の観測範囲でもTS Compiler APIのユースケースが徐々に増えてきたように思える。 まだまだドキュメントが整備されていなかったり、問題にハマった際に泣きつく場所がなかったりと課題はあるものの、使いこなせればTypeScriptが持つ強力な型システムの恩恵を様々な場で受けることができるのでユーザーが広まっていくといいなぁ
P.S. TypeScriptにcustom reporterを渡せればわざわざGitHub Actionsを作る必要もなかった気がする