SFC のパースと SFCDescriptor
ここからは先ほどまでに説明した各パーツの詳細を見ていきましょう.
SFC のパーサーは SFC のコンパイラの一部なので,compiler-sfc
に実装されています.
SFCDescriptor
まず,パース結果となる SFCDescriptor
というオブジェクトですが,これは SFC の情報を持ったオブジェクトです.
filename や template の情報, script の情報, style の情報などが含まれています.
76export interface SFCDescriptor {
77 filename: string
78 source: string
79 template: SFCTemplateBlock | null
80 script: SFCScriptBlock | null
81 scriptSetup: SFCScriptBlock | null
82 styles: SFCStyleBlock[]
83 customBlocks: SFCBlock[]
84 cssVars: string[]
85 /**
86 * whether the SFC uses :slotted() modifier.
87 * this is used as a compiler optimization hint.
88 */
89 slotted: boolean
90
91 vapor: boolean
92
93 /**
94 * compare with an existing descriptor to determine whether HMR should perform
95 * a reload vs. re-render.
96 *
97 * Note: this comparison assumes the prev/next script are already identical,
98 * and only checks the special case where <script setup lang="ts"> unused import
99 * pruning result changes due to template changes.
100 */
101 shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
102}
template や script, style はそれぞれ SFCBlock
というオブジェクトを継承しており,この SFCBlock
はその内容を表す content
や,lang, setup, scoped などを表す attrs
,SFC 全体のどの位置にあるか示す loc
の情報などを持っています.
39export interface SFCBlock {
40 type: string
41 content: string
42 attrs: Record<string, string | true>
43 loc: SourceLocation
44 map?: RawSourceMap
45 lang?: string
46 src?: string
47}
template
は SFCTemplateBlock
というオブジェクトで表現されており,ここに先ほど説明した AST を持っています.
49export interface SFCTemplateBlock extends SFCBlock {
50 type: 'template'
51 ast?: RootNode
52}
script も同様に SFCScriptBlock
というオブジェクトで表現されています.
ここには setup であるかどうかのフラグや,import しているモジュールの情報,ブロックの中身であるスクリプト (JS, TS) の AST などが含まれています.
54export interface SFCScriptBlock extends SFCBlock {
55 type: 'script'
56 setup?: string | boolean
57 bindings?: BindingMetadata
58 imports?: Record<string, ImportBinding>
59 scriptAst?: import('@babel/types').Statement[]
60 scriptSetupAst?: import('@babel/types').Statement[]
61 warnings?: string[]
62 /**
63 * Fully resolved dependency file paths (unix slashes) with imported types
64 * used in macros, used for HMR cache busting in @vitejs/plugin-vue and
65 * vue-loader.
66 */
67 deps?: string[]
68}
style も同様に SFCStyleBlock
というオブジェクトで表現されています.
70export interface SFCStyleBlock extends SFCBlock {
71 type: 'style'
72 scoped?: boolean
73 module?: string | boolean
74}
SFCDescriptor
の概要はざっとこんな感じです.
実際に以下のような SFC をパースすると
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button type="button" @click="count++">{{ count }}</button>
</template>
以下のような SFCDescriptor
が得られます.
ast については今は詳しく読めなくて問題ありません.これから解説します.
※ 一部省略しています.
{
"filename": "path/to/core-vapor/playground/src/App.vue",
"source": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n</script>\n\n<template>\n <button type=\"button\" @click=\"count++\">{{ count }}</button>\n</template>\n",
"template": {
"type": "template",
"content": "\n <button type=\"button\" @click=\"count++\">{{ count }}</button>\n",
"attrs": {},
"ast": {
"type": 0,
"source": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n</script>\n\n<template>\n <button type=\"button\" @click=\"count++\">{{ count }}</button>\n</template>\n",
"children": [
{
"type": 1,
"tag": "button",
"tagType": 0,
"props": [
{
"type": 6,
"name": "type",
"value": {
"type": 2,
"content": "button",
"source": "\"button\""
}
},
{
"type": 7,
"name": "on",
"rawName": "@click",
"exp": {
"type": 4,
"content": "count++",
"isStatic": false,
"constType": 0,
"ast": {
"type": "UpdateExpression",
"start": 1,
"end": 8,
"operator": "++",
"prefix": false,
"argument": {
"type": "Identifier",
"identifierName": "count"
},
"name": "count"
},
"extra": {
"parenthesized": true,
"parenStart": 0
},
"comments": [],
"errors": []
}
},
"arg": {
"type": 4,
"content": "click",
"isStatic": true,
"constType": 3
},
"modifiers": []
],
"children": [
{
"type": 5,
"content": {
"type": 4,
"content": "count",
"isStatic": false,
"constType": 0,
"ast": null
}
}
]
}
]
}
},
"script": null,
"scriptSetup": {
"type": "script",
"content": "\nimport { ref } from 'vue'\n\nconst count = ref(0)\n",
"attrs": {
"setup": true,
"lang": "ts"
},
"setup": true,
"lang": "ts"
},
"styles": []
}
パーサーの実装
パーサーの実装は以下の parse
という関数です.
126export function parse(
127 source: string,
128 options: SFCParseOptions = {},
129): SFCParseResult {
104export interface SFCParseResult {
105 descriptor: SFCDescriptor
106 errors: (CompilerError | SyntaxError)[]
107}
source には SFC の文字列が入ってきます
そしてその文字列を解析し,SFCDescriptor
を返します.
まず,template のパーサーによって SFC 全体パースします.
162 const ast = compiler.parse(source, {
163 parseMode: 'sfc',
164 prefixIdentifiers: parseExpressions,
165 ...templateParseOptions,
166 onError: e => {
167 errors.push(e)
168 },
169 })
compiler.parse の compiler は options からきているもので,これは実は compiler-core
にある template のパーサーです.
「なぜ SFC なのに template のパーサーを使うの・・・?」
そう思うのもまぁ無理はありません.その通りだと思います.
しかし,よく考えてみてほしいでのすが,これで十分なのです.
template も SFC も構文としてはほぼ HTML です.
Vue.js の HTML っぽいものを解析するときは基本 compiler-core
のパーサーを使います.
若干の差異はあるので,引数の parseMode
に 'sfc'
を渡してるのもわかると思います.
まぁ,つまるところ,compiler-core
は template 専用のパーサを実装しているというよりももっと汎用的な立ち位置で,compiler-sfc のパーサーはそれのラッパーであるということです.
このパース処理によって概ね template
や script
, style
などの大枠の構造を得ることができるので,あとはそれぞれで分岐をして詳細なパース処理を行います.
184 switch (node.tag) {
185 case 'template':
215 case 'script':
229 case 'style':
詳細部分では,先ほどの SFCBlock
を継承したそれぞれの Block を生成するために処理を行っています.
(概ね createBlock という整形関数を読んだり,エラハンを行ったりしているだけなのでコードは省略します)
あとは,ソースマップの生成等を行います.
285 if (sourceMap) {
286 const genMap = (block: SFCBlock | null, columnOffset = 0) => {
287 if (block && !block.src) {
288 block.map = generateSourceMap(
289 filename,
290 source,
291 block.content,
292 sourceRoot,
293 !pad || block.type === 'template' ? block.loc.start.line - 1 : 0,
294 columnOffset,
295 )
296 }
297 }
298 genMap(descriptor.template, templateColumnOffset)
299 genMap(descriptor.script)
300 descriptor.styles.forEach(s => genMap(s))
301 descriptor.customBlocks.forEach(s => genMap(s))
302 }
377function generateSourceMap(
378 filename: string,
379 source: string,
380 generated: string,
381 sourceRoot: string,
382 lineOffset: number,
383 columnOffset: number,
384): RawSourceMap {
なんと,これで SFC のパース処理は終わりです.