Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

latest progress #58

Open
ErKeLost opened this issue Nov 29, 2024 · 14 comments
Open

latest progress #58

ErKeLost opened this issue Nov 29, 2024 · 14 comments

Comments

@ErKeLost
Copy link
Contributor

hi, @JafarAkhondali is there any latest progress now? I'm ready to continue developing unplugin-vue-fervid and @farmfe/plugin-vue

@phoenix-ru
Copy link
Owner

phoenix-ru commented Nov 29, 2024

Hi @ErKeLost, curious as to why you ping Jafar as he isn't a part of the project.

To answer your question, I am actively working on making Fervid fully compliant with the official specification. You can see progress on the umbrella issue: #16

If you want to contribute, there are two untouched opportunities at the moment:

  1. Working on the SSR code generation (this will open up integrating Farm as the official fully-native build tool into Nuxt, which is my ultimate goal).
  2. Working on making the Node.js / Farm plugins better. They already support lots of options and work good enough to pass spec tests, but are somewhat limited and not well polished.

Please tell me if you're interested in any of the options above or maybe would like to contribute somewhere else? :)

@ErKeLost
Copy link
Contributor Author

I want to first try to build plugins that are completely consistent with unplugin-vue behavior, and then if you have any unfinished projects I'm willing to try

I now have a question about 'vue/compiler-sfc' if we ignore the ssr issue for the time being, can the entire behavior support me to complete the unplugin-vue-fervid project? I also have another idea if we can ultimately maintain the same behavior as vue/compiler-sfc, then we can finally discuss merging unplugin-vue-fervid into unplugin-vue,(this requires completing ssr work)

The ultimate goal is to implement a pure native @farmfe/plugin-vue plug-in so as to ensure 100% performance output

I am really interested in vue Ecology recording an issue under unplugin-vue
unplugin/unplugin-vue#169
The first point has been achieved, and I am implementing the second point, so I wanted to ask you

@ErKeLost
Copy link
Contributor Author

My idea is that if the basic project unplugin-vue-fervid can be successfully run on various frameworks, then I am very willing to continue to help you solve the remaining problems and then develop the final goal of farm's native plugin.

@phoenix-ru
Copy link
Owner

if we ignore the ssr issue for the time being, can the entire behavior support me to complete the unplugin-vue-fervid project

Yes, the CSR code output is almost identical with some minor discrepancies which I am actively trying to cover. It is generally very usable already (although needs "highly experimental" note).

ultimately maintain the same behavior as vue/compiler-sfc

I am afraid it is technically impossible to maintain 1-to-1 compatibility without severely crippling the native speed of Fervid. The reason being - JS compiler is lousy in that it uses JS plugins, functions, etc:

Fervid intends to be the real parallel compiler (with speed of up to 60K files/sec), and calling JS destroys that dream

The ultimate goal is to implement a pure native @farmfe/plugin-vue plug-in so as to ensure 100% performance output

I agree here, I think having a native plugin is the ultimate goal :)

unplugin-vue-fervid can be successfully run on various frameworks

What do you mean by "frameworks"?

final goal of farm's native plugin

I see Farm initially providing a plugin for CSR and later supporting SSR as well

@ErKeLost
Copy link
Contributor Author

  1. The goal is to benefit some other frameworks such as rspack vite webpack first. The current problem is that, for example, rspack does not support rust plug-ins. I think we can provide them with js plugins first to enhance our influence, so that more and more people know that not only react has rust compiler vue

  2. Farm, the function of ssr, can also be realized. I want to implement unplugin-vue-fervid first and then help you solve ssr issues with all your strength. Farm is about to release 2.0. The goal of 2.0 is to create a complete set of rust ecological chain.

  3. Framework means implementing js versions of plugins for other frameworks

@phoenix-ru
Copy link
Owner

I agree with your points. Is there anything else or more specific you have questions about?

@ErKeLost
Copy link
Contributor Author

Not yet. I want to start this project. If I have any questions, I will always ask you.

@ErKeLost
Copy link
Contributor Author

ErKeLost commented Dec 3, 2024

hi @phoenix-ru

