Skip to content

Flow of SFC Compilation

Now, let's look at the implementation to produce the final output code.

Once again,

vue
<template>
  <p>Hello, Vapor!</p>
</template>

is transformed into

js
const _sfc_main = {};
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}
export default Object.assign(_sfc_main, {
  render: _sfc_render,
  vapor: true,
  __file: "/path/to/App.vue",
});

This is how we create it.

Since we've already understood the most complex rendering output from the previous page, we should be able to proceed smoothly from here.

Reading compiler-sfc and vite-plugin-vue

From here on, it's more about the implementation of compiler-sfc and vite-plugin-vue rather than Vapor Mode.

In the non-Vapor case, generally, it becomes like:

js
const _sfc_main = {};
import { createElement as _createElement } from "vue";

function _sfc_render(_ctx) {
  return _createElement("p", null, "Hello, World!");
}
export default Object.assign(_sfc_main, {
  render: _sfc_render,
  __file: "/path/to/App.vue",
});

The surrounding code and transformation flow do not change.

The compiler is just called at the appropriate timing, implemented in compiler-vapor.

When is the SFC Compiled?

As you can see from the entry point of compiler-sfc, only separate compilers are exported here.

4export { parse } from './parse'
5export { compileTemplate } from './compileTemplate'
6export { compileStyle, compileStyleAsync } from './compileStyle'
7export { compileScript } from './compileScript'

There is no implementation here that handles these in an integrated manner.

Vite's Plugin System

As mentioned at the beginning, these implementations are called by tools like bundlers, and each compilation is executed.

There are various tools, but let's look at this assuming Vite.

In Vite, the official plugin vite-plugin-vue is famous for this role.

https://github.com/vitejs/vite-plugin-vue

The plugin becomes effective by setting it in vite.config.js.

When you use Vue.js with Vite, you might write something like this:

js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
});

This vue() generates the plugin code.

Before looking at the implementation of this plugin, let's first grasp the concept of plugins in Vite.

Mainly, the important hook is transform.

https://vitejs.dev/guide/api-plugin.html#simple-examples

As a premise, Vite's plugin system is a superset of Rollup's, and in production builds, Rollup's plugin system is actually used.

https://vitejs.dev/guide/api-plugin.html#plugin-api

Vite plugins extend Rollup's well-designed plugin interface with a few extra Vite-specific options. As a result, you can write a Vite plugin once and have it work for both dev and build.

transform Hook

When implementing a plugin, you can transform modules by writing processes in this transform hook.

Roughly, you can compile SFCs by running the compiler here.

Then, when is this transform executed? Generally, it's when the module is loaded from JavaScript.

More specifically, when import or import() is executed.

Development Mode

When the import is executed depends on the mode.

In development mode, the JavaScript with the import is loaded in the browser, and when it's executed, a request is sent to the development server using the browser's Native ESM mechanism.

The development server handles it, executes transform, and returns the result to the browser.

From there, it's the same as Native ESM.

This mechanism is implemented by Vite.

