编写 Rust 插件
用 Rust 写你的插件是一个推荐的方式,因为 Rust 插件比 JavaScript 插件更快和富有表现力。一个 Rust 插件应该是实现了 farmfe_core::plugin::Plugin trait 的 struct, 例如
#![deny(clippy::all)]
use farmfe_core::{config::Config, plugin::Plugin};
use farmfe_macro_plugin::farm_plugin;
// define your rust plugins
#[farm_plugin]
pub struct FarmPluginExample {}
impl FarmPluginExample {
// 一个 Rust 插件必须导出一个名称是 new 的函数 并且初始化的时候接受两个参数
fn new(config: &Config, options: String) -> Self {
Self {}
}
}
// 实现插件的 trait 来定义插件 hooks
impl Plugin for FarmPluginExample {
fn name(&self) -> &str {
"FarmPluginExample"
}
// more hooks here
}
Rust 插件注意事项:
struct必须是pub并且需要有#[farm_plugin]属性struct必须实现Plugintrait, 并且name方法必须要实现struct必须导出一个new的方法,在初始化的时候接受两个参数 第一个参数是&Config, 第二个参数是String。new方法在插件加载的时候调用。Config是 farm 项目的配置String是插件的选项
我们同时提供了 Rust 插件示例代码仓库:farm-rust-plugin-example
约定
对于特定的 Farm 插件
-
一个 Farm 的 Rust 插件应该有一个
farm-plugin-前缀的名称并且语义清晰 -
package.json 里面有
farm-plugin-关键字
如果你的插件仅仅适配特定框架,其名称应遵循以下前缀格式:
-
farm-plugin-vue-: 作为 Vue 插件前缀 -
farm-plugin-react-: 作为 React 插件前缀 -
farm-plugin-svelte-: 作为 Svelte 插件前缀 -
...
概念
在开始编写 Rust 插件之前,你应该了解以下概念:
- module_type:模块的类型,他可能是
js,ts,css,sass,json等等。Farm 原生支持js/ts/jsx/tsx,css,html,json,static asserts(png, svg等等)。module_type会被load或者transform钩子返回 - resolved_path 和 module_id:
resolved_path是一个模块的绝对路径,module_id是一个模块的唯一 id,通常是模块对于项目根目录的相对路径+query。例如 我们引用了一个模块import './a?query'resolved_path 是/project/src/a.tsmodule_id 是src/a.ts?query - context: 所有的插件都会接受一个
context参数,它有 Farm 项目的整个编译上下文,你可以从里面拿到 ModuleGraph,Module,Resources等等 - Resource and Resource Pot:
Resource是输出出来的最终打包产物,Resource Pot是资源的抽象表示,类似于其他打包器的Chunk。在 Farm 项目中,我们首先从ModuleGraph生成Resource Pots, 渲染Resource Pots,最终从Resource Pots生成 `Resource
模块类型
在 Farm 中,一切都被认为是“一等公民”,因此 Farm 设计 module_type 来标识模块类型,并在用不同的插件处理不同的模块类型
Module_type 由 load 钩子返回,并且可以由 transform 钩子转换。Farm 原生支持 js/ts/jsx/tsx、css、html、json、static assets(png、svg等)。对于这些模块类型,你可以直接在 load 或 transform hook 中返回。但是如果你想处理自定义模块类型,你需要实现其他钩子例如 parse, render_resource_pot_modules, generate resources 等来控制如何对自定义模块进行类型解析,渲染和生成资源。
创建插件
Farm 提供了官方模板来帮助你快速创建 Rust 插件:
pnpm create farm-pluginnpm create farm-plugin@latestyarn create farm-plugin然后按照提示创建插件
或者直接运行以下命令创建插件:
pnpm create farm-plugin my-farm-plugin --type rustnpm create my-farm-plugin --type rustyarn create my-farm-plugin --type rust上面的命令会在当前目录中创建一个名为 my-farm-plugin 的js插件。——type 可以是 rust 或者 js
插件项目结构
一个插件项目结构如下:
my-farm-plugin
├── .github
│ └── workflows
| ├── release.yml
| ├── build.yml
│ └── ci.yml
├── Cargo.toml
|── .gitignore
├── npm
│ ├── darwin-x64
│ ├── linux-x64-gnu
| ├── win32-x64-msvc
│ └── ...
├── package.json
├── src
│ └── lib.rs
└── rust-toolchain.toml
值得注意的文件和目录:
src/lib.rs: 插件的主要文件,你在这里定义你的插件Cargo.toml: Rust 项目的清单package.json: npm 项目清单npm: 平台特定的二进制包所在的位置。在发布插件之前,这些包应该发布到 npm registry.github/workflows: 用于在 github actions 中交叉构建和发布插件rust-toolchain.toml: rust 工具链文件,它不应该 被手动修改,它应该始终使用 与 farm core相同的版本。
Farm 提供了一个工具 (@farmfe/plugin-tools) 来帮助你构建和发布插件,参考 package.json:
{
// ...
"scripts": {
// build your plugin for current platform
"build": "farm-plugin-tools build --platform -p my_farm_plugin --release",
// publish all platform packages under npm directory to npm registry
"prepublishOnly": "farm-plugin-tools prepublish"
},
// ...
}
开发插件
为了在本地开发和测试你的插件 你首先依据你平台构建插件,运行:
pnpm build
然后你可以使用你构建好的插件,在 frame.config.ts 的 plugins添加你的插件:
import { defineConfig } from '@farmfe/core';
export default defineConfig({
plugins: [
'my-farm-plugin'
]
});
在你的 farm 项目中运行 pnpm i 并且运行 farm start 来运行你的带有你插件的 farm 项目
当对插件进行更改时,应该重新构建插件并重启 farm 项目以查看更改。例如,在你的插件中添加 load 钩子:
// ... ignore other code
impl Plugin for FarmPluginExample {
fn name(&self) -> &str {
"FarmPluginExample"
}
fn load(
&self,
param: &farmfe_core::plugin::PluginLoadHookParam,
_context: &std::sync::Arc<farmfe_core::context::CompilationContext>,
_hook_context: &farmfe_core::plugin::PluginHookContext,
) -> farmfe_core::error::Result<Option<farmfe_core::plugin::PluginLoadHookResult>> {
println!(
"load path: {:?}, id: {:?}",
param.resolved_path, param.module_id
);
Ok(None)
}
}
然后用 pnpm build 重新构建你的插件,用 farm start 重新启动你的 farm 项目,你会看到 load 钩子在编译你的 farm 项目时被调用。
处理 ModuleType
module_type 由 load hook 或 transform hook 返回。你在 load hook 中可以给 module 设置任意的 module type,该模块将由支持该模块类型的相应插件处理。
对于原生支持的模块类型,你可以在 load 钩子中返回模块类型:
// ... ignore other code
impl Plugin for FarmPluginExample {
fn name(&self) -> &str {
"FarmPluginExample"
}
fn load(
&self,
param: &farmfe_core::plugin::PluginLoadHookParam,
_context: &std::sync::Arc<farmfe_core::context::CompilationContext>,
_hook_context: &farmfe_core::plugin::PluginHookContext,
) -> farmfe_core::error::Result<Option<farmfe_core::plugin::PluginLoadHookResult>> {
// handle virtual module
if param.module_id.starts_with("virtual:my-css:css") {
// return module type and content
Ok(Some(farmfe_core::plugin::PluginLoadHookResult {
module_type: "css".to_string(),
content: ".red { color: red; }".to_string(),
..Default::default()
}))
} else {
Ok(None)
}
}
}
对于原生支持的模块类型,你应该使用 transform hook 将模块类型转换为原生支持的模块类型,否则你需要实现 parse、renderResourcePot hook 来处理你的自定义模块类型:
// ... ignore other code
impl Plugin for FarmPluginExample {
fn name(&self) -> &str {
"FarmPluginExample"
}
fn transform(
&self,
param: &farmfe_core::plugin::PluginTransformHookParam,
_context: &std::sync::Arc<farmfe_core::context::CompilationContext>,
_hook_context: &farmfe_core::plugin::PluginHookContext,
) -> farmfe_core::error::Result<Option<farmfe_core::plugin::PluginTransformHookResult>> {
// module type guard is required
if matches!(param.module_type, ModuleType::Custom("sass")) {
// compile sass and transform the module type from sass to css
Ok(Some(farmfe_core::plugin::PluginTransformHookResult {
module_type: "css".to_string(),
content: compileSass(param.content),
..Default::default()
}))
} else {
Ok(None)
}
}
}
或者实现 parse,render_resource_pot_modules 钩子来处理你的自定义模块类型 参考 farm 如何原生处理 css 插件如何处理 css 模块类型的 farm-plugin-css
处理插件选项
rust 插件选项可以在farm.config.ts中配置:
import { defineConfig } from '@farmfe/core';
export default defineConfig({
plugins: [
['my-farm-plugin', {
// plugin options
myOption: 'myOption'
}]
]
});
该选项将被 json 序列化并传递给插件的new方法,你可以在new方法中处理该选项:
// ... ignore other code
// define your rust plugin options
#[derive(serde::Deserialize)]
pub struct Options {
pub my_option: Option<String>,
}
impl FarmPluginExample {
fn new(config: &Config, options: String) -> Self {
// deserialize the options
let my_option: Options = serde_json::from_str(&options).unwrap();
// handle the options...
Self {}
}
}
请注意,你应该将依赖 serde 和 serde_json 添加到你的Cargo.toml中。来支持反序列化:
[dependencies]
# ... ignore other code
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
在插件里面使用 farm_core
Farm 在 farmfe_core crate 中暴露所有核心结构和工具函数。更多细节请参阅farmfe_core文档。
警告
在插件中使用 SWC
请注意,你的 rust 插件不应该使用任何与 SWC 相关的包,如swc_common、swc_transforms等。SWC 在进程中存储全局状态,当你在插件中使用 SWC 时,它可能会导致死锁。
如果你想要修改你的 Farm 项目的AST,建议写[SWC Plugin](/zh/docs/using-plugins#using-swc plugins)。关于如何编写SWC插件,请参阅编写SWC插件。
选择 Rust 工具链
因为 Farm 的 Rust 插件是一个动态链接库,你应该始终使用和 farm core 相同版本的 Rust 工具链。rust 工具链定义在 rust-toolchain.toml中。它不应该被手动修改。
并且应该始终使用 Rust 构建插件,因为Farm Core 不支持 FFI,也不承诺 ABI 稳定性以提供最佳性能。
插件的兼容性
Farm core 维护了一个向插件暴漏出来一个 API 版本。如果你遇到类似Incompatible Rust Plugin: Current core's version…,这意味着你的插件与当前 farm core版本不兼容。你应该更新你的插件到最新版本来解决这个问题。
对于插件作者来说,你应该重新用最新的 farm core 版本构建和发布插件,来让你的插件与最新的 farm core版本兼容。
交叉构建
一个 Farm Rust 插件是一个特定于平台的动态链接库,你应该为你想要支持的所有平台构建插件。 Farm 提供了一个使用 github actions 构建插件的示例,请参阅.github/workflows/build.yml
默认情况下,farm rust 插件应该针对以下平台构建:
linux-x64-gnulinux-x64-musldarwin-x64win32-x64-msvclinux-arm64-musllinux-arm64-gnudarwin-arm64win32-ia32-msvcwin32-arm64-msvc
对于公开发布到 npm registry 的插件,建议发布支持上面所有平台的插件。对于私有 rust 插件,你可以为任何你想支持的平台构建插件。
发布
发布 Rust插件的有以下的步骤:
- 交叉构建动态链接库的 Rust 插件,详情请参阅交叉构建
- 将二进制文件复制到 npm 目录下,例如:复制到
npm/linux-x64-gnu/index.farm - 在 npm 目录下发布平台特定的包,你可以使用
farm-plugin-tool prepublish在 npm 目录下发布包 - 发布包本身
参见示例github actions publish workflow
例子
我们将使用 @farmfe/plugin-sass 作为一个真正的 Rust 插件示例。这个插件将支持在你的项目中编译.scss 和 .sass 文件
定义一个插件
导出一个名为 FarmPluginSass 的 Rust struct
use farmfe_macro_plugin::farm_plugin;
// 1. define a struct with #[farm_plugin] attribute
#[farm_plugin]
pub struct FarmPluginSass {
sass_options: String,
regex: Regex,
}
impl FarmPluginSass {
// 2. define a new method with 2 arguments
pub fn new(_config: &Config, options: String) -> Self {
Self {
sass_options: options,
regex: Regex::new(r#"\.(sass|scss)$"#).unwrap(),
}
}
}
- struct 必须是
pub并且必须有#[farm_plugin]属性。 - 结构体必须导出一个
new方法,该方法接受两个参数作为初始化参数,第一个参数是&Config,第二个参数是String。
实现插件 Trait
Plugin trait 用于定义可以挂接到 farm compiler 的 hooks
use farmfe_core::plugin::Plugin;
use farmfe_macro_plugin::farm_plugin;
// 1. define a struct with #[farm_plugin] attribute
#[farm_plugin]
pub struct FarmPluginSass {
sass_options: String,
regex: Regex,
}
impl FarmPluginSass {
// 2. define a new method with 2 arguments
pub fn new(_config: &Config, options: String) -> Self {
Self {
sass_options: options,
regex: Regex::new(r#"\.(sass|scss)$"#).unwrap(),
}
}
}
// Implement Plugin Trait
impl Plugin for FarmPluginSass {
fn name(&self) -> &str {
"FarmPluginSass"
}
// this plugin should be executed before internal plugins
fn priority(&self) -> i32 {
101
}
}
加载 .scss 文件
实现 load 钩子以支持加载 .scss 文件
// ignore other code ...
// Implement Plugin Trait
impl Plugin for FarmPluginSass {
fn name(&self) -> &str {
"FarmPluginSass"
}
// this plugin should be executed before internal plugins
fn priority(&self) -> i32 {
101
}
fn load(
&self,
param: &farmfe_core::plugin::PluginLoadHookParam,
_context: &std::sync::Arc<farmfe_core::context::CompilationContext>,
_hook_context: &farmfe_core::plugin::PluginHookContext,
) -> farmfe_core::error::Result<Option<farmfe_core::plugin::PluginLoadHookResult>> {
if param.query.is_empty() && self.regex.is_match(param.resolved_path) {
let content = fs::read_file_utf8(param.resolved_path);
if let Ok(content) = content {
return Ok(Some(farmfe_core::plugin::PluginLoadHookResult {
content,
module_type: ModuleType::Custom(String::from("sass")),
}));
}
}
Ok(None)
}
}
在 load 钩子中,我们只读取以.scss 或者 .sass结尾的文件,返回文件内容并将其 module_type 设置为ModuleType::Custom(String::from("sass"))。
转化 sass 文件
加载 .scss 文件之后,我们需要在 transform hook 中将其转换为 css,然后 Farm 将在接下来的过程中将其视为 css
// ignore other code ...
fn transform(
&self,
param: &farmfe_core::plugin::PluginTransformHookParam,
context: &std::sync::Arc<farmfe_core::context::CompilationContext>,
) -> farmfe_core::error::Result<Option<farmfe_core::plugin::PluginTransformHookResult>> {
// module type guard is neccessary
if param.module_type == ModuleType::Custom(String::from("sass")) {
// ... ignore other code
// parse options
const options = parse_options(&self.options, param.module_id);
// compile sass to css
let compile_result = compileSass(¶m.content, options);
return Ok(Some(farmfe_core::plugin::PluginTransformHookResult {
content: compile_result.css,
source_map: compile_result.source_map,
// tell farm compiler that we have transformed this module to css
module_type: Some(farmfe_core::module::ModuleType::Css),
ignore_previous_source_map: false,
}));
}
Ok(None)
}
Farm