Skip to content

SFC のパースと SFCDescriptor

ここからは先ほどまでに説明した各パーツの詳細を見ていきましょう.

SFC のパーサーは SFC のコンパイラの一部なので,compiler-sfc に実装されています.

packages/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}

templateSFCTemplateBlock というオブジェクトで表現されており,ここに先ほど説明した 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 をパースすると

vue
<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);
</script>

<template>
  <button type="button" @click="count++">{{ count }}</button>
</template>

以下のような SFCDescriptor が得られます.
ast については今は詳しく読めなくて問題ありません.これから解説します.
※ 一部省略しています.

json
{
  "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 のパーサーはそれのラッパーであるということです.

このパース処理によって概ね templatescript , style などの大枠の構造を得ることができるので,あとはそれぞれで分岐をして詳細なパース処理を行います.

184    switch (node.tag) {
185      case 'template':

詳細部分では,先ほどの 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 のパース処理は終わりです.