67export function transformRequest(
68  environment: DevEnvironment,
69  url: string,
70  options: TransformOptions = {},
71): Promise<TransformResult | null> {
126  const request = doTransform(environment, url, options, timestamp)
190  const result = loadAndTransform(
191    environment,
192    id,
193    url,
194    options,
195    timestamp,
196    module,
197    resolved,
198  )
344  const transformResult = await pluginContainer.transform(code, id, {
345    inMap: map,
346  })
456    for (const plugin of this.getSortedPlugins('transform')) {
457      if (this._closed && this.environment.config.dev.recoverable)
458        throwClosedServerError()
459      if (!plugin.transform) continue
460
461      ctx._updateActiveInfo(plugin, id, code)
462      const start = debugPluginTransform ? performance.now() : 0
463      let result: TransformResult | string | undefined
464      const handler = getHookHandler(plugin.transform)
465      try {
466        result = await this.handleHookPromise(
467          handler.call(ctx as any, code, id, optionsWithSSR),
468        )
469      } catch (e) {

Production Mode

In production mode builds, the rollup bundler runs.

The bundler reads import when resolving modules.

At that time, it executes transform and uses the result as the resolved result.

This is implemented by Rollup.

Vite is roughly just calling Rollup's bundle function.

507export async function build(
508  inlineConfig: InlineConfig = {},
509): Promise<RollupOutput | RollupOutput[] | RollupWatcher> {
838    const { rollup } = await import('rollup')
839    startTime = Date.now()
840    bundle = await rollup(rollupOptions)

Code where Rollup calls transform:

31export default async function transform(
32	source: SourceDescription,
33	module: Module,
34	pluginDriver: PluginDriver,
35	log: LogHandler
36): Promise<TransformModuleJSON> {
102		code = await pluginDriver.hookReduceArg0(
103			'transform',
327			await module.setSource(
328				await transform(sourceDescription, module, this.pluginDriver, this.options.onLog)
329			);
191	hookReduceArg0<H extends AsyncPluginHooks & SequentialPluginHooks>(
192		hookName: H,
193		[argument0, ...rest]: Parameters<FunctionPluginHooks[H]>,
194		reduce: (
195			reduction: Argument0<H>,
196			result: ReturnType<FunctionPluginHooks[H]>,
197			plugin: Plugin
198		) => Argument0<H>,
199		replaceContext?: ReplaceContext
200	): Promise<Argument0<H>> {
201		let promise = Promise.resolve(argument0);
202		for (const plugin of this.getSortedPlugins(hookName)) {
203			promise = promise.then(argument0 =>
204				this.runHook(
205					hookName,
206					[argument0, ...rest] as Parameters<FunctionPluginHooks[H]>,
207					plugin,
208					replaceContext
209				).then(result => reduce.call(this.pluginContexts.get(plugin), argument0, result, plugin))
210			);
211		}
212		return promise;
213	}

transform Hook in vite-plugin-vue

The implementation of the transform hook in vite-plugin-vue is around here.

320    transform(code, id, opt) {
321      const ssr = opt?.ssr === true
322      const { filename, query } = parseVueRequest(id)
323
324      if (query.raw || query.url) {
325        return
326      }
327
328      if (!filter.value(filename) && !query.vue) {
329        return
330      }
331
332      if (!query.vue) {
333        // main request
334        return transformMain(
335          code,
336          filename,
337          options.value,
338          this,
339          ssr,
340          customElementFilter.value(filename),
341        )

Here, it executes a function called transformMain.

transformMain is implemented in vite-plugin-vue/packages/plugin-vue/src/main.ts.

30export async function transformMain(
31  code: string,
32  filename: string,
33  options: ResolvedOptions,
34  pluginContext: TransformPluginContext,
35  ssr: boolean,
36  customElement: boolean,
37) {

In this, compileScript and compileTemplate from compiler-sfc are called.

This should make it clear how Vue.js's compiler is set up and when it is executed.

Grasping the Whole Output Code by Calling transformMain

Recall such a compilation result.

js
const _sfc_main = {};
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}
export default Object.assign(_sfc_main, {
  render: _sfc_render,
  vapor: true,
  __file: "/path/to/App.vue",
});

How should we output code like this? Generally,

js
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}

This part is generated by a function called compileTemplate.

js
const _sfc_main = {};

// <---------------- insert compileTemplate result

export default Object.assign(_sfc_main, {
  render: _sfc_render,
  vapor: true,
  __file: "/path/to/App.vue",
});

If there is a <script> or <script setup>, code like this is generated, mainly by compileScript:

js
const constant = 42;

const _sfc_main = {
  props: {
    count: {
      type: Number,
      required: true,
    },
  },
  setup() {
    const localCount = ref(0);
    return { localCount };
  },
};

// <---------------- insert compileTemplate result

export default Object.assign(_sfc_main, {
  render: _sfc_render,
  vapor: true,
  __file: "/path/to/App.vue",
});

In other words, it's like:

js
// <---------------- insert compileScript result

// <---------------- insert compileTemplate result

export default Object.assign(_sfc_main, {
  render: _sfc_render,
  vapor: true,
  __file: "/path/to/App.vue",
});

Then, the last part where properties are added to _sfc_main is collected as attachedProps and expanded as code.

The source code for the above discussion is in the following parts.

(They are calling genScriptCode and genTemplateCode instead of compileScript and compileTemplate, but think of them as wrapper functions.)

68  const attachedProps: [string, string][] = []
71  // script
72  const { code: scriptCode, map: scriptMap } = await genScriptCode(
73    descriptor,
74    options,
75    pluginContext,
76    ssr,
77    customElement,
78  )
79
80  // template
81  const hasTemplateImport =
82    descriptor.template && !isUseInlineTemplate(descriptor, options)
83
84  let templateCode = ''
85  let templateMap: RawSourceMap | undefined = undefined
86  if (hasTemplateImport) {
87    ;({ code: templateCode, map: templateMap } = await genTemplateCode(
88      descriptor,
89      options,
90      pluginContext,
91      ssr,
92      customElement,
93    ))
94  }
122  const output: string[] = [
123    scriptCode,
124    templateCode,
125    stylesCode,
126    customBlocksCode,
127  ]
231    output.push(
232      `import _export_sfc from '${EXPORT_HELPER_ID}'`,
233      `export default /*#__PURE__*/_export_sfc(_sfc_main, [${attachedProps
234        .map(([key, val]) => `['${key}',${val}]`)
235        .join(',')}])`,
236    )

(The joined output becomes the final result.)

240  let resolvedCode = output.join('\n')

(Note: Collection of attachedProps)

96  if (hasTemplateImport) {
97    attachedProps.push(
98      ssr ? ['ssrRender', '_sfc_ssrRender'] : ['render', '_sfc_render'],
99    )
100  } else {
131  if (devToolsEnabled || (devServer && !isProduction)) {
132    // expose filename during serve for devtools to pickup
133    attachedProps.push([
134      `__file`,
135      JSON.stringify(isProduction ? path.basename(filename) : filename),
136    ])
137  }

In this way, roughly speaking, we generate the final code like:

js
const _sfc_main = {};
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}
export default Object.assign(_sfc_main, {
  render: _sfc_render,
  vapor: true,
  __file: "/path/to/App.vue",
});

Switching the Vapor Mode Compiler

Finally, let's look at how to switch between the Vapor Mode compiler and the regular compiler before ending this page. In fact, vite-plugin-vue has a vapor branch dedicated to Vapor Mode.

This is because Vapor Mode is currently in the R&D stage. The vuejs/core-vapor is being developed separately to avoid impacting the existing codebase. The same applies to vite-plugin-vue.

It's somewhat inevitable that something like compiler switching partially intrudes into vite-plugin-vue. This is circumvented by switching branches and changing the npm package distribution name.

Here is the branch:

https://github.com/vitejs/vite-plugin-vue/tree/vapor

The distributed package is this, provided as @vue-vapor/vite-plugin-vue:

https://www.npmjs.com/package/@vue-vapor/vite-plugin-vue

This branch provides a flag to switch whether it is vapor or not. To be precise, this option is intended to fall through to the implementation of vuejs/core-vapor, so it's described to omit from the type.

42  script?: Partial<
43    Omit<
44      SFCScriptCompileOptions,
45      | 'id'
46      | 'isProd'
47      | 'inlineTemplate'
48      | 'templateOptions'
49      | 'sourceMap'
50      | 'genDefaultAs'
51      | 'customElement'
52      | 'defineModel'
53      | 'propsDestructure'
54      | 'vapor'
55    >
67  template?: Partial<
68    Omit<
69      SFCTemplateCompileOptions,
70      | 'id'
71      | 'source'
72      | 'ast'
73      | 'filename'
74      | 'scoped'
75      | 'slotted'
76      | 'isProd'
77      | 'inMap'
78      | 'ssr'
79      | 'ssrCssVars'
80      | 'preprocessLang'
81      | 'vapor'
82    >
83  >

In other words, the definitions themselves exist in SFCScriptCompileOptions and SFCTemplateCompileOptions.

128  /**
129   * Force to use of Vapor mode.
130   */
131  vapor?: boolean

After that, you can switch the compiler by passing this flag as an argument when setting up the plugin. For reference, in the vuejs/core-vapor playground, it is set up as follows:

18  plugins: [
19    Vue({
20      vapor: true,
21      compiler: CompilerSFC,
22    }),

Then, as long as the implementation switches the compiler based on the flag that has fallen through from here, it should work. This implementation is done below:

212  const defaultCompiler = vapor
213    ? // TODO ssr
214      (CompilerVapor as TemplateCompiler)
215    : ssr
216      ? (CompilerSSR as TemplateCompiler)
217      : CompilerDOM
218  compiler = compiler || defaultCompiler

About the Compiler Switching API

In the future, switching the compiler will be possible per component. Although the API is not yet decided, something like <script vapor> is proposed.

Incidentally, the formulation of the API is being discussed in the following issue:

https://github.com/vuejs/core-vapor/issues/198