🧊

Prettierプラグインの仕組みと人気プラグインの実装

Prettierのプラグインとは、具体的にどういうものなのか?を真に理解したく。

ESLintほど、独自プラグインを作る文化もないと思う。

基本デザイン

Plugins · Prettier https://prettier.io/docs/plugins

まず冒頭に書いてある通り。

Plugins are ways of adding new languages or formatting rules to Prettier.

・・・というわけで、基本的には、Prettierがビルトインでサポートしていない拡張子をサポートするための仕組み。

プラグインという言葉から、JS/TSみたくビルトインでサポートされてるファイルをフォーマットする過程に介入できる?と思いがちだが、そうではない。 いや、もちろんやれないことはないと思うけども。

実装を見ると、以下をexportするJSのモジュールがプラグインである、と書いてある。

  • languages: どういう拡張子に対して利用したいか、どのパーサーを使うか
  • parsers: どういうASTを出力するのか、実際のパース関数
  • printers: どのASTに対応するのか、事前処理、実際のプリント関数
  • options: サポートするオプションのスキーマ
  • defaultOptions: デフォルトオプション

parsersの型で、特に気になるところはこう。

function preprocess(text: string, options: object): string;

function parse(text: string, options: object): Promise<AST> | AST;

printersはこう。

function preprocess(ast: AST, options: Options): AST | Promise<AST>;
function getVisitorKeys(node, nonTraversableKeys: Set<string>): string[];

function print(
  // Path to the AST node to print
  path: AstPath,
  options: object,
  // Recursively print a child node
  print: (selector?: string | number | Array<string | number> | AstPath) => Doc,
): Doc;

function embed(
  // Path to the current AST node
  path: AstPath,
  // Current options
  options: Options,
):
  | ((
      // Parses and prints the passed text using a different parser.
      // You should set `options.parser` to specify which parser to use.
      textToDoc: (text: string, options: Options) => Promise<Doc>,
      // Prints the current node or its descendant node with the current printer
      print: (
        selector?: string | number | Array<string | number> | AstPath,
      ) => Doc,
      // The following two arguments are passed for convenience.
      // They're the same `path` and `options` that are passed to `embed`.
      path: AstPath,
      options: Options,
    ) => Promise<Doc | undefined> | Doc | undefined)
  | Doc
  | undefined;

そういう意味では、ここにある単位でしかプラグインとして介入できない。

Prettier本体とプラグインの責務の分担としては、

  • プラグイン: まず実装する
  • 本体: そのプラグインを読み込み、サポートする拡張子リストや使用する実装を更新
  • 本体: ファイルを処理するとき、その拡張子を担当するプラグインを選ぶ
  • 本体: そのプラグインのパーサーを使う
  • プラグイン: そのファイルをparse()してASTを返す
  • 本体: パースされたASTを走査しながら、Docを貯めていく
  • プラグイン: AstPath形式で特定のノードが渡されるので、それをPrettierのIRであるDocにして返す
  • 本体: Docを文字列に変換して、フォーマット完了

という感じ。

AstPathは、どんなASTでも走査できるようにデザインされた汎用的なクラス。

https://github.com/prettier/prettier/blob/9e9f65e7b9277e7af12362628b42e003393731e6/src/common/ast-path.js

公開されてるAPIではなくて、内部的にprintAstToDoc()するときに生成される。

https://github.com/prettier/prettier/blob/9e9f65e7b9277e7af12362628b42e003393731e6/src/main/ast-to-doc.js#L34

print()は、ASTの該当ノードを含むpath: AstPathを受け取って、Docを返す処理を実装する。

function print(path, options, print) {
  const node = path.node;

  switch (node.type) {
    // ...
    return group(["..."]);
  }
}

Prettier内部でやってるのと同じで、コールバックとしてのprint()を呼び戻す必要があるところまで一緒。

で、Docを返すためには、Prettier IRの形を知っている必要があるので、そのためのヘルパーとしてビルダー関数たちが公開されてる。

