Skip to content

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.

sh
pwd # ~
rm -rf ./plugin-sample

Also, install the main Vite package in order to create a Vite plugin.

sh
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.

sh
pwd # ~
mkdir -p packages/@extensions/vite-plugin-chibivue
touch packages/@extensions/vite-plugin-chibivue/index.ts

~/packages/@extensions/vite-plugin-chibivue/index.ts

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.

sh
pwd # ~
touch examples/playground/src/App.vue

examples/playground/src/App.vue

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

ts
import { createApp } from 'chibivue'
import App from './App.vue'

const app = createApp(App)

app.mount('#app')

playground/vite.config.js

ts
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.

vite_error

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

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.

sh
pwd # ~
mkdir packages/compiler-sfc
touch packages/compiler-sfc/index.ts

In SFC compilation, the SFC is represented by an object called SFCDescriptor.

sh
touch packages/compiler-sfc/parse.ts

packages/compiler-sfc/parse.ts

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

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.

sh
pwd # ~
touch packages/compiler-sfc/compileTemplate.ts

~/packages/compiler-sfc/compileTemplate.ts

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

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

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.

parse_sfc1

/_ Omitted for brevity _/

parse_sfc2

It seems that parsing was successful. Great job!

Source code up to this point:
chibivue (GitHub)

Released under the MIT License.