farm-fe / farm

Extremely fast Vite-compatible web build tool written in Rust
https://farmfe.org
MIT License
4.61k stars 154 forks source link

[Feature] Rust Vue SFC compile #125

Open wre232114 opened 1 year ago

wre232114 commented 1 year ago

Investigate https://github.com/HerringtonDarkholme/vue-compiler

lovetingyuan commented 1 year ago

After vue-compiler is released, will farm naturally provide built-in support for .vue files, similar to .jsx?

wre232114 commented 1 year ago

we will provide official plugin and scaffold to support.

phoenix-ru commented 10 months ago

Any plans for the Rust/SWC plugin system support?

wre232114 commented 10 months ago

we already have rust/swc plugins supported. see @farmfe/plugin-react, and examples/vue-jsx

wre232114 commented 10 months ago

Swc plugins: https://farm-fe.github.io/docs/config/farm-config#scriptplugins Rust plugins: https://github.com/farm-fe/farm/tree/main/rust-plugins

phoenix-ru commented 10 months ago

Judging by the examples, does it only support string output?

The farmfe_core::plugin::Plugin trait has transform and process_module functions, of which transform seems to be the right one to take the source (param.content). Now, how would you write a plugin (or loader in webpack world) which takes a string and returns an SWC AST?

wre232114 commented 10 months ago

transform is string to string, then the string will be passed to parse hook and be parsed to ast.

process_module is ast to ast. swc pklugins are executed during this stage.

if you want to parse string to ast directly, you need to use parse hook. for most situation transform would be enough, I think load + transform = webpack loader

phoenix-ru commented 10 months ago

One more question relevant to Vue compilation. Vue SFC's have <style> blocks, which usually contain CSS/SCSS/etc.

Ideally, from the SFC compiler's perspective, I'd love to have the following flow:

  1. parse, as you mentioned, takes .vue and converts it to SWC AST;
  2. While doing so, let's assume compiler finds <style lang="css" scoped> and <style lang="scss" scoped>.
  3. It needs Farm to compile SCSS/LESS/etc. to CSS and then it needs the compilation result;
  4. Then it applies necessary Vue transforms thanks to lightningcss;
  5. Finally, it informs Farm that this CSS is a dependency of current module;

How do I implement points 3 and 5?

wre232114 commented 10 months ago

We already have implemented Vue SFC compilation in JS, see https://github.com/farm-fe/farm/blob/main/js-plugins/vue/src/farm-vue-plugin.ts#L43.

I think there are 2 methods to implement Vue compilation in Rust:

  1. The same as the js-plugin-vue, use load + transform. In transform, Vue SFC compiler compiles .vue into blocks(js/jsx/ts/tsx, scss/less/etc), then transform js/jsx/ts/tsx block to js/jsx/ts/tsx module, insert import './xx.scss?vue&scoped&hash=xxx' to that module and return its code. So the .vue file are compiled to a script module and several scss/less/css module. Then other plugins will be responsible for compiling js/jsx/ts/tsx and scss/less, including transforming and code generation. That's how vite and webpack deal with .vue.
  2. Treat .vue as a separate basic module type in Farm, you need to implement following hooks:
    • load: use load first to support load .vue as custom module type like ModuleType::Customer("vue")
    • parse: then use parse to parse your custom module type
    • analyze_deps: return the specifiers like ./helpers to resolve dependencies.
    • process_module: do AST transformation for your custom module type.
    • build_end: split your vue module type into js/jsx/ts/tsx and css modules that farm natively supports, and connect them in the ModuleGraph, so you do not need to implement optimization and code generation hooks again for .vue.

I prefer method 1 because it's simple, the plugin-vue just needs to compile itself to js/jsx/ts/tsx and scss/less/css and other plugins will take the rest. If you want to reuse ast between transform and parse, you can implement both hooks, just cache the ast in transform and then reuse it in parse.

phoenix-ru commented 10 months ago

Big thanks for the explanation, I would certainly try the flow you suggested. Small follow-up question: do you have examples of how to cache intermediate plugin results (meaning cache managed by Farm), or should it be an own static cache?

I was initially thinking of implementing method 1 as well, because it is mentally easier. But as you dig in the webpack/rollup loader implementations, especially when tracing Typescript flows, you notice that .vue are sometimes parsed 3-4 times for every import './xx.vue?vue&block=style'. Not only that, but the compiler tends to produce pretty bloated code when it treats SFC blocks as independent JS modules (this is a big issue for projects with 2000+ modules). And compilation in such cases gets as slow as < 100 modules/sec in best case (raw JS perf is around 1500). Which are all the reasons I started the Rust implementation.

That's why I think of implementing method 2, even if it means adapting it to every major bundler API out there.

wre232114 commented 10 months ago

I think if the compiled blocks can be cached and reused, the performance of these two methods wouldn't be a big difference.

Farm doesn't manage cache for plugins, plugins should handle their cache themselves, see ast cache examples: https://github.com/farm-fe/farm/blob/main/crates/plugin_css/src/lib.rs#L49. Farm reuses ast for css modules implementation in the example