🧊

PrettierのCSSサポートについて

PrettierのGraphQLサポートについて | Memory ice cubes https://leaysgur.github.io/posts/2026/06/16/125413/

GraphQL編に続いて、CSS編。今回もバージョンは3.8.4時点。

CSSはやばいと話だけは聞いてたけど、少しだけそれがわかった気がした。

パーサーはなんと6種類

GraphQLはgraphql-js一発だったが、CSSはリレー形式。

  • postcss: メインのAST構築
  • postcss-less: LESS方言用のsyntax差し替え
  • postcss-scss: SCSS方言用のsyntax差し替え
  • postcss-selector-parser: セレクタ(LHS)の再パース
  • postcss-values-parser: 値(RHS)の再パース
  • postcss-media-query-parser: @mediaの値の再パース

後段の再パースに使ってるやつらは、歴史的経緯により、古いバージョンに固定されたまま。

Note about updating postcss-values-parser (May 2023) · Issue #14814 · prettier/prettier https://github.com/prettier/prettier/issues/14814

CSS/SCSS/LESSの3パーサーは、全部src/language-css/parser-postcss.jsがエントリーポイント。 options.parserで分岐して、postcss / postcss-scss / postcss-lessを呼び分ける。

ただ、postcss(-scss|less)は、大枠のノードに分割するだけで、セレクタや値の中身は生文字列のまま。 なので、必要に応じて詳細化するという流れ。

  • セレクタ: parse-selector.jspostcss-selector-parserを呼ぶ
  • 値: parse-value.jspostcss-values-parserを呼ぶ
  • @media: parse-media-query.jspostcss-media-query-parserを呼ぶ

parseのpostprocess

2段階パースしただけだと整形に必要な情報が足りないのか、parser-postcss.jsparseNestedCSS()でガッツリ補正してる。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parser-postcss.js#L24

  • at-ruleの名前による分岐(後述)
  • LESS変数の救出(後述)
  • SCSSの!default/!globalをノードのフラグに立て直し(後述)
  • url()内のSCSS補間の特別扱い(後述)

Lessは、入力テキストの前処理として、インラインコメント内のクォート置換もやってるとのこと。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parser-postcss.js#L430

ただリンクされてるissue的にはもう修正済みってなってるので、デッドコードなのかも?

quotes in comments break stylelint in less mode · Issue #145 · shellscape/postcss-less https://github.com/shellscape/postcss-less/issues/145

@xxxは標準もSCSSもLESSも区別しない

parseNestedCSS()内のat-rule処理は、ハードコードされた名前リストで分岐している。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parser-postcss.js#L289-L376

つまり、標準CSSのat-ruleとSCSS独自のat-ruleが同じリストに入って、同じパスで処理されている。 define-mixin/add-mixinに至ってはpostcss-mixinsプラグインの独自構文で、SCSSでもLESSでもない。

他にも、importは標準CSSだが、useforwardはSCSS独自(Sass modules)。これが同じSetで同じmodule系at-ruleとして扱われてる。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/utilities/is-module-rule-name.js#L1

方言区別はほぼなく、新しいat-ruleが標準入りしても、ここのリストに追加されるまでは値がparseValue()されず、原文字列のまま素通しになる。

ただ、この素通しのおかげで救われるケースもある。

@apply text-red-500 hover:bg-blue-700のようなTailwindの記法を、もし値パーサーに通すと、次のような事故が起きる。

  • dark:bg-x:をproperty colonと誤認し、スペース挿入されてdark: bg-x
  • @custom-variant dark (&:is(.dark))(...)を関数呼び出しと誤認して、dark(&: is(.dark))

対応してないが結果として対応できてることになるという、ちょっと逆説的な構造。(整形はできないけど・・・)

SCSS固有のフラグ

postcss-scssが返すノードに!default/!globalがついていたら、Prettier側でscssDefault/scssGlobalフラグを立て直す処理がある。 標準化された型情報じゃなくて、フラグを後付け。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parser-postcss.js#L165-L184

@mixin / @include の引数

SCSSの@mixin foo($a, $b)$a, $b部分も、SCSS用の特別なパーサーは無い。parseValue()に渡される前に、正規表現で空白を整形してからpostcss-values-parserに流される。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parser-postcss.js#L345-L351

つまりSCSS制御構文の引数は、CSSの値パーサーが解釈している。postcss-values-parserはSCSS変数$xも値の一部として読めるから。

LESS変数の救出処理

LESSの@color: blue;postcss-lessが変数として認識してくれるが、下記2パターンは認識してくれない・・・。

  • @color:blue;: コロン前後にスペースがない
  • @color :blue;: コロン前にスペースがある

これをPrettier側でnode.name.includes(":")したり、node.params?.[0] === ":"したりして救出してる。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parser-postcss.js#L244-L274

parse/parse-selector.js

postcss-selector-parserでセレクタの再パース。

コメント検出で先にfallback

セレクタ内に///*があると、postcss-selector-parserがコメントの中身までセレクタとしてパースしようとして壊れる。事前検知してselector-unknown扱い。文字列リテラル内のコメントは除外するという手の込みよう。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parse/parse-selector.js#L9-L14

というか、CSS系の処理は基本的に、“構造化できないものはverbatimに出す”のが鉄板の流れになってる。ほんとよく頑張ってるよ・・・。

parse/parse-value.js

postcss-values-parserを呼んでるところで、ここでもいろいろ補正してる。

50... workaround

SCSS arbitrary arguments の 50...postcss-values-parser50. + unit .. と誤パースする。それを手動で 50 + unit ... に直す・・・。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parse/parse-value.js#L33-L44

selector() 関数の中身は、セレクタとして再パース

@supports selector(...)用のCSS関数。中身は値ではなくセレクタなので、値パーサーで読んだ結果のサブツリーを、セレクタパーサーの結果でまるごと差し替える。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parse/parse-value.js#L46-L56

url() の中身を文字列に潰すケース

SCSS補間(#{})が含まれる場合、または、文字列も関数もSCSS変数も含まない素朴な値(クオートなし裸URL)の場合、url()の引数をまるごと文字列として再構築する。

postcss-values-parserがurl仕様(クォート無し、空白OKなど)に対応しきれないのでバイパスしてる。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parse/parse-value.js#L58-L80

LESSのインラインJSは諦め

LESSの@var: ~`js code`;形式のJavaScript埋め込みは、パースせずvalue-unknownで素通り。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parse/parse-value.js#L179-L181

LESSってJS埋め込めんの・・・まじか・・・。

parse/utilities.js

3つのパーサー結果を統合するためのglue。地味だが構造的に大事。

addTypePrefix: 型名の事後正規化

3つのパーサーが返すノード型に、後付けでprefixを貼る関数。

  • selector-parser: selector-*
  • values-parser: value-*
  • media-query-parser: media-*

同じ型でも、セレクタ由来か値由来かを後付けで区別するという事後正規化で、3パーサーを統一名前空間に押し込んでる。

https://github.com/prettier/prettier/blob/1c6ba5539141552e0e8e22d401ea620d8fdff468/src/language-css/parse/utilities.js#L3-L19

まとめ

続・Prettier互換ですって言うのは大変。

まあでもCSSだってJSと同じくらい歴史があるのなら、それだけ闇が深くてもまあ当然か・・・。