ts-morphとは
https://github.com/dsherret/ts-morph
https://ts-morph.com/
元は ts-simple-ast
という名前で、TypeScriptのASTを簡単に触れるようにしたラッパーで、ASTのナビゲーションやファイル操作、カスタムトランスフォーマーが簡単に使えるようなAPIを提供してくれている
どんなときに使うの?
- 既存資産の大規模リファクタ用スクリプト
- 機械的な置換だけじゃ対応しきれない、コンパイラが知ってる情報を使った変換をしたいときに利用出来る
- ファイル内の特定の関数やinterfaceを別ファイルに分離したいとか
- 開発補助ツールとして
- カスタムlinterとかカスタムトランスフォーマーとかジェネレータとか
- Angular CLIのgenerateみたく、ファイル生成+ファイル変更みたいなことがしたいときにファイル変更がやりやすくなる
-
@Takepepe さん作のvuexの型自動生成ツール みたいなの
- (こちらはCompilerAPIを生で使っているが、こういうのを楽に作れる(はず
- カスタムlinterとかカスタムトランスフォーマーとかジェネレータとか
使い方
最低限のセットアップ
TypeScriptのプロジェクトを触るための初期セットアップコードはこれだけ
import { Project } from 'ts-morph';
const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });
この project
はTypeScriptプロジェクトに関する情報をよしなに作ってくれて、TypeScript自体のLanguageServiceやCompilerAPIへはこの project
を経由してアクセスが出来るようになっている
AST Navigation
AST Nodeの走査やファイル内のinterfaceのみを抽出するメソッド、コンパイルエラーの取得などの便利メソッドがある
import { Project } from 'ts-morph';
const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });
const file = project.getSourceFile('path/to/file.ts');
const diagnostics = file.getPreEmitDiagnostics(); // コンパイルエラー一覧取得
const statements = file.getStatements(); // ファイル内の文を取得。トップレベルに定義されている定義などが列挙されるっぽい
const interfaces = file.getInterfaces(); // interfaceだけ取ってくるとかもできちゃう
file.forEachDescendant(node => myVisitors.forEach(v => v.visit(node))); // 子孫Nodeを列挙してくれる。生CompilerAPIだと自前で再帰しないと末端Nodeまで辿れないけどこいつは全部出してくれる風味(ちゃんと調べてないので嘘かも
一度生CompilerAPIを触ったことある人なら↑のメソッドだけでも便利さが伝わると思う
一々Nodeを走査してDeclarationStatementか判定して・・・とかやって取ってきてた情報がメソッド一発なので超便利
ソースコードの変更
import { Project } from 'ts-morph';
async function main(){
const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });
const file = project.getSourceFile('path/to/file.ts');
file.move('path/to/newFile.ts'); // ファイル移動。もちろん参照してるファイルのimport文なんかも変更される
file.organizeImports(); // import文の整理してくれるやつ
file.getInterface().setIsExported(true) // 任意のDeclarationにexportつけるためのメソッドまであって便利
await file.save(); // fileへの変更の保存
await project.save(); // project全体の変更の保存。操作したファイル以外に影響する変更が保存される
}
main().catch(console.error);
変更系も便利メソッドが豊富で非常に扱いやすい
コンパイル結果出力
import { Project } from 'ts-morph';
async function main(){
const project = new Project({ tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json') });
await project.emit(); // project全体のコンパイル結果出力
const file = project.getSourceFile('path/to/file.ts');
await file.save(); // file単体の出力も可能っぽい
}
main().catch(console.error);
emit周りは全くいじってないのであんまわからない。。
おまけ
実際に自分が書いたスクリプト
このスクリプトは、 src/modules/**/*MT.ts
ファイルに置かれていたinterface郡を interface.ts
に分離して、import/exportを解決ということをしている
単純にinterface定義を別ファイルに移動するだけだとTypeScriptのリファクタリング機能ではimportの解決が出来ない(エディタで同様のことをやったときのことを想像してもらえればと)ので、
- interfaceを別ファイルへ移動
- コンパイルエラーが出ているimport文を削除
- fixMissingImportsを実行
という手順でファイルの変更を実施している(ちょっと強引)
なお、この fixMissingImports
は同名の変数が複数あると適切なのを選ぶわけじゃなく、候補の一番上のものをimportしちゃうので、そこだけは手で直す必要があるのでちょい残念
というわけでts-morph便利なのでみんな使おう