Flow of SFC Compilation
Now, let's look at the implementation to produce the final output code.
Once again,
<template>
<p>Hello, Vapor!</p>
</template>
is transformed into
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:
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:
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.
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,
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
.
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
:
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:
// <---------------- 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:
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
60 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: