Writing Rust Plugins
Rust plugins are the recommended way to write your plugins cause Rust plugins are much faster and powerful than Js Plugins. A Rust plugin is a struct
that implements farmfe_core::plugin::Plugin
trait, example:
#![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 {
// a Rust plugin must export a new method that accepts 2 arguments for initializationใ
fn new(config: &Config, options: String) -> Self {
Self {}
}
}
// Implement Plugin trait to define plugin hooks
impl Plugin for FarmPluginExample {
fn name(&self) -> &str {
"FarmPluginExample"
}
// more hooks here
}
Note for a Rust plugin struct:
- The struct must be
pub
and#[farm_plugin]
attribute is required. - The struct must implement
Plugin
trait, and thename
method must be implemented. - The struct must export a
new
method that accepts 2 arguments for initialization, the first argument is&Config
and the second argument isString
. Thenew
method is called when the plugin is loaded, and theConfig
is the farm project config, and theString
is the plugin options.
We also provide a Rust plugin example repository: farm-rust-plugin-example.
This document only covers how to create, develop and publish a rust plugin, for more detail about the plugin hooks, see Plugin Hooks.
Conventionsโ
For farm specific Rust plugins:
- The Farm plugin should have a name with a
farm-plugin-
prefix and clear semantics. - Include the
farm-plugin-
keyword in package.json.
If your plugin is only applicable to a specific framework, its name should follow the following prefix format:
farm-plugin-vue-
: Prefix as a Vue pluginfarm-plugin-react-
: Prefix as a React pluginfarm-plugin-svelte-
: Prefix as a svelte plugin- ...
Conceptsโ
Before you start to write your rust plugin, you should know the following concepts:
- module_type: The type of the module, it can be
js
,ts
,css
,sass
,json
, etc. Farm supportsjs/ts/jsx/tsx
,css
,html
,json
,static assets(png, svg, etc)
natively.module_type
is returned byload
hook. You can extend natively supported module type by Rust plugins the same as Farm internal plugins. - resolved_path and module_id:
resolved_path
is the absolute path of the module, andmodule_id
is the unique id of the module, it's usuallyrelative path of the module from the project root
+query
. For example, we import a module asimport './a?query'
, the resolved_path is/project/src/a.ts
and the module_id issrc/a.ts?query
. - context: All the hooks in the plugin accept a
context
argument, it's the compilation context of the farm project, you can use it to get the ModuleGraph, Module, Resources, etc. - Resource and Resource Pot:
Resource
is the final output bundle file, andResource Pot
is the abstract representation of the resource, similar toChunk
of other bundlers. Inside Farm, first we will generateResource Pots
fromModuleGraph
, renderResource Pots
and finally generateResources
fromResource Pots
.
Module Typeโ
In Farm, every thing is First Class Citizens
, so Farm designs module_type
to identify the type of a module and handle different kinds of ModuleTypes in different plugins.
module_type
returned by load
hook, and can be transformed by transform
hook. Farm supports js/ts/jsx/tsx
, css
, html
, json
, static assets(png, svg, etc)
natively. For these module types, you can return them directly in load
or transform
hook directly. But if you want to handle custom module types, you may need to implement ohter hooks like parse
, render_resource_pot_modules
, generate resources
, etc to control how to parse, render and generate resources for the custom module types.
Create Pluginโ
Farm provides official templates to help your create your rust plugins quickly:
- pnpm
- npm
- yarn
pnpm create farm-plugin
npm create farm-plugin@latest
yarn create farm-plugin
then follow the prompts to create your plugin.
or you can create a plugin derectly by running the following command:
- pnpm
- npm
- yarn
pnpm create farm-plugin my-farm-plugin --type rust
npm create my-farm-plugin --type rust
yarn create my-farm-plugin --type rust
Above command will create new rust plugin with name my-farm-plugin
in the current directory. --type
can be rust
or js
Plugin Project Structureโ
The plugin project structure is as follows:
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
Notable files and directories:
src/lib.rs
: The main file of the plugin, where you define your plugin.Cargo.toml
: The manifest file for Rust.package.json
: The manifest file for npm.npm
: Where your platform specific binary packages placed. These packages should be published to npm registry before publish the plugin..github/workflows
: Used to cross build and publish your plugin in github actions.rust-toolchain.toml
: The rust toolchain file, it should not be modified manually, it should always using the same version as the farm core.
Farm provides a tool(@farmfe/plugin-tools
) to help you build and publish your rust plugin, see 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"
},
// ...
}
More detail about building and publishing your plugin, see buidling and publishing sections.
Develop Pluginโ
To develop and test your plugin locally, you should build your plugin for your platform first, run:
pnpm build
Then you can use the built plugin in your farm project by adding the plugin to the plugins
field in farm.config.ts
:
import { defineConfig } from '@farmfe/core';
export default defineConfig({
plugins: [
'my-farm-plugin'
]
});
and execute pnpm i
in your farm project, and run farm start
to start your farm project with your plugin.
when you make changes to your plugin, you should rebuild your plugin and restart your farm project to see the changes. for example, add load
hook to your plugin:
// ... 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)
}
}
Then rebuild your plugin with pnpm build
and restart your farm project with farm start
, you will see the load
hook is called when compiling your farm project.
For more detail about the plugin hooks, see Plugin Hooks.
Handle ModuleTypeโ
module_type
is returned by the load
hook or transform
hook. Your set any module type to the module in the load
hook, and the module will be processed by the corresponding plugin that supports the module type.
For native supported module types, you can just return the module type in the load
hook:
// ... 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)
}
}
}
For non-native supported module types, you should use transform
hook to transform the module type to a native supported module type, otherwise you need to implement parse
, renderResourcePot
hook to handle your custom module type:
// ... 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)
}
}
}
Module type guard like matches!(param.module_type, ModuleType::Custom("sass"))
is required in the transform
hook, cause the transform
hook will be called for all module types, and you should only handle your custom module type in the transform
hook. So do the parse
and other hooks.
or implement parse
, render_resource_pot_modules
hook to handle your custom module type, see how native farm css plugin handle css
module type in farm-plugin-css.
Handle Plugin Optionsโ
The rust plugin options can be configured in farm.config.ts
:
import { defineConfig } from '@farmfe/core';
export default defineConfig({
plugins: [
['my-farm-plugin', {
// plugin options
myOption: 'myOption'
}]
]
});
The Option will be json serialized and passed to the new
method of your plugin, you can handle the options in the new
method:
// ... 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 {}
}
}
Note that you should add dependencies serde
and serde_json
to your Cargo.toml
to support options deserialization:
[dependencies]
# ... ignore other code
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Non json serializable options are not supported. Which means you can only use types like string, number, boolean, array, object, etc. function options
are not supported.
Using farm_core In Pluginโ
Farm exposes all core structures and utilities in farmfe_core
crate. Refer to the farmfe_core documentation for more detail.
If you want to use swc structures like Module
, Program
, etc. in your plugin, you should use farmfe_core::swc_ast
that re-exposed by farm core. Cause the swc version used by farm core may be different from the swc version you used in your plugin, and the swc version used by farm core is guaranteed to be compatible with farm core.
Caveatsโ
Using SWC In Pluginโ
Note that your rust plugin should not use any SWC related packages like swc_common
, swc_transforms
, etc. Cause SWC stores the global state in the process, it may cause dead lock when you use SWC in your plugin.
Farm recommended to write SWC Plugin if you want to make changes to the AST of your farm project. For how to write SWC plugin, see Write SWC Plugin.
Choosing Rust toolchainโ
Cause Farm Rust Plugin is a dynamic linked library, you should always use the same version of the rust toolchain as the farm core. The rust toolchain is defined in rust-toolchain.toml
, it should not be modified manually.
And should should always build your plugin from Rust, cause Farm Core does not support FFI and not promise ABI stability to provide best performance.
Plugin Compatibilityโ
Farm core maintains a API version that exposes to the plugin. If you met a message like Incompatible Rust Plugin: Current core's version...
, it means your plugin is not compatible with the current farm core version. You should update your plugin to the latest version to fix the issue.
For plugin authors, you should rebuild and publish your plugin for the latest farm core version to make your plugin compatible with the latest farm core version.
Farm promises API Compatibility for the same major version, for example, if your plugin is compatible with farm core 1.0.0, it should also be compatible with farm core 1.1.0, 1.2.0, etc. which means your plugin will always work for the same major version of farm.
Cross Buildโ
A Farm Rust Plugin is a platform specific dynamic linked library, you should build your plugin for all platforms you want to support. Farm provided a example for how to build your plugin using github actions, see .github/workflows/build.yml
By default, A farm rust plugin should be built for the following platforms:
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
For a public plugin that published to npm registry, we recommend you to publish your plugin for all platforms above. For a private rust plugin, you can build your plugin for any platform you want to support.
Cause a rust plugin is a pure dynamic linked library, if you have questions about how to build your plugin for a specific platform, just google how to build a dynamic linked library for that platform in Rust.
Publishโ
Steps to publish your Rust plugin:
- Cross build the Rust plugin to dynamic linked library, see Cross Build for detail.
- Copy the binary artifacts to npm dir, for example: Copy to
npm/linux-x64-gnu/index.farm
. - Publish platform specific packages under npm dir, you can use
farm-plugin-tool prepublish
to publish packages undernpm
dir. - Publish the package itself
see example github actions publish workflow
Examplesโ
We will use @farmfe/plugin-sass
as demostration to a real Rust plugin example. This plugin will support compiling .scss
and .sass
file in your farm project.
Define Pluginโ
Exports a Rust struct named FarmPluginSass
.
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(),
}
}
}
- The struct must be
pub
and#[farm_plugin]
attribute is required. - The struct must export a
new
method that accepts 2 arguments for initialization, the first argument is&Config
and the second argument isString
.
Implement Plugin Traitโ
Plugin
trait is used to define hooks
that can hook into Farm compiler.
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
}
}
Load .scss
Fileโ
Implement load
hook to support load .scss
files.
// 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)
}
}
In the load
hook, we only read the file that ends with .scss
or .sass
, return the file content and maked its module_type as ModuleType::Custom(String::from("sass"))
.
Transform sass
Fileโ
After we load the .scss
file, we need to transform it to css
in transform
hook, then Farm will treat it as css in following process.
// 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)
}
This example only covers how to implement a transformer plugin. For more abilities that Farm support, refer to Plugin Hooks.