Implementing the SFC Parser
Preparation
Although this is a sample plugin that we created earlier, let's delete it because it is no longer needed.
pwd # ~
rm -rf ./plugin-sample
Also, install the main Vite package in order to create a Vite plugin.
pwd # ~
ni vite
This is the main part of the plugin, but since this is originally outside the scope of vuejs/core, we will create a directory called @extensions
in the packages
directory and implement it there.
pwd # ~
mkdir -p packages/@extensions/vite-plugin-chibivue
touch packages/@extensions/vite-plugin-chibivue/index.ts
~/packages/@extensions/vite-plugin-chibivue/index.ts
import type { Plugin } from 'vite'
export default function vitePluginChibivue(): Plugin {
return {
name: 'vite:chibivue',
transform(code, id) {
return { code }
},
}
}
Now, let's implement the SFC compiler.
However, it may be difficult to imagine without any substance, so let's implement a playground and do it while running it.
We will create a simple SFC and load it.
pwd # ~
touch examples/playground/src/App.vue
examples/playground/src/App.vue
<script>
import { reactive } from 'chibivue'
export default {
setup() {
const state = reactive({ message: 'Hello, chibivue!', input: '' })
const changeMessage = () => {
state.message += '!'
}
const handleInput = e => {
state.input = e.target?.value ?? ''
}
return { state, changeMessage, handleInput }
},
}
</script>
<template>
<div class="container" style="text-align: center">
<h2>{{ state.message }}</h2>
<img
width="150px"
src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1200px-Vue.js_Logo_2.svg.png"
alt="Vue.js Logo"
/>
<p><b>chibivue</b> is the minimal Vue.js</p>
<button @click="changeMessage">click me!</button>
<br />
<label>
Input Data
<input @input="handleInput" />
</label>
<p>input value: {{ state.input }}</p>
</div>
</template>
<style>
.container {
height: 100vh;
padding: 16px;
background-color: #becdbe;
color: #2c3e50;
}
</style>
playground/src/main.ts
import { createApp } from 'chibivue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
playground/vite.config.js
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import chibivue from '../../packages/@extensions/vite-plugin-chibivue'
const dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
export default defineConfig({
resolve: {
alias: {
chibivue: path.resolve(dirname, '../../packages'),
},
},
plugins: [chibivue()],
})
Let's try starting in this state.
Of course, it will result in an error. Well done (?).
Resolving the Error
Let's resolve the error for now. We don't aim for perfection right away.
First, let's limit the target of transform
to "*.vue".
We can write a branching statement with id
as we did in the sample, but since Vite provides a function called createFilter
, let's create a filter using that.
(There is no particular reason for this.)
~/packages/@extensions/vite-plugin-chibivue/index.ts
import type { Plugin } from 'vite'
import { createFilter } from 'vite'
export default function vitePluginChibivue(): Plugin {
const filter = createFilter(/\.vue$/)
return {
name: 'vite:chibivue',
transform(code, id) {
if (!filter(id)) return
return { code: `export default {}` }
},
}
}
We created a filter and transformed the file content to export default {}
if it was a Vue file.
The error should disappear and the screen should not display anything.
Implementation of the Parser on compiler-sfc
Now, this is just a temporary solution, so let's implement a proper solution.
The role of vite-plugin is to enable transformation with Vite, so the parsing and compilation are in the main Vue package.
That is the compiler-sfc
directory.
https://github.com/vuejs/core/blob/main/.github/contributing.md#package-dependencies
The SFC compiler is the same for both Vite and Webpack.
The core implementation is in compiler-sfc
.
Let's create compiler-sfc
.
pwd # ~
mkdir packages/compiler-sfc
touch packages/compiler-sfc/index.ts
In SFC compilation, the SFC is represented by an object called SFCDescriptor
.
touch packages/compiler-sfc/parse.ts
packages/compiler-sfc/parse.ts
import { SourceLocation } from '../compiler-core'
export interface SFCDescriptor {
id: string
filename: string
source: string
template: SFCTemplateBlock | null
script: SFCScriptBlock | null
styles: SFCStyleBlock[]
}
export interface SFCBlock {
type: string
content: string
loc: SourceLocation
}
export interface SFCTemplateBlock extends SFCBlock {
type: 'template'
}
export interface SFCScriptBlock extends SFCBlock {
type: 'script'
}
export declare interface SFCStyleBlock extends SFCBlock {
type: 'style'
}
Well, there's nothing particularly difficult.
It's just an object that represents the SFC information.
In packages/compiler-sfc/parse.ts
, we will parse the SFC file (string) into SFCDescriptor
.
Some of you may be thinking, "What? You worked so hard on the template parser, and now you're creating another parser...? It's a hassle." But don't worry.
The parser we're going to implement here is not a big deal. That's because we're just separating the template, script, and style by combining what we've created so far.
First, as a preparation, export the template parser we created earlier.
~/packages/compiler-dom/index.ts
import { baseCompile, baseParse } from '../compiler-core'
export function compile(template: string) {
return baseCompile(template)
}
// Export the parser
export function parse(template: string) {
return baseParse(template)
}
Keep these interfaces in the compiler-sfc side.
pwd # ~
touch packages/compiler-sfc/compileTemplate.ts
~/packages/compiler-sfc/compileTemplate.ts
import { TemplateChildNode } from '../compiler-core'
export interface TemplateCompiler {
compile(template: string): string
parse(template: string): { children: TemplateChildNode[] }
}
Then, just implement the parser.
packages/compiler-sfc/parse.ts
import { ElementNode, NodeTypes, SourceLocation } from '../compiler-core'
import * as CompilerDOM from '../compiler-dom'
import { TemplateCompiler } from './compileTemplate'
export interface SFCParseOptions {
filename?: string
sourceRoot?: string
compiler?: TemplateCompiler
}
export interface SFCParseResult {
descriptor: SFCDescriptor
}
export const DEFAULT_FILENAME = 'anonymous.vue'
export function parse(
source: string,
{ filename = DEFAULT_FILENAME, compiler = CompilerDOM }: SFCParseOptions = {},
): SFCParseResult {
const descriptor: SFCDescriptor = {
id: undefined!,
filename,
source,
template: null,
script: null,
styles: [],
}
const ast = compiler.parse(source)
ast.children.forEach(node => {
if (node.type !== NodeTypes.ELEMENT) return
switch (node.tag) {
case 'template': {
descriptor.template = createBlock(node, source) as SFCTemplateBlock
break
}
case 'script': {
const scriptBlock = createBlock(node, source) as SFCScriptBlock
descriptor.script = scriptBlock
break
}
case 'style': {
descriptor.styles.push(createBlock(node, source) as SFCStyleBlock)
break
}
default: {
break
}
}
})
return { descriptor }
}
function createBlock(node: ElementNode, source: string): SFCBlock {
const type = node.tag
let { start, end } = node.loc
start = node.children[0].loc.start
end = node.children[node.children.length - 1].loc.end
const content = source.slice(start.offset, end.offset)
const loc = { source: content, start, end }
const block: SFCBlock = { type, content, loc }
return block
}
I think it's easy for everyone who has implemented the parser so far. Let's actually parse SFC in the plugin.
~/packages/@extensions/vite-plugin-chibivue/index.ts
import { parse } from '../../compiler-sfc'
export default function vitePluginChibivue(): Plugin {
//.
//.
//.
return {
//.
//.
//.
transform(code, id) {
if (!filter(id)) return
const { descriptor } = parse(code, { filename: id })
console.log(
'🚀 ~ file: index.ts:14 ~ transform ~ descriptor:',
descriptor,
)
return { code: `export default {}` }
},
}
}
This code runs in the process where Vite is running, which means it is executed in Node, so I think the console output is displayed in the terminal.
/_ Omitted for brevity _/
It seems that parsing was successful. Great job!
Source code up to this point:
chibivue (GitHub)