this is my current understanding of the vue compilation process

i will first summarize it as a whole and then proceed to the point of compiling the specific core parts of the code

i will 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

i 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

plugin compilation process

unplugin-vue compiles vue

  1. handle helper code
load(id) {
  // 1. handle helper code
  if (id === EXPORT_HELPER_ID) {
    return helperCode;
  }
}

helper code

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;
}
`;
  1. merge options

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

  1. inject props

Traverse the props array, injecting each attribute into the component options

These attributes typically include:

  • __file components name
  • __scopeId css scoped id
  • __hmrId handle hmr
  • render or ssrRender render function

The 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

transform code

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:

  1. transformMain: main request

Triggered when a. vue file is requested directly, the first compilation is triggered

  • For example:/src/App. vue
if (!query.vue) {
  return transformMain(
    code,
    filename,
    options.value,
    context,
    ssr,
    customElementFilter.value(filename),
  );
}

Main responsibilities:

  • Integrate all sub-modules (template, script, style)
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,
  ];
}
  • Generate final component definitions
// 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    ")}
  ])`,
);
  • Handling HMR-related logic
// 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)
      }
    })`,
  );
}
  • Add SSR support (pending now fervid not support ssr)

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

  • script code
  "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' +
    '})',
  • template code
'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";
  • style code
 'import "/Users//examples/vite/src/App.vue?vue&type=style&index=0&lang.css"',
  • hmr code
'_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)",
  "  }",
  "})";
  • then we finally generate the code with
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

First function point descriptor

  1. createDescriptor
const { descriptor, errors } = createDescriptor(filename, code, options);
  1. descriptor structure
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
}
  1. Descriptor main logic:
  • share the same descriptor for all components in the same file
// handler script info
const scriptBindings = descriptor.scriptSetup?.bindings;
// handler scoped id check if has scoped
const hasScoped = descriptor.styles.some((s) => s.scoped);
  1. Hmr support
// diff old descriptor and new descriptor to detect if only template has changed
if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
  // update template only
}
  1. Cross-block communication
// Example: CSS variables are passed from style to template
const cssVars = descriptor.cssVars;

Importance of Descriptors:

  1. Unified interface: Provides a unified data structure for different compilation stages

  2. Information sharing: Allow different compilation steps to share information

  3. Incremental updates: support for HMR and on-demand compilation

  4. Extensibility: Support custom blocks and various preprocessors

  5. Optimization support: Provide necessary information for various optimizations

There are also a few more important methods in the compilation of vue

I was thinking about distinguishing these methods and code logic in fervid. Now that fervid only provides one method, can we achieve consistent behavior with compiler-sfc?

I am thinking that without considering caching and ssr in the first step, we need to implement the following method

  • compiler.compileScript (generate script code main __sfc_main)
  • compiler.compileTemplate (generate template code)
  • compiler.compileStyle (generate style code)
  • compiler.parse (generate descriptor)

@phoenix-ru
Copy link
Owner

Hi @ErKeLost, I've read through your research and it looks mostly correct.
To answer your question as to why Fervid only provides a single method in comparison to the official compiler — costs of serialisation/deserialisation are very high.
Instead of doing back-and-forth between JS and Rust side, I provide only one input and return only one output.

Fervid also significantly differs in a way it compiles the code. Instead of doing string concatenation like the official compiler, Fervid fully operates with IR+AST. It provides some guarantees about the processing and the output. The official compiler is also moving towards AST, but very slowly and inconsistently.

As to assembling the parts of SFC into a single file (which you called "optimization"), Fervid does it out-of-the-box and much smarter. Since it works with AST directly, it merges properties directly into the object and thus no helpers/additional things are needed.

The only thing Fervid does not handle by design is HMR (because it differs across bundlers). This will be up to the plugin.

So, in short — there will be no descriptor in the JS side and any manipulations should happen in the Rust side.

@ErKeLost
Copy link
Contributor Author

ErKeLost commented Dec 4, 2024

Has fervid already done all the operations on the rust side? For example, the problem of attribute merging incorporates some functions like scopedId? I haven't checked the specific code of fervid yet. I will check the code logic in the near future.

@phoenix-ru
Copy link
Owner

Yep. Most of Rust transforms are implemented and I am polishing them at the moment so that it matches the official compiler as much as possible.

@ErKeLost
Copy link
Contributor Author

ErKeLost commented Dec 4, 2024

my idea is whether we can synchronize compiler-sfc methds as much as possible and expose them through napi, which will allow us to better optimize the code and synchronize the code in the future, regardless of compiler-sfc updates or we can eventually replace compiler-sfc. Of course, we can support the current effect of compile. I wonder if we can split it more carefully and release different js methods for different functions.

@phoenix-ru
Copy link
Owner

phoenix-ru commented Dec 4, 2024

synchronize compiler-sfc methds

Would this mean exposing the descriptor somehow? If you want to go this route, we can certainly do it using External

You would need to provide methods which operate on an External descriptor. It should be doable, although there is no direct mapping of compiler-sfc descriptor to the one used by Fervid (as Fervid uses a lot of internal structs instead)


Another question - how much freedom in JS-land do you want to have? You wouldn't be able to operate on a descriptor except for calling Napi functions exported by Fervid.


A good place for you to start would be

/// A more general-purpose SFC compilation function.
/// Not production-ready yet.
pub fn compile(source: &str, options: CompileOptions) -> Result<CompileResult, CompileError> {
let mut all_errors = Vec::<CompileError>::new();
// Options
let is_prod = options.is_prod.unwrap_or_default();
let is_custom_element = options.is_custom_element.unwrap_or_default();
// Parse
let mut sfc_parsing_errors = Vec::new();
let mut parser = SfcParser::new(source, &mut sfc_parsing_errors);
let sfc = parser.parse_sfc()?;
all_errors.extend(sfc_parsing_errors.into_iter().map(From::from));
// For scopes
// TODO Research if it's better to compute that on the caller site or here
let file_hash = {
let mut hasher = FxHasher32::default();
source.hash(&mut hasher);
let num = hasher.finish();
format!("{:x}", num)
};
// Transform
let mut transform_errors = Vec::new();
let transform_options = TransformSfcOptions {
is_prod,
is_ce: is_custom_element,
props_destructure: options.props_destructure.unwrap_or_default(),
scope_id: &file_hash,
filename: &options.filename,
};
let transform_result = transform_sfc(sfc, transform_options, &mut transform_errors);
all_errors.extend(transform_errors.into_iter().map(From::from));
// Codegen
let mut ctx = CodegenContext::with_bindings_helper(transform_result.bindings_helper);
let template_expr: Option<Expr> = transform_result
.template_block
.and_then(|template_block| ctx.generate_sfc_template(&template_block));
let sfc_module = ctx.generate_module(
template_expr,
*transform_result.module,
transform_result.exported_obj,
transform_result.setup_fn,
options.gen_default_as.as_deref(),
);
// Convert AST to string
let (code, source_map) = CodegenContext::stringify(
&source,
&sfc_module,
FileName::Custom(options.filename.to_string()),
options.source_map.unwrap_or(false),
false,
);
let styles = transform_result
.style_blocks
.into_iter()
.map(|style_block| CompileEmittedStyle {
code: style_block.content.to_string(),
is_compiled: should_transform_style_block(&style_block),
lang: style_block.lang.to_string(),
is_scoped: style_block.is_scoped,
})
.collect();
let other_assets = transform_result
.custom_blocks
.into_iter()
.map(|block| {
CompileEmittedAsset {
lo: 0, // todo
hi: 0, // todo
tag_name: block.starting_tag.tag_name.to_string(),
content: block.content.to_string(),
}
})
.collect();
Ok(CompileResult {
code,
file_hash,
errors: all_errors,
styles,
other_assets,
source_map,
setup_bindings: ctx.bindings_helper.setup_bindings,
})
}

@ErKeLost
Copy link
Contributor Author

ErKeLost commented Dec 5, 2024

What I want to do is try to keep the api consistent. The descriptor only corresponds to the behavior of compiler-sfc. I don't seem to need to change the descriptor at the moment. I don't know if there are still some deviations in my understanding

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants