跳到主要内容
版本:1.0.0

编写 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 必须实现 Plugin trait, 并且 name 方法必须要实现
  • struct 必须导出一个 new 的方法,在初始化的时候接受两个参数 第一个参数是 &Config, 第二个参数是 Stringnew 方法在插件加载的时候调用。 Config 是 farm 项目的配置 String 是插件的选项

我们同时提供了 Rust 插件示例代码仓库:farm-rust-plugin-example

备注

本文章仅仅涵盖如何创建,开发和发布一个 Rust 插件,更多的细节参考 插件 Hooks

约定

对于特定的 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_idresolved_path 是一个模块的绝对路径,module_id是一个模块的唯一 id,通常是模块对于项目根目录的相对路径 + query。例如 我们引用了一个模块 import './a?query' resolved_path 是 /project/src/a.ts module_id 是 src/a.ts?query
  • context: 所有的插件都会接受一个 context 参数,它有 Farm 项目的整个编译上下文,你可以从里面拿到 ModuleGraph,Module,Resources等等
  • Resource and Resource PotResource 是输出出来的最终打包产物, Resource Pot是资源的抽象表示,类似于其他打包器的 Chunk。在 Farm 项目中,我们首先从 ModuleGraph 生成 Resource Pots, 渲染 Resource Pots,最终从 Resource Pots 生成 `Resource

模块类型

在 Farm 中,一切都被认为是“一等公民”,因此 Farm 设计 module_type 来标识模块类型,并在用不同的插件处理不同的模块类型 Module_type load 钩子返回,并且可以由 transform 钩子转换。Farm 原生支持 js/ts/jsx/tsxcsshtmljsonstatic assets(png、svg等)。对于这些模块类型,你可以直接在 loadtransform hook 中返回。但是如果你想处理自定义模块类型,你需要实现其他钩子例如 parserender_resource_pot_modulesgenerate resources 等来控制如何对自定义模块进行类型解析,渲染和生成资源。

创建插件

Farm 提供了官方模板来帮助你快速创建 Rust 插件:

pnpm create farm-plugin

然后按照提示创建插件

或者直接运行以下命令创建插件:

pnpm create farm-plugin 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 --cargo-name my_farm_plugin -p my_farm_plugin --release",
// publish all platform packages under npm directory to npm registry
"prepublishOnly": "farm-plugin-tools prepublish"
},
// ...
}

更多关于构建和发布的细节参考 构建发布章节:

开发插件

为了在本地开发和测试你的插件 你首先依据你平台构建插件,运行:

pnpm build

然后你可以使用你构建好的插件,在 frame.config.tsplugins添加你的插件:

import { defineConfig } from '@farmfe/core';

export default defineConfig({
plugins: [
'my-farm-plugin'
]
});

在你的 farm 项目中运行 pnpm i 并且运行 farm start 来运行你的带有你插件的 farm 项目 当对插件进行更改时,应该重新构建插件并重启 farm 项目以查看更改。例如,在你的插件中添加 load 钩子:

src/lib.rs
// ... 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 项目时被调用。

备注

想了解更多关于插件 hooks , 参阅 插件钩子.

处理 ModuleType

module_typeload hook 或 transform hook 返回。你在 load hook 中可以给 module 设置任意的 module type,该模块将由支持该模块类型的相应插件处理。 对于原生支持的模块类型,你可以在 load 钩子中返回模块类型:

src/lib.rs
// ... 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 将模块类型转换为原生支持的模块类型,否则你需要实现 parserenderResourcePot hook 来处理你的自定义模块类型:

src/lib.rs
// ... 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)
}
}
}
备注

模块类型保护,如 matches!(module_type, ModuleType::Custom("sass")) transform 钩子是必需的,因为所有模块类型都会调用 transform 钩子,并且你应该只在 transform hook 中处理你的自定义模块类型。parse和其他 hook 也是如此。

或者实现 parserender_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方法中处理该选项:

src/lib.rs
// ... 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 {}
}
}

请注意,你应该将依赖 serdeserde_json 添加到你的Cargo.toml中。来支持反序列化:

[dependencies]
# ... ignore other code
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
备注

不能被 json 序列化选项不被支持。这意味着你只能使用字符串、数字、布尔值、数组、对象等类型。不支持函数选项

在插件里面使用 farm_core

Farm 在 farmfe_core crate 中暴露所有核心结构和工具函数。更多细节请参阅farmfe_core文档。

备注

如果你想在插件中使用 swc 中的 ModuleProgram 等结构,你应该使用由 farm_core 重新暴露的 farmfe_core::swc_ast。因为 farm_core 使用的 swc 版本可能与你在插件中使用的 swc 版本不同,并且 farm_core 使用的 swc 版本保证与 farm_core 兼容。

警告

在插件中使用 SWC

请注意,你的 rust 插件不应该使用任何与 SWC 相关的包,如swc_commonswc_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 承诺与相同主版本的 AP I兼容,例如,如果你的插件兼容 Farm core 1.0.0,那么它也应该兼容 Farm core 1.1.0、1.2.0等,这意味着你的插件将始终适用于相同的 Farm 主版本。

交叉构建

一个 Farm Rust 插件是一个特定于平台的动态链接库,你应该为你想要支持的所有平台构建插件。 Farm 提供了一个使用 github actions 构建插件的示例,请参阅.github/workflows/build.yml

默认情况下,farm rust 插件应该针对以下平台构建:

  • linux-x64-gnu
  • linux-x64-musl
  • darwin-x64
  • win32-x64-msvc
  • linux-arm64-musl
  • linux-arm64-gnu
  • darwin-arm64
  • win32-ia32-msvc
  • win32-arm64-msvc

对于公开发布到 npm registry 的插件,建议发布支持上面所有平台的插件。对于私有 rust 插件,你可以为任何你想支持的平台构建插件。

提示

因为 rust 插件是一个纯动态链接库,如果您有关于如何为特定平台构建插件的问题,只需谷歌如何在 rust 中为该平台构建动态链接库。

发布

发布 Rust插件的有以下的步骤:

  1. 交叉构建动态链接库的 Rust 插件,详情请参阅交叉构建
  2. 将二进制文件复制到 npm 目录下,例如:复制到 npm/linux-x64-gnu/index.farm
  3. 在 npm 目录下发布平台特定的包,你可以使用farm-plugin-tool prepublish在 npm 目录下发布包
  4. 发布包本身

参见示例github actions publish workflow

例子

我们将使用 @farmfe/plugin-sass 作为一个真正的 Rust 插件示例。这个插件将支持在你的项目中编译.scss.sass 文件

定义一个插件

导出一个名为 FarmPluginSass 的 Rust struct

src/lib.rs
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(&param.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 支持的更多能力,请参阅插件钩子

Extremely Fast Web Build Tool Written in Rust

Copyright © 2024 Farm Community. Built with Docusaurus.