Vue/HTML/Markdownなどに埋め込まれたJS/TS部分が、どうフォーマットされるのか調べたかった。
Here be dragons…, yet another dragons… 🐉
js-in-xxxになれるparser
まず、どこで観測できるか。
- Markdown系
markdown: コードブロック内mdx: コードブロック内、import/export部
- HTML系
html:<script>タグ内、onclick等のイベントハンドラ属性vue:<script>タグ内、テンプレート内の各種ディレクティブ・式angular: テンプレート内の各種バインディング・ディレクティブ
HTML系のparserは、他にもmjmlとかlwcとかもあるけど、そこでは処理されない。
埋め込み処理の流れ
- 親ファイル側の処理の一環として、埋め込み部を処理するパートがある
- その際、
textToDoc()という内部的な処理があって、そこで状況に応じたparserとprinterを使う - それが
babelやtypescriptといった既存のJS/TSの処理でも使われてるビルトイン実装な場合もあれば - 特殊なケースでは
__vue_expressionのような特別な実装が使われる
コードとしては、src/language-xxx/embed.jsから追っていくとよい。
この基本の流れは、css-in-jsでもjs-in-vueでもなんでも同じ。
ただそれぞれの組み合わせごとに、サポートしてる埋め込み対象も異なるなら、やってることもまちまちなんよな・・・。
今回の主旨であるjs-in-xxxという組み合わせでいうと、language-html/embed.jsとlanguage-markdown/embed.jsから追う。
markdown/mdxの場合
3パターンある。
- 1: コードブロック
- 見つけたら、言語名を取得
- サポートしてる言語かどうか判定
javascript,js,jsx,typescript,ts,tsxなど、linguist-languageで取れたやつ- ->
babelortypescript
- ->
angular-ts->typescriptもある
typescriptの場合、TSXかどうかを区別するために、options.filepathが上書きされる
- 2: MDXの場合、
import/exportのそれぞれを、babelを使って個別に対応 - 3: MDXの場合、JSX部も
__js_expressionを使って対応
htmlの場合
ピュアなHTMLの場合は2パターン。
- 1:
<script>タグ ->babeltype="module"なら、__babelSourceType: "module"を設定
- 2: イベントハンドラ(
onclick="...") ->babel__isHtmlInlineEventHandlerというフラグで、末尾;を処理したり、シングルクォートを使ったり
__embeddedInHtmlというフラグにも関連がある
vueの場合
- 1:
<script>タグ ->babelortypescriptlang="ts"かどうか
単純なのはこれだけで、あと全部は特殊なケース。
標準JSとして無効な構文を、有効なJSの構文になるようラップしてからパースしたりしてる。
2: <script generic="...">属性
(使われてるの見たことないけど)
- 入力:
T extends Record<string, unknown>, U - ラップ:
type T<T extends Record<string, unknown>, U> = any - 抽出:
AST.program.body[0].typeParameters.params
babel-tsを使って、__isEmbeddedTypescriptGenericParametersというフラグで処理する。
3: v-forディレクティブ
なんと左辺と右辺で処理フローが違う!
左辺は、
- 入力:
(item, index) in items - ラップ:
function _(item, index) {} - 抽出:
AST.program.body[0].params
これも特殊なケースなのに、babel or babel-tsで無理やり処理されてるシリーズ。
フラグは__isVueForBindingLeftってやつ。
右辺のパーサーは、__js_expression or __ts_expressionになる。
4: v-slotディレクティブ
- 入力:
{ item, index } - ラップ:
function _({ item, index }) {} - 抽出:
AST.program.body[0].params
ここもbabel or babel-tsでやってて、フラグは__isVueBindingsというやつ。
5: @click(v-on)ディレクティブ
少し変わってて、初手は__vue_expressionで処理しようとして、エラーになったら、__vue_event_bindingにフォールバックする。
これは@click="foo()"と@click="a++;b()"の差をよしなにするためらしいが、アクロバティックすぎる・・・。
6: それ以外
{{ expr }}:__vue_expressionor__vue_ts_expressionv-if,v-show:__js_expressionor__ts_expression:class,:style(v-bind):__vue_expressionor__vue_ts_expression
angularの場合
angular-estree-parserを使ってて、こっちはbabelを使ってない。
いちおう内部的なparser名はこういう感じ。
- 1:
__ng_action:(click)="onClick()" - 2:
__ng_binding:[class]="myClass" - 3:
__ng_interpolation:{{ expr | uppercase }} - 4:
__ng_directive:*ngFor="let item of items; let i = index; trackBy: trackFn"
最初の2つはJS/TS互換なシンタックスだが、あと2つは違う。
特殊なparserたちのまとめ
これまで見てきた__vue_expressionとかのこと。textToDocOptions.parserで調べると出てくるやつらをまとめておく。
だいたいは、babelのパーサー定義時に、しれっと一緒に定義されてる。
__(j|t)s_expression:babelorbabel-tsをisExpression: trueで使ってる__vue(_ts)_expression: 同上__vue(_ts)_event_binding:babelorbabel-tsそのまま
__ng_xxxはbabelを使ってない。(大事なことなので2回)
これらは全部angular-estree-parserのparseXxx()を使ってる。
printerはというと、ぜんぶestreeで賄う。
特殊なフラグたちのまとめ
そして次に、特殊な処理のために使われるフラグたち。
__embeddedInHtml: HTML内埋め込みのコンテキストマーク、</scriptのエスケープに__babelSourceType:moduleとしてパースすべきかどうか__isInHtmlInterpolation:bracketSpacing: false時に、{{({a: 1})}}のようにするために__isInHtmlAttribute: HTMLの属性内ではシングルクォート強制__isHtmlInlineEventHandler: ASIの;を抑制するために__onHtmlBindingRoot: ASTをチェックできるコールバックで、行展開するか決めてたり__isVueBindings:v-slot、パラメータをそのまま出力__isVueForBindingLeft:v-for左辺、複数パラメータは()で囲まれる__isEmbeddedTypescriptGenericParameters:generic属性、型パラメータを抽出
もうわけがわからんね。
ポイントとしては、これらのフラグはparserで差し込まれるけど、それを使うのはprinter側ってところ。