Skip to content

Parsing SFC and SFCDescriptor

From here, let's look at the details of each part explained earlier.

Since the parser for SFC is part of the SFC compiler, it is implemented in compiler-sfc.

packages/compiler-sfc

SFCDescriptor

First, the object called SFCDescriptor, which is the result of parsing, is an object that holds information about the SFC.
It includes the filename, template information, script information, style information, etc.

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}

The template, script, and style each inherit from an object called SFCBlock, and this SFCBlock contains information such as content, which represents its content, attrs, which represents attributes like lang, setup, scoped, etc., and loc, which indicates where in the whole SFC it is located.

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}

The template is represented by an object called SFCTemplateBlock, which contains the AST explained earlier.

49export interface SFCTemplateBlock extends SFCBlock {
50  type: 'template'
51  ast?: RootNode
52}

Similarly, the script is represented by an object called SFCScriptBlock.
This includes a flag indicating whether it is setup or not, information about the modules being imported, and the AST of the script (JS, TS) that is the content of the block.

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}

Similarly, the style is represented by an object called SFCStyleBlock.

70export interface SFCStyleBlock extends SFCBlock {
71  type: 'style'
72  scoped?: boolean
73  module?: string | boolean
74}

That's roughly the outline of SFCDescriptor.

If you actually parse an SFC like the following:

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

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

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

Then you get an SFCDescriptor like the following.
You don't need to read the ast in detail now. We will explain it later.
Note: Some parts are omitted.

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": []
}

Implementation of the Parser

The implementation of the parser is the parse function below.

126export function parse(
127  source: string,
128  options: SFCParseOptions = {},
129): SFCParseResult {
104export interface SFCParseResult {
105  descriptor: SFCDescriptor
106  errors: (CompilerError | SyntaxError)[]
107}

The source contains the string of the SFC.
It parses that string and returns an SFCDescriptor.

First, it parses the entire SFC using the template parser.

162  const ast = compiler.parse(source, {
163    parseMode: 'sfc',
164    prefixIdentifiers: parseExpressions,
165    ...templateParseOptions,
166    onError: e => {
167      errors.push(e)
168    },
169  })

The compiler.parse in compiler comes from options, and this is actually the template parser in compiler-core.

"Why use the template parser even though it's an SFC...?"

It's understandable to think so. That's correct.
However, if you think about it carefully, this is sufficient.

Both template and SFC are almost HTML in terms of syntax.
When parsing something HTML-like in Vue.js, we basically use the parser in compiler-core.
There are slight differences, so you can see that we're passing 'sfc' as the parseMode argument.

In other words, compiler-core is in a more general position rather than implementing a parser exclusively for templates, and the parser in compiler-sfc is a wrapper around it.

Through this parsing process, we can get the rough structure of template, script, style, etc., so we then branch and perform detailed parsing for each.

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

In the detailed parts, we process to generate each Block that inherits from the earlier SFCBlock.
(Basically, we're just calling a formatting function called createBlock and doing error handling, so we'll omit the code.)

After that, we generate source maps, etc.

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 {

Surprisingly, this completes the parsing process of the SFC.