Due to the need to consider performance
and compatibility
issues, fervid
needs to refactor node
api. Please look forward to it.
[!IMPORTANT] 🚧 Working in Progress. May be break changes in the future. The final implementation goal should be consistent with the vue compiler behavior
this is my current understanding of the vue compilation process
first summarize it as a whole and then proceed to the point of compiling the specific core parts of the code
only describe the vue compilation process and the current difference between fervid and compiler-sfc, the next goal is to get as close to compiler-sfc functionality as possible
choose vite-plugin-vue as the segmentation point among vue-loader and vite-plugin-vue to be more in line with the modern plugin process
unplugin-vue
compiles vue
load(id) {
// 1. handle helper code
if (id === EXPORT_HELPER_ID) {
return helperCode;
}
}
export const EXPORT_HELPER_ID = "\0/plugin-vue/export-helper";
export const helperCode = `
export default (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
}
`;
sfc.vccOpts || sfc
: Get the option object of the component __vccOpts
is Vue Component Compiled Options, If there is no __vccOpts
, use sfc itself directly
Traverse the props array, injecting each attribute into the component options
These attributes typically include:
__file
components name__scopeId
css scoped id__hmrId
handle hmrrender or ssrRender
render functionThe role of this helper is to ensure:
Correct attribute merge: Ensure that all compile-time attributes are correctly merged into the component
Scope isolation: Supported scoped CSS through __scopeId
such as vite bundler, We need to distinguish between the development environment and the production environment. For example, vite optimizes the development environment dev server and does not split multiple module requests, that is, it does not split the entire vue file into multiple js modules and return the esm http request.
There are three operations in transform:
Triggered when a. vue file is requested directly, the first compilation is triggered
if (!query.vue) {
return transformMain(
code,
filename,
options.value,
context,
ssr,
customElementFilter.value(filename),
);
}
Main responsibilities:
export async function transformMain(code, filename, options, ...) {
// 1. create descriptor
const { descriptor, errors } = createDescriptor(filename, code, options);
// 2. handle script
const { code: scriptCode, map: scriptMap } = await genScriptCode(
descriptor,
options,
pluginContext,
ssr,
customElement,
);
// 3. handle template
let templateCode = '';
if (hasTemplateImport) {
({ code: templateCode } = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr,
customElement,
));
}
// 4. handle styles
const stylesCode = await genStyleCode(
descriptor,
pluginContext,
customElement,
attachedProps,
);
// 5. handler custom blocks
const customBlocksCode = await genCustomBlockCode(descriptor, pluginContext);
// merge all compilation code
const output: string[] = [
scriptCode,
templateCode,
stylesCode,
customBlocksCode,
];
}
// 1. add components props scopedId
if (hasScoped) {
attachedProps.push([`__scopeId`, JSON.stringify(`data-v-${descriptor.id}`)]);
}
// 2. add file info (for dev tools)
if (devToolsEnabled || (devServer && !isProduction)) {
attachedProps.push([
`__file`,
JSON.stringify(isProduction ? path.basename(filename) : filename),
]);
}
// 3. use helper to merge all properties (helper will merge all properties into component)
output.push(
`export default /*#__PURE__*/ _export_helper(_sfc_main, [
${attachedProps.map(([key, val]) => `['${key}',${val}]`).join(",\n ")}
])`,
);
// 1. check if HMR is enabled
if (
devServer &&
devServer.config.server.hmr !== false &&
!ssr &&
!isProduction
) {
// 2. add HMR ID
output.push(
`_sfc_main.__hmrId = ${JSON.stringify(descriptor.id)}`,
// 3. create HMR record
`typeof __VUE_HMR_RUNTIME__ !== 'undefined' && ` +
`__VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)`,
// 4. listen to file changes
`import.meta.hot.on('file-changed', ({ file }) => {
__VUE_HMR_RUNTIME__.CHANGED_FILE = file
})`,
);
// 5. check if only template has changed
if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
output.push(
`export const _rerender_only = __VUE_HMR_RUNTIME__.CHANGED_FILE === ${JSON.stringify(
normalizePath(filename),
)}`,
);
}
// 6. add HMR accept handling
output.push(
`import.meta.hot.accept(mod => {
if (!mod) return
const { default: updated, _rerender_only } = mod
if (_rerender_only) {
// only template changed, just rerender
__VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
} else {
// other changes, reload the entire component
__VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)
}
})`,
);
}
The above is the logic of all transform main. In the first stage, we can actually complete all hmr and vue compilation functions in the development environment
The main code logic we generated is
This is the most important code in the first phase of dev mode
"import { defineComponent as _defineComponent } from 'vue'\n" +
'import { ref } from "vue";\n' +
'\n' +
'const _sfc_main = /*@__PURE__*/_defineComponent({\n' +
" __name: 'App',\n" +
' setup(__props, { expose: __expose }) {\n' +
' __expose();\n' +
'\n' +
'const msg = ref("22222222");\n' +
'console.log(123132);\n' +
'\n' +
'const __returned__ = { msg }\n' +
"Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })\n" +
'return __returned__\n' +
'}\n' +
'\n' +
'})',
'import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, vModelText as _vModelText, withDirectives as _withDirectives, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"\n' +
"\n" +
"function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {\n" +
' return (_openBlock(), _createElementBlock("div", null, [\n' +
' _cache[1] || (_cache[1] = _createTextVNode(" 123132 ")),\n' +
' _cache[2] || (_cache[2] = _createElementVNode("div", null, "4565465", -1 /* HOISTED */)),\n' +
' _cache[3] || (_cache[3] = _createElementVNode("h1", null, "Hello world", -1 /* HOISTED */)),\n' +
' _cache[4] || (_cache[4] = _createTextVNode(" 3123132ww ")),\n' +
' _createElementVNode("h2", null, _toDisplayString($setup.msg), 1 /* TEXT */),\n' +
' _cache[5] || (_cache[5] = _createTextVNode(" 12313211231222146521312wwwwww321 ")),\n' +
' _withDirectives(_createElementVNode("input", {\n' +
' "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($setup.msg) = $event)),\n' +
' type: "text"\n' +
" }, null, 512 /* NEED_PATCH */), [\n" +
" [_vModelText, $setup.msg]\n" +
" ])\n" +
" ]))\n" +
"}",
"\n";
'import "/Users//examples/vite/src/App.vue?vue&type=style&index=0&lang.css"',
'_sfc_main.__hmrId = "7a7a37b1"',
"typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)",
"import.meta.hot.on('file-changed', ({ file }) => {",
" __VUE_HMR_RUNTIME__.CHANGED_FILE = file",
"})",
"import.meta.hot.accept(mod => {",
" if (!mod) return",
" const { default: updated, _rerender_only } = mod",
" if (_rerender_only) {",
" __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)",
" } else {",
" __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)",
" }",
"})";
import _export_sfc from '\x00/plugin-vue/export-helper'",
`export default /*#__PURE__*/_export_sfc(_sfc_main, [['render',_sfc_render],['__file',"/Users//examples/vite/src/App.vue"]])`
Running this code, we complete the compilation of vue. This is the complete first stage of the process.
Next we start to describe the details
const { descriptor, errors } = createDescriptor(filename, code, options);
interface SFCDescriptor {
// file path
filename: string;
// source code
source: string;
// unique identifier
id: string;
// information of each block
template: SFCTemplateBlock | null;
script: SFCScriptBlock | null;
scriptSetup: SFCScriptBlock | null;
styles: SFCStyleBlock[];
customBlocks: SFCBlock[];
// CSS variables
cssVars: string[];
// Whether it contains the Slotted API
slotted: boolean;
}
interface SFCTemplateBlock extends SFCBlock {
type: "template";
content: string;
lang?: string; // such as 'pug'
ast?: any; // template AST
src?: string; // external template path
}
interface SFCScriptBlock extends SFCBlock {
type: "script";
content: string;
lang?: string; // such as 'ts'
src?: string; // external script path
setup?: boolean; // whether it's a setup script
bindings?: BindingMetadata; // variable bindings
}
interface SFCStyleBlock extends SFCBlock {
type: "style";
content: string;
lang?: string; // such as 'scss'
scoped?: boolean; // whether it's scoped style
module?: string | boolean; // CSS Modules configuration
}
// handler script info
const scriptBindings = descriptor.scriptSetup?.bindings;
// handler scoped id check if has scoped
const hasScoped = descriptor.styles.some((s) => s.scoped);
// diff old descriptor and new descriptor to detect if only template has changed
if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
// update template only
}
// Example: CSS variables are passed from style to template
const cssVars = descriptor.cssVars;
Unified interface: Provides a unified data structure for different compilation stages
Information sharing: Allow different compilation steps to share information
Incremental updates: support for HMR and on-demand compilation
Extensibility: Support custom blocks and various preprocessors
Optimization support: Provide necessary information for various optimizations
about distinguishing these methods and code logic in fervid. Now that fervid only provides one method, can we achieve consistent behavior with compiler-sfc?
that without considering caching and ssr in the first step, we need to implement the following method
__sfc_main
)