https://github.com/prettier/prettier/blob/9e9f65e7b9277e7af12362628b42e003393731e6/src/document/public.js

人気プラグイン

https://github.com/wooorm/npm-high-impact/blob/main/lib/top.js

今年の6月のデータなので、少し古いかもしれないが。

  • 1st / 3543: prettier-plugin-tailwindcss
  • 2nd / 5458: @trivago/prettier-plugin-sort-imports
  • 3rd / 5539: prettier-plugin-organize-imports
  • 4th / 6522: prettier-plugin-packagejson
  • 5th / 6815: @ianvs/prettier-plugin-sort-imports
  • 6th / 6822: prettier-plugin-svelte
  • 7th / 7626: prettier-plugin-solidity
  • 8th / 7656: prettier-plugin-java
  • 9th / 7717: prettier-plugin-jsdoc

Tailwindが人気すぎ・・。

とはいえimportを並べ替えるプラグインが3つもあったりと、まあそのへんか。

人気プラグインの仕組み

  • prettier-plugin-astro
  • prettier-plugin-svelte
  • @ianvs/prettier-plugin-sort-imports
  • prettier-plugin-tailwindcss

このあたりがどういう実装になってるかみておく。

prettier-plugin-astro

withastro/prettier-plugin-astro: Prettier plugin for Astro https://github.com/withastro/prettier-plugin-astro

特筆すべきはこれくらい。

Prettierビルトインのパーサーを使ってるのもあって、プラグイン実装としてはそんなに多くない。 お作法に則ってるだけ。

prettier-plugin-svelte

sveltejs/prettier-plugin-svelte: Format your svelte components using prettier. https://github.com/sveltejs/prettier-plugin-svelte

Astroとだいたい同じかと思って読み進めてたけど、ぜんぜん違ったし、どちらも単にscriptstyleを埋め込みで処理して終わり、ではない。

とはいえ、プラグインとしてはこれがスタンダードで、他の言語向けのプラグインも同様の構成になってる。 埋め込み言語がないだけ、もっとシンプルなくらい。

@ianvs/prettier-plugin-sort-imports

IanVS/prettier-plugin-sort-imports: An opinionated but flexible prettier plugin to sort import statements https://github.com/ianvs/prettier-plugin-sort-imports

ここからは毛色がまた違う。 新たな拡張子をサポートするものがプラグインなら、既知の拡張子に対する処理へ介入する”メタ”プラグインといったところか。

元コード > ソート用AST > ソート済AST > ソート済みコード > フォーマット用AST > フォーマット済みコードという流れ。

最大のポイントは、“同じ名前でparsersを登録することで、既存のビルトインの挙動を変える”ってところ! そのため、最後に読み込ませる必要がある。

prettier-plugin-tailwindcss

tailwindlabs/prettier-plugin-tailwindcss: A Prettier plugin for Tailwind CSS that automatically sorts classes based on our recommended class order. https://github.com/tailwindlabs/prettier-plugin-tailwindcss

これも、いわゆるメタなプラグインで、プラグインの最後に読み込むことを要求してる。 が、そうすると、同様のsort-importsみたいなプラグインと競合することになる。

という問題を解決するために、このプラグインは内部で互換リストを持ってて、それぞれに応じたワークアラウンドを実装として持ってるとのこと・・・。

https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins

さて。

Svelteの場合だけ特別な処理フローになってた。

npmのダウンロード数ランキングなんかもみてみたけど、これがおそらくPrettierプラグイン界隈の親玉みたいな存在になってる。

まとめ

独自プラグインを作る文化がない理由がおわかりいただけただろうか・・・!

プラグインはともかく、メタプラグインに関しては、もはやハックに近いな〜というのが正直な感想。

oxfmtでどうやってこのあたりを統合するかもずっと考えてるけど、まだ道筋が見えてない。

特にTailwindは需要も多いけど、やってる内容が一番アクロバティックなので・・・。