🧊

JSX/TSXのASTを、できる限り元のコード文字列に戻したい

単に動くコード文字列を生成できるのは最低限として、

  • JSだけじゃなくて、TSも対応したい
  • なんならJSXもサポートする
  • Stage3くらいのプロポーザルにも、できれば対応したい
  • できることならコメントも復元したい・・・

という場合に、現状どういう実装パターンがあるのかを調べたかった。

どのASTを選ぶか

これがほとんど決定的な要因になってる。

  • ESTree
  • TS-ESTree
  • Babel
  • SWC
  • TypeScript

ざっと調べるとこれくらい?

コードをパースしてASTを取得するところまでは、以下のライブラリでやれそう。

というわけで、どれを使うかによって後続のツールも決まってくる。

@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

https://github.com/swc-project/swc/blob/81fcd01680e8333154cbc1265d54d83b4b78eb02/packages/core/src/index.ts#L200

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 | nullTSNullKeywordになぜか対応してない

という惜しい感じだった。Svelteコンパイラの内部用途のために用意されてて、そこでは今のところ困ってないってことかな?

コメントの対応も実装としては入ってるようだったけど、どうやら自分で任意のASTノードにleadingCommentstrailingCommentsを差し込まないといけないようだった。

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としてはどうしようか・・・。