単に動くコード文字列を生成できるのは最低限として、
- JSだけじゃなくて、TSも対応したい
- なんならJSXもサポートする
- Stage3くらいのプロポーザルにも、できれば対応したい
- できることならコメントも復元したい・・・
という場合に、現状どういう実装パターンがあるのかを調べたかった。
どのASTを選ぶか
これがほとんど決定的な要因になってる。
- ESTree
- TS-ESTree
- Babel
- SWC
- TypeScript
ざっと調べるとこれくらい?
コードをパースしてASTを取得するところまでは、以下のライブラリでやれそう。
- ESTree
- TS-ESTree
- Babel
- SWC
- TypeScript
というわけで、どれを使うかによって後続のツールも決まってくる。
@sveltejs/acorn-typescript
もTS-ESTreeを出力しようとしてるけど、完全ではないみたい。
Further align with
@typescript-eslint/types
· Issue #7 · sveltejs/acorn-typescript https://github.com/sveltejs/acorn-typescript/issues/7
あとは、ESTreeをBabelに戻そうという試みもあった。
coderaiser/estree-to-babel: convert estree ast to babel https://github.com/coderaiser/estree-to-babel
ちなみにBabelとしても、Babel ASTをESTree化するプラグインがある。
babel/packages/babel-parser/src/plugins/estree.ts at main · babel/babel https://github.com/babel/babel/blob/main/packages/babel-parser/src/plugins/estree.ts
ではコード生成の話へ。
Babel, SWC, TypeScript
まずはそれだけで完結する帝国シリーズ。
@babel/generator
おそらくもっともシェアがあり信頼度も高いパターン、と勝手に思ってる。
言わずもがなあらゆる構文がサポートされてるし、@babel/traverse
でASTを変更して、@babel/generator
でコードに戻せる。
@babel/generator · Babel https://babeljs.io/docs/babel-generator
コメントについてもデフォルトで対応されてるご様子。
ちなみに、こういう用途ではrecast
も割とよく聞く気がする。
benjamn/recast: JavaScript syntax tree transformer, nondestructive pretty-printer, and automatic source map generator https://github.com/benjamn/recast
ただデフォルトのパーサーはesprima
なので、最新の構文やTSに対応するためには、結局Babelを指定して使うことになりそう。
@swc/core
printSync()
というAPIは確かに存在してて、手元ではコードも動いてたけど、なぜかドキュメントに載ってない。
もしかしたら非公認なのかもしれない?
@swc/core https://swc.rs/docs/usage/core
あと、コメントは対応してなかった。
ts-morph
TypeScript本家をそのまま使いたい人はそうそういないと思うので、ts-morph
を見ておく。
project.createSourceFile()
して、TS ASTをよしなに更新したら、sourceFile.getFullText()
でコード文字列に戻せる。
ts-morph - Transforms https://ts-morph.com/manipulation/transforms
コメントもちゃんと出力されてた。
問題は、ASTを変更するAPIが複雑なのと、パフォーマンスが気になるところ。
(TS-)ESTree
次にエコシステム重視なシリーズ。
escodegen
estools/escodegen: ECMAScript code generator https://github.com/estools/escodegen
2年前で更新が止まっていた・・・!
JSXサポートしたforkも11年前に。
dary/escodegen-jsx: ECMAScript code generator with JSX https://github.com/dary/escodegen-jsx?utm_source=chatgpt.com
astring
davidbonnet/astring: 🌳 Tiny and fast JavaScript code generator from an ESTree-compliant AST. https://github.com/davidbonnet/astring
これもよく聞くかも。
コメントの対応は一応あるけど、そのためにはastravel
を併用しないといけないとのこと。
davidbonnet/astravel: 👟 Tiny and fast ESTree-compliant AST walker and modifier. https://github.com/davidbonnet/astravel#attachcomments
いわゆるdanglingComments
はよしなに配置されちゃう。
しかし、TSの対応はないらしい。
Generating a typescript file · Issue #701 · davidbonnet/astring https://github.com/davidbonnet/astring/issues/701
有志によるTSのプラグインはあった。
vardario/astring-ts-generator https://github.com/vardario/astring-ts-generator
JSXも、外部のプラグインで拡張しないといけない。
Qard/astring-jsx: 🕵🏻 A wrapper around https://www.npmjs.com/package/astring, adding JSX support https://github.com/Qard/astring-jsx
estree-util-to-js
syntax-tree/estree-util-to-js: estree (and esast) utility to serialize as JavaScript https://github.com/syntax-tree/estree-util-to-js
unifiedjs
チームのやつで、こちらも2年前で止まってる。
実装としてはastring
に依存してて、JSXサポートも内部的に持ってる。
コメントに関しては、別のライブラリが用意されてる。
syntax-tree/estree-util-attach-comments: utility to attach comments to estree nodes https://github.com/syntax-tree/estree-util-attach-comments/tree/main
astravel
のそれとの違いは、Comment
のデータの持ち方が違うのと、アルゴリズムも微妙に違う。
TSの対応はなさそう。
esrap
sveltejs/esrap: Parse in reverse https://github.com/sveltejs/esrap
安心と信頼のSvelteチーム製のやつ。(元々は個人の方が作ってて、それの更新停止を期に派生したらしい)
ドキュメントによると、acorn
のESTreeと、TS-ESTreeにも対応しているとのこと。
ただ、TS-ESTreeは@typescript-eslint/typescript-estree
由来ではなく、先述のsveltejs/acorn-typescript
によるASTらしいので、先述の通り微妙に差分がある。
そして、
- JSXに対応してない
- Stage3のような新しいものも対応してなさそう
a: string | null
のTSNullKeyword
になぜか対応してない
という惜しい感じだった。Svelteコンパイラの内部用途のために用意されてて、そこでは今のところ困ってないってことかな?
コメントの対応も実装としては入ってるようだったけど、どうやら自分で任意のASTノードにleadingComments
とtrailingComments
を差し込まないといけないようだった。
https://github.com/sveltejs/esrap/blob/668872702d43a7542abbaeb20cac886f6988c35a/test/common.js#L14
ざわ・・・。
やっぱりPrettier
👻 < 呼んだ?
というわけで、まあやりたいことはPrettierのそれよね・・・。
コメントはどこまでいっても鬼門であり、厳密にやろうとすればそれはもうPrettier相当を実装することになるってこと。
そもそもTS-ESTreeをいい感じに出力できるのも、Prettierくらいしかなさそう。
Is it possible to print the AST produced by typescript-estree? · typescript-eslint/typescript-eslint · Discussion #6377 https://github.com/typescript-eslint/typescript-eslint/discussions/6377
ただ今回の用途でPrettierを使うとすると、__debug.formatAST()
という明らかに気が引けるAPIを使うのがまず1つ。
import { parseSync } from "oxc-parser";
import { __debug } from "prettier";
const { program, comments } = parseSync("a.tsx", CODE, {});
program.comments = comments;
// ...
const f = await __debug.formatAST(program, {
...options,
originalText: CODE,
});
console.log(f.formatted);
このルートは、Prettierが内部的にやってるASTの精査ステップみたいなものを通らないはずなので、それはそれで意図しない結果になるかもしれない。
もしくは、カスタム何もしないパーサーとして使う。
import { parseSync } from "oxc-parser";
import { format } from "prettier";
const { program, comments } = parseSync("a.tsx", CODE, {});
program.comments = comments;
// ...
const f = await format(CODE, {
parser: "typescript",
plugins: [
{
parsers: {
typescript: {
parse: () => program,
astFormat: "estree",
locStart: (n) => n.start,
locEnd: (n) => n.end,
},
},
},
],
});
正攻法だが冗長。けど、これが一番確実で妥当なコードが生成できるという・・・。 ただまあ別にPrettyにしてほしいわけではないし、パフォーマンスも気になるし、という悩みもある。
まとめ
やはりこういう一連の流れを完結させるためには、Babelみたいな一貫したツール群を使うほうが安牌に感じる。
(TS-)ESTreeは、部品を取っ替え引っ替えできるのはいいことではありつつ、ズバリで欲しいものが存在しないこともある。まぁ作ればいいんやけども。
少なくとも現時点で、TS-ESTree+JSXに対応したコード生成ライブラリは、Prettier以外見つけられなかった。
さて、チームOXCとしてはどうしようか・・・。