- 前端构建
- 构建工具
- 开发工具
【免费下载链接】farm
Extremely fast Vite-compatible web build tool written in Rust
本文围绕 Farm 仓库中的设计文档 2026-05-30-plugin-vue-jsx-design.md 展开,系统讲解@farmfe/plugin-vue-jsx这款 Farm 原生 Rust 插件如何把 Vue JSX/TSX 语法(v-model、v-show、v-html、插槽等指令)在构建期转换为 Vue 3 运行时调用。文章将带你走通插件从配置、Hook 触发、AST 变换到产物生成的完整链路,掌握全部 7 个配置项的作用与默认值,并理解其与旧方案(@vitejs/plugin-vue-jsx/swc-plugin-vue-jsx)的架构差异。读者读完后,可以独立配置并调优该插件,也能直接读懂其 Rust 源码与测试夹具。
背景:为什么需要 Farm 原生 Vue JSX 插件
在@farmfe/plugin-vue-jsx出现之前,Farm 用户要在 Vue 项目里使用 JSX/TSX,通常需要两条路:
- 借助 Farm 的 Vite 插件适配器,把
@vitejs/plugin-vue-jsx挂载到vitePlugins上运行; - 使用以 WASM 字节码形式分发的 SWC 插件
swc-plugin-vue-jsx。
这两种方案都绕了路:Vite 适配器带来一层额外的兼容开销;SWC 插件以 WASM 形式运行,与 Farm 主链路存在二进制交互成本。而 Farm 自身就是通过farmfe_toolkit以原生库方式集成 SWC 的——那么与其在 WASM 沙箱里跑一个 JSX 转换器,不如直接把 SWC 的 visitor 以原生 Rust 插件形态嵌入 Farm 的编译管线,一次性消除 WASM 开销和 Vite 适配器依赖。这正是设计文档 Summary 给出的核心动机。
从仓库实际结构看,该设计已经落地:插件以独立 crate 形式存在于 rust-plugins/vue-jsx/,npm 包名为@farmfe/plugin-vue-jsx(见 package.json),并在 website/docs/plugins/official-plugins/vue-jsx.mdx 提供了官方文档。
插件目录结构与工程形态
设计文档规划的目录结构与仓库实际实现一一对应:
rust-plugins/vue-jsx/ ├── Cargo.toml # crate: farmfe_plugin_vue_jsx,cdylib + rlib ├── package.json # @farmfe/plugin-vue-jsx ├── options.d.ts # 共享的 TS 选项类型声明 ├── scripts/ │ ├── func.js # export default (options) => [binPath, options] │ ├── index.js # 平台二进制解析器 │ └── index.d.ts # TypeScript 声明 ├── npm/ # 各平台 npm 包(由 farm-plugin-tools 生成) ├── src/ │ ├── lib.rs # 插件实现:new()、transform hook │ ├── options.rs # Options 结构体 + serde 反序列化 │ ├── directive.rs # v-model/v-html/v-text/v-show/v-slots/自定义指令解析 │ ├── patch_flags.rs # Vue PatchFlags 位标志 │ ├── slot_flag.rs # SlotFlag 枚举(Stable / Dynamic) │ ├── util.rs # build_slot_helper、dedupe_props、decouple_v_models、transform_text、is_on、is_constant │ └── resolve_type.rs # defineComponent props/emits 的 TS 类型解析 └── tests/ ├── mod.rs # 集成测试 └── fixtures/ # 从 swc-plugin-vue-jsx/test/fixture/ 移植而来工程形态上有几个值得注意的点:
- crate 双形态:Cargo.toml 中
crate-type = ["cdylib", "rlib"],cdylib用于编译成原生二进制供 Farm 通过 napi 加载,rlib用于本地单元测试直接链接调用; - 平台二进制分发:package.json 的
napi.targets列出了 x64/arm64、linux-gnu/musl、windows-msvc、darwin 共 9 个目标平台,scripts/index.js 按process.platform/process.arch运行时解析对应平台的.farm二进制(musl 通过process.report的glibcVersionRuntime判断); - JS 入口极薄:scripts/func.js 只有一行——
export default (options) => [binPath, options],把用户传入的 options 原样透传给 Rust 侧,符合 Farm 原生插件“options 以 JSON 字符串传给 Rust”的约定。
插件数据流:从配置到产物
设计文档给出了 4 步数据流,与 lib.rs 的Plugin for FarmPluginVueJsx实现完全吻合:
- 用户配置:
import vueJsx from '@farmfe/plugin-vue-jsx'; plugins: [vueJsx({...})],JS 侧将 options 透传; - 插件实例化:Farm 调用
FarmPluginVueJsx::new(config, options_json),把 options JSON 字符串存入options_json字段;get_options()在每次 transform 时通过serde_json::from_str反序列化(空字符串或解析失败则回落到Options::default()); - 逐模块 transform:对每个
.jsx/.tsx模块,transformhook 先检查param.module_type是否为ModuleType::Jsx | ModuleType::Tsx(否则直接return Ok(None)放行),随后经farmfe_toolkit的parse_module把源码解析为 SWC AST,构造VueJsxTransformVisitor(携带 options、unresolved_mark、comments、resolved_path),执行ast.visit_mut_with(&mut visitor); - 生成产物:用
create_codegen_config+codegen_module重新代码生成,返回PluginTransformHookResult { content, module_type: Some(param.module_type.clone()), source_map: None, ignore_previous_source_map: false }。
一个细节:TSX 模块使用Syntax::Typescript(TsSyntax { tsx: true, .. })解析,普通 JSX 则用Syntax::Es(Default::default())——这保证了.tsx中的类型语法能被 SWC 正确消费。
Plugin Hook 面:只依赖 transform
设计文档明确:该插件只需要transform一个 hook,不需要load、config或update_modules。这一点在 lib.rs 中得到验证——实现里只有name()(返回"FarmPluginVueJsx")、priority()(返回 100)和transform()。
transform的输入输出契约如下:
| 输入 | 类型 | 说明 |
|---|---|---|
param.module_type | ModuleType::Jsx \| ModuleType::Tsx | 仅处理 JSX/TSX 模块 |
param.content | String | 原始源码 |
param.resolved_path | &str | 文件路径(用于错误上报) |
| 输出 | 类型 | 说明 |
|---|---|---|
content | String | 转换后的 JS/TS 代码 |
module_type | Option<ModuleType> | 保持原类型(Jsx→Jsx,Tsx→Tsx) |
source_map | Option<String> | 初始为None |
ignore_previous_source_map | bool | 设计文档为true,实际实现为false,源码注释说明原因:AST 重写会使旧的 source map 失效 |
全部 7 个配置项详解
插件的 Options 结构体定义在 options.rs,使用#[serde(rename_all = "camelCase", default)]反序列化,保证与 options.d.ts 的 TS 类型一一对应。下表完整列出设计文档中的 7 个配置项:
interface VueJsxPluginOptions { /** Convert `on` / `nativeOn` attrs to use `@vue/babel-helper-vue-transform-on` */ transformOn?: boolean; // default: false /** Inject PatchFlags for optimized VNode updates */ optimize?: boolean; // default: false /** Custom element detection patterns (regex strings) */ customElementPatterns?: string[]; // default: [] /** Merge attribute objects with `mergeProps` from Vue */ mergeProps?: boolean; // default: true /** Enable object slot detection via `_isSlot` helper */ enableObjectSlots?: boolean; // default: true /** Custom pragma (e.g. `h`), overrides `createVNode` */ pragma?: string; // default: undefined (uses createVNode) /** Resolve TypeScript types for `defineComponent` props/emits */ resolveType?: boolean; // default: false }transformOn:on/nativeOn 的运行时转换
当transformOn: true时,属性on与nativeOn会被包成对@vue/babel-helper-vue-transform-on默认导出的调用(lib.rs 中生成_transformOn(...)调用,并在模块头部自动插入import _transformOn from '@vue/babel-helper-vue-transform-on',见 lib.rs)。
需要特别提醒:这是一个用户侧的外部 JS 依赖,不随插件分发。开启该选项后必须在项目里手动安装:
pnpm add @vue/babel-helper-vue-transform-on这与swc-plugin-vue-jsx、Vite 插件的既有行为保持一致(见设计文档 "Dependency mapping" 一节与 vue-jsx.mdx 的 transformOn 说明)。
optimize:VNode 更新优化的 PatchFlags 注入
optimize: true时插件会分析属性与插槽结构,向createVNode调用注入 PatchFlags 数字参数和动态属性名数组,让 Vue 3 运行时在 diff 时跳过不必要的检查。patch_flags.rs 定义了完整位标志:
pub struct PatchFlags: i16 { const TEXT = 1; // 1 << 0 const CLASS = 1 << 1; const STYLE = 1 << 2; const PROPS = 1 << 3; const FULL_PROPS = 1 << 4; const HYDRATE_EVENTS = 1 << 5; const STABLE_FRAGMENT = 1 << 6; const KEYED_FRAGMENT = 1 << 7; const UNKEYED_FRAGMENT = 1 << 8; const NEED_PATCH = 1 << 9; const DYNAMIC_SLOTS = 1 << 10; const HOISTED = -1; const BAIL = -2; }分析逻辑在transform_attrs(lib.rs):出现展开属性(SpreadElement)或动态计算键时置FULL_PROPS;否则按class绑定 →CLASS、style绑定 →STYLE、非空动态属性集 →PROPS、非onclick/onUpdate:modelValue的事件绑定 →HYDRATE_EVENTS逐项累加;若同时存在ref或指令,则叠加NEED_PATCH。
customElementPatterns:自定义元素识别
该配置是一个正则字符串数组。is_component判断(lib.rs)会遍历这些 pattern:若标签名匹配任一正则,则视为自定义元素(按原生元素处理、不转换为组件插槽);同时标准 HTML/SVG 标签(来自css_dataset::tags)永远按原生元素处理。测试夹具 custom-element 展示了用法:
{ "customElementPatterns": ["foo"] }配合transform_tag(lib.rs):对于匹配 pattern 的元素,生成的createVNode第一个参数是字符串字面量,而不是组件标识符。
mergeProps:属性对象合并
mergeProps: true(默认)时,遇到展开属性({...attrs})会把已有 props 收集起来,与展开对象一起交给 Vue 的mergeProps(...)调用合并(lib.rs)。展开属性还会触发has_dynamic_keys = true,从而产生FULL_PROPSPatchFlag。合并过程中 util.rs 的dedupe_props会做键去重:对重复的class、style和on*事件属性,把多个值收进数组;其余重复键保留首个。
enableObjectSlots:对象插槽检测
该选项控制是否注入_isSlot辅助函数。generate_slot_helper惰性生成名为_isSlot的私有标识符,build_slot_helper(util.rs)生成如下形态的函数定义并注入模块头部:
function _isSlot(s) { return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !isVNode(s)) }当组件 JSX 的唯一子节点是标识符/表达式时,插件会生成_isSlot(x) ? x : wrapChildren(...)三元表达式:如果子节点是函数或普通对象(且不是 VNode),说明调用方传的是“具名插槽对象”,直接原样透传;否则包成默认插槽。关闭该选项(disable-object-slot 夹具)则退化为总是包默认插槽。
pragma:自定义创建 VNode 的函数
优先级为:源码注释中的@jsxpragma > options.pragma > 默认createVNode。search_jsx_pragma(lib.rs)从模块及其语句的 leading comments 中扫描@jsx标记;get_pragma(lib.rs)再兜底 options.pragma,最后才从vue导入createVNode。测试夹具 custom-pragma-in-options 验证了 options 路径({"pragma": "custom"}),custom-pragma-in-comment 验证了注释路径。
resolveType:defineComponent 的类型推断
resolveType: true时,插件会从defineComponent(setup)的 setup 函数参数类型注解中提取 props/emits 类型,反推为运行时选项注入defineComponent的第二个参数(lib.rs 的visit_mut_call_expr与inject_define_component_option)。例如 resolve-props-types/options-object 夹具:
defineComponent((props: { foo: number }) => {}, { props: { foo: { type: Number, required: true } } });底层 resolve_type.rs 实现了相当完整的类型降级:支持 interface / type alias、extends继承、interface 合并、索引访问类型(T[K])、交叉/联合类型、Partial/Required/Pick/Omit工具类型、以及把string|number|boolean等关键字类型映射为运行时构造器。生成mergeDefaults(...)调用时还会通过 comments 机制附加/*#__PURE__*/注解(resolve_type.rs),便于压缩器做无用代码消除。
指令转换机制:directive.rs 的解析核心
directive.rs 是整个转换器的指令解析核心。识别入口is_directive判断属性名是否以v-或v+ 大写字母开头(matches!(name.as_bytes(), [b'v', b'-' | b'A'..=b'Z', ..])),随后parse_directive按名称分发到五类:
v-html/v-text:分别转换为innerHTML/textContent属性,并标记为动态属性(见 v-html 夹具:<h1 v-html="<div>foo</div>" />→_createVNode("h1",{"innerHTML":"<div>foo</div>"},null));v-model:解析出 value、argument(v-model:xxx)与 modifiers,按目标元素类型选择运行时指令——select→vModelSelect、textarea→vModelText,普通元素按type属性区分vModelCheckbox(checkbox)、vModelRadio(radio)、vModelText(其余字符串/无 type),动态类型则用vModelDynamic(lib.rs);v-show:转换为withDirectives(vnode, [[vShow, value]]);v-slots:提取标识符/对象形式的插槽对象,合并进组件 children;- 自定义指令(
v-xxx):统一转换为resolveDirective("xxx")运行时解析。
v-model在组件与原生元素上的形态不同(lib.rs):
- 组件上:编译为
modelValue/onUpdate:modelValue属性和modelModifiers对象(对应v-model:xxx则是xxx/onUpdate:xxx/xxxModifiers),并通过$event => (value) = $event箭头函数完成双向绑定; - 原生元素上:走
withDirectives+ 运行时指令(如vModelCheckbox)。
v-model的修饰符解析很灵活:属性名中的_后缀(v-model_trim)、数组字面量(v-model={[value, "arg", ["modifier"]]})、v-model:arg_modifier命名空间写法均支持。修饰符集合最终转换为对象字面量,如{"modifier": true}。
v-models(多模型绑定)是较特殊的一条路径:visit_mut_jsx_opening_element(lib.rs)在进入元素时先把v-models的二维数组属性解耦成多个独立v-model属性(util.rs 的decouple_v_models),再由常规指令流程逐一处理。看 v-models 夹具的转换结果:
// 输入 <C v-models={[[foo, ["modifier"]], [bar, "bar", ["modifier1", "modifier2"]]]} /> // 输出 _createVNode(C,{ "modelValue":foo,"modelModifiers":{"modifier":true},"onUpdate:modelValue":$event=>(foo)=$event, "bar":bar,"barModifiers":{"modifier1":true,"modifier2":true},"onUpdate:bar":$event=>(bar)=$event },null);组件、插槽与子节点转换规则
is_component(lib.rs)决定 JSX 元素被当作组件还是原生元素:标准 HTML/SVG 标签、匹配customElementPatterns的标签、以及Fragment(已有导入时)和KeepAlive特判之外,大写开头的标识符、成员表达式均视为组件。transform_tag对组件的处理分三种:
- 首字母大写、可在当前文件解析的标识符 → 保留原标识符;
- 带
unresolved_mark(无法从当前文件解析)的标识符 → 转换为resolveComponent("Name")调用; JSXMemberExpr(如<A.B>)→ 保留成员表达式。
子节点转换规则(transform_children,lib.rs):
- JSX 文本 →
createTextVNode("...")(空文本被过滤;文本先经 util.rs 的transform_text做首尾 trim 与多行折叠); - 单个标识符/表达式子节点(组件场景)→ 结合
enableObjectSlots生成_isSlot三元判断; - 函数/箭头函数子节点 → 包成
{ default: () => ... }对象插槽; - 对象字面量子节点 → 直接作为插槽对象,
optimize开启时追加_槽位标志; - 其余多节点 → 原生元素用数组、组件用
{ default: () => [...] }包装。
SlotFlag(slot_flag.rs,Stable = 1/Dynamic = 2)在optimize模式下通过slot_flag_stack向上冒泡:子表达式若是本地标识符引用则标记为Dynamic,最终写入插槽对象的_键(与 Vue 编译器的SLOT_FLAG约定一致),并随插槽对象透传给运行时。
从 swc-plugin-vue-jsx 移植的关键差异
设计文档的 "Migration Change Log" 一节记录了移植过程中最有价值的工程决策,这些都在源码中得到了落实:
错误处理:HANDLER.with(...) → Result::Err
原 SWC 插件使用HANDLER.with(handler, handler.span_err(...))处理约 10 处诊断(如 v-text 缺表达式、类型引用无法解析、v-model 形状非法)。Farm 插件模型没有线程局部错误处理器,因此所有错误点统一返回Err(CompilationError::TransformError { resolved_path, msg })——例如解析失败、codegen 失败、非法 UTF-8 输出都会带[vue-jsx]前缀信息返回给编译管线(lib.rs)。另有一部分“软错误”直接用eprintln!("[vue-jsx] ...")打印提示后跳过该元素(如 v-models 参数不是二维数组),保证单点错误不拖垮整个模块编译。
注释基础设施
swc_plugin通过 SWC 插件运行器提供metadata.comments;Farm 侧则从swc_ecma_parser的解析选项中获得 comments。parse_module返回的comments会被传入 visitor,并在 codegen 时通过CodeGenCommentsConfig重新写回。注释机制服务于两处(设计文档明确指出):
resolve_type:在生成的mergeDefaults(...)调用上附加/*#__PURE__*/;- pragma 搜索:从 leading comments 中读取
@jsx指令。
依赖映射
| swc-plugin-vue-jsx | farmfe-plugin-vue-jsx |
|---|---|
swc_core | farmfe_core+farmfe_toolkit+farmfe_macro_plugin |
bitflags | bitflags(同 crate) |
css_dataset | css_dataset(同 crate) |
fnv | fxhash(Farm 约定) |
indexmap | indexmap(同 crate) |
regex | farmfe_core::regex(实际为regex+ 自定义 serde 包装,见 options.rs) |
serde | serde(同 crate) |
对应 Cargo.toml 的依赖声明:farmfe_core、farmfe_macro_plugin(提供#[farm_plugin]宏)、farmfe_toolkit(re-export SWC 的 parser/codegen/visit/atoms)、farmfe_toolkit_plugin_types(SWC 插件代理类型)、serde、bitflags、css_dataset、indexmap、fxhash、regex。其中regex被包装成Regex新类型并实现自定义Deserialize(options.rs),非法正则字符串会在反序列化时报出明确的a valid regex错误。
测试体系:43 个夹具的移植与验证
设计文档要求“移植 swc-plugin-vue-jsx 的全部 43 个测试夹具”。从 tests/fixtures/ 看,每个夹具包含input.jsx+output.js,需要不同选项的夹具额外携带config.json;resolve-props-types下还有disabled子目录验证“resolveType 关闭时不输出 props 选项”的反向行为。
测试入口 tests/mod.rs 提供两个核心工具函数:
make_plugin(options: &str):用Config::default()构造FarmPluginVueJsx;assert_transform(fixture_name):读取夹具的 input/expected,以ModuleType::Tsx跑 transform 后做 trim 后的全等比较。
测试用例覆盖(与设计文档的示例完全一致):
#[test] fn v_model_with_checkbox() { assert_transform("v-model-with-checkbox"); }具体夹具示例(v-model-with-checkbox):
// input.jsx <input type="checkbox" v-model={test} /> // output.js import{createVNode as _createVNode,vModelCheckbox as _vModelCheckbox,withDirectives as _withDirectives}from"vue"; _withDirectives(_createVNode("input",{"type":"checkbox","onUpdate:modelValue":$event=>(test)=$event},null),[[_vModelCheckbox,test]]);这个输出同时印证了前文所述的多条规则:type="checkbox"选择vModelCheckbox运行时指令、组件内(此处为原生 input 元素特殊处理)生成onUpdate:modelValue赋值箭头函数、vue命名导入自动合并为单条 import 语句。此外还有一个专门测试ignores_non_jsx_files,验证.js模块会返回None而不被误转换。
v-show夹具则展示了 withDirectives 与文本节点转换的协同:
// <div v-show={x}>vShow</div> import{createTextVNode as _createTextVNode,createVNode as _createVNode,vShow as _vShow,withDirectives as _withDirectives}from"vue"; _withDirectives(_createVNode("div",null,[_createTextVNode("vShow")]),[[_vShow,x]]);边界与不属于本插件的职责
设计文档 "Out of Scope" 明确划清了插件边界,避免与 Farm 其他模块混淆:
- HMR:不需要实现。JSX 转换是幂等的,转换结果仍是普通 JS 模块,Farm 内置 HMR 直接接管;
- Vue SFC 编译:
.vue单文件组件由既有的@farmfe/plugin-vue处理(见 rust-plugins/vue/); @vue/babel-helper-vue-transform-on打包:属于用户侧依赖,不随插件分发。
这也解释了为什么该插件只需transform一个 hook——SFC 解析、模块依赖更新都不在它的职责内,JSX/TSX 纯函数式变换与 Farm 编译管线的其他环节天然解耦。
官方集成方式与文档现状
按照 website/docs/frameworks/vue.mdx 的 "Integrating jsx" 一节,在 Vue 项目中启用该插件的标准配置为:
// farm.config.ts import { defineConfig } from '@farmfe/core'; import vue from '@farmfe/plugin-vue'; import vueJsx from '@farmfe/plugin-vue-jsx'; export default defineConfig({ plugins: [vue(), vueJsx()], });官方插件文档 website/docs/plugins/official-plugins/vue-jsx.mdx 给出了带选项的写法(如同时开启optimize、transformOn、resolveType)与安装命令(npm/yarn/pnpm 三选一),并提醒 transformOn 模式下需额外安装@vue/babel-helper-vue-transform-on。设计文档中规划的“在官网框架页替换旧 jsx 方案、在 official-plugins 新增 vue-jsx.mdx”两项更新均已落地。
总结
@farmfe/plugin-vue-jsx是一个典型的 Farm 原生 Rust 插件样板:入口极薄、单 hook、纯函数式 AST 变换、用大量夹具做输出快照测试。它把 Vue 官方 JSX 编译器的能力(指令、插槽、PatchFlags、类型推导)以原生二进制形态嵌入 Farm 构建链,为 Vue + JSX/TSX 项目提供了零 JS 运行时开销的编译路径。无论你是要接入该插件,还是参考它编写自己的 Farm Rust 插件,设计文档 与 rust-plugins/vue-jsx/ 源码都是一份自洽、可验证的完整参考资料。
- 前端构建
- 构建工具
- 开发工具
【免费下载链接】farm
Extremely fast Vite-compatible web build tool written in Rust
相关推荐
Farm 原生 Rust 插件 @farmfe/plugin-vue-jsx 全解析:从 SWC 移植到 Vue JSX/TSX 构建流水线
Farm 原生 Rust 插件 @farmfe/plugin vue jsx 全解析:从 SWC 移植到 Vue JSX/TSX 构建流水线 @farmfe/p
前端构建构建工具开发工具Farm 官方 Vue 插件 `@farmfe/plugin-vue` 全解析:基于 fervid 的 Rust 原生 SFC 编译实现与落地指南
Farm 官方 Vue 插件 @farmfe/plugin vue 全解析:基于 fervid 的 Rust 原生 SFC 编译实现与落地指南 导读 @farm
前端构建构建工具开发工具@farmfe/plugin-vue 设计剖析:用 fervid 在 Rust 侧构建 Farm 的 Vue 3 SFC 编译插件
@farmfe/plugin vue 设计剖析:用 fervid 在 Rust 侧构建 Farm 的 Vue 3 SFC 编译插件 导读 @farmfe/plu
前端构建构建工具开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考