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
.
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:
<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.
{
"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':
215 case 'script':
229 case 'style':
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.