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.jsがpostcss-selector-parserを呼ぶ - 値:
parse-value.jsがpostcss-values-parserを呼ぶ @media:parse-media-query.jsがpostcss-media-query-parserを呼ぶ
parseのpostprocess
2段階パースしただけだと整形に必要な情報が足りないのか、parser-postcss.jsのparseNestedCSS()でガッツリ補正してる。
- at-ruleの名前による分岐(後述)
- LESS変数の救出(後述)
- SCSSの
!default/!globalをノードのフラグに立て直し(後述) url()内のSCSS補間の特別扱い(後述)
Lessは、入力テキストの前処理として、インラインコメント内のクォート置換もやってるとのこと。
ただリンクされてる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処理は、ハードコードされた名前リストで分岐している。
つまり、標準CSSのat-ruleとSCSS独自のat-ruleが同じリストに入って、同じパスで処理されている。
define-mixin/add-mixinに至ってはpostcss-mixinsプラグインの独自構文で、SCSSでもLESSでもない。
他にも、importは標準CSSだが、useとforwardはSCSS独自(Sass modules)。これが同じSetで同じmodule系at-ruleとして扱われてる。
方言区別はほぼなく、新しい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フラグを立て直す処理がある。
標準化された型情報じゃなくて、フラグを後付け。
@mixin / @include の引数
SCSSの@mixin foo($a, $b)の$a, $b部分も、SCSS用の特別なパーサーは無い。parseValue()に渡される前に、正規表現で空白を整形してからpostcss-values-parserに流される。
つまり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] === ":"したりして救出してる。
parse/parse-selector.js
postcss-selector-parserでセレクタの再パース。
コメント検出で先にfallback
セレクタ内に//や/*があると、postcss-selector-parserがコメントの中身までセレクタとしてパースしようとして壊れる。事前検知してselector-unknown扱い。文字列リテラル内のコメントは除外するという手の込みよう。
というか、CSS系の処理は基本的に、“構造化できないものはverbatimに出す”のが鉄板の流れになってる。ほんとよく頑張ってるよ・・・。
parse/parse-value.js
postcss-values-parserを呼んでるところで、ここでもいろいろ補正してる。
50... workaround
SCSS arbitrary arguments の 50... をpostcss-values-parserが 50. + unit .. と誤パースする。それを手動で 50 + unit ... に直す・・・。
selector() 関数の中身は、セレクタとして再パース
@supports selector(...)用のCSS関数。中身は値ではなくセレクタなので、値パーサーで読んだ結果のサブツリーを、セレクタパーサーの結果でまるごと差し替える。
url() の中身を文字列に潰すケース
SCSS補間(#{})が含まれる場合、または、文字列も関数もSCSS変数も含まない素朴な値(クオートなし裸URL)の場合、url()の引数をまるごと文字列として再構築する。
postcss-values-parserがurl仕様(クォート無し、空白OKなど)に対応しきれないのでバイパスしてる。
LESSのインラインJSは諦め
LESSの@var: ~`js code`;形式のJavaScript埋め込みは、パースせずvalue-unknownで素通り。
LESSってJS埋め込めんの・・・まじか・・・。
parse/utilities.js
3つのパーサー結果を統合するためのglue。地味だが構造的に大事。
addTypePrefix: 型名の事後正規化
3つのパーサーが返すノード型に、後付けでprefixを貼る関数。
selector-parser:selector-*values-parser:value-*media-query-parser:media-*
同じ型でも、セレクタ由来か値由来かを後付けで区別するという事後正規化で、3パーサーを統一名前空間に押し込んでる。
まとめ
続・Prettier互換ですって言うのは大変。
まあでもCSSだってJSと同じくらい歴史があるのなら、それだけ闇が深くてもまあ当然か・・・。