一見簡単に見えるこれが意外と大変だった・・・ので、その学びをメモ。
ブラウザで動きつつ、型チェックオプションも有効化できるtypescript-estree
は、ここで試せます!(宣伝)
@typescript-eslint/typescript-estree
TypeScriptのASTといえばTS-ESTreeというわけで、現状のデファクトであるパーサー実装がコレ。
typescript-eslint/typescript-eslint: :sparkles: Monorepo for all the tooling which enables ESLint to support TypeScript https://github.com/typescript-eslint/typescript-eslint
パーサー実装の本体であるtypescript-estree
の実装は、
- ファイルをTSCでパース
- 型チェックが必要なら、TSの
Project
を用意してDiagnostics
を取得
- 型チェックが必要なら、TSの
- それをESTreeライクに整形
という2段階の処理になってる。
@typescript-eslint/parser
と@typescript-eslint/typescript-estree
の関係 | Memory ice cubes https://leaysgur.github.io/posts/2025/05/08/123105/
で、ASTはブラウザで見たいよねってなった時に、愚直にimport
してビルドしようとすると、まぁエラーになるんだな〜これが。
いろんな事情があるけど、基本的にはTypeScriptとESLintをブラウザで動かすのが簡単ではないってところに起因してると思う。
なので、それぞれの問題を解決してやれば、ブラウザでも動かせるようになる。ただただ面倒なだけ。
今回は、TS-ESTreeのASTだけでなく、型チェックオプションであるerrorOnTypeScriptSyntacticAndSemanticIssues
も使えるようにしたかったので、余計に大変だった。
先行者たち
たとえば、ast-explorer
の場合。
AST Explorer https://ast-explorer.dev/
unenv
のモックを駆使してrolldown
でブラウザ向けに事前ビルドしておいて、それを使ってる。
単にASTが欲しいだけなら、これがもっともクリーンな正攻法になると思う。
ただし、typescript-estree
ではなくparser
のほうを使ってるので、型チェックはできてない。
たとえば、astexplorer
の場合。
AST explorer https://astexplorer.net/
こちらも同様に、webpack.config.js
で、同じようにNode依存の部分をモックしてる。
最後に、本家のPlaygroundの場合。
型チェックもできてるように思えたので仕組みを追ってみたところ、
- どうやら別建てでTSCを動かしつつ
monaco
エディターとも連携したり- その流れでTS-ESTreeを表示したり
と、だいぶがんばってた。
ともあれ方向性としてはこれが求めていたものなので、TSCとTS-ESTreeを分けて考えることにした。
TypeScriptをブラウザで動かして型チェックもする
というわけで、自作していく。
公式によると、ブラウザであれこれしたい場合は、Virtual File Systemを使うというのが筋らしい。
TypeScript-Website/packages/typescript-vfs at v2 · microsoft/TypeScript-Website https://github.com/microsoft/TypeScript-Website/tree/70bf492500c6b2f2296ec28fc0d825d5a98ffd86/packages/typescript-vfs
ただ・・・、READMEをチラ見するとすぐ気付くと思うけど、それなりに難解なので、これまた既存の便利なライブラリを利用する。
ts-morph/packages/bootstrap at latest · dsherret/ts-morph https://github.com/dsherret/ts-morph/tree/a1c61c7e04928de5d8fc12f32a76e3ed5b7ffea1/packages/bootstrap
みんな大好きts-morph
には、ずばりな用途のサブパッケージがあって、@ts-morph/bootstrap
が顧客が本当に欲しかったもの。
これを使うと、なんとこれだけのコードで、ブラウザで型チェックができちゃう。
import { createProject, ts } from "@ts-morph/bootstrap";
const project = await createProject({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile("main.ts", "let a: number | null = null;");
const diagnostics = ts.getPreEmitDiagnostics(project.craeteProrgram());
便利すぎる。
このsourceFile
がTS ASTになってるので、あとはこれをTS-ESTreeに変換するだけ。
@typescript-eslint/typescript-estree/use-at-your-own-risk
次に、TS ASTをTS-ESTreeに変換したいけど、それ用の処理は表向きには公開されてない・・・。
が、さっき見てたPlayground用に抜け道が公開されてるので、それを使う。use-at-your-own-risk
という名に従いながら。
astConverter()
が欲しかったやつで、sourceFile
を渡せばそれで終わり。
import { createProject } from "@ts-morph/bootstrap";
import { astConverter } from "@typescript-eslint/typescript-estree/use-at-your-own-risk";
const project = await createProject({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile(filename, code);
const { estree } = astConverter(sourceFile, options, false);
これでTS-ESTreeのASTが手に入るので、あとはよしなに表示すればよい。
型エラーに関しては、どうやらTSから発せられたすべてのエラーをthrow
してるわけではないらしかった。
というわけで、このファイルに従ってフィルターしてあげる。
これで、typescript-estree
のparseAndGenerateServices()
という一番ローレベルなAPIを再現できたというわけ。
一連のコードはここにある。
これが最小手数なのでは?と思ってるけど、もっとシュッとできる方法があれば教えてください!
感想
大変だった・・・!
そして遅延ロードできるようにはしてるものの、やっぱりファイルサイズがデカいのよな〜。なんと9MBくらいかかる。
それに引き換えoxc-parser
なら、2MBいかないくらいで済むし、WASMなのでブラウザでも動くし、だいぶ敷居が低いと思う。
TSの型エラーは完全に対応できてないけど、そこはこれからに期待しよう。