Rust Plugin Api
本文档仅涵盖插件钩子详情。 有关如何创建、构建和发布 rust 插件,请参阅:编写 Rust 插件
配置 Rust 插件
通过 plugins
选项添加 Rust 插件:
import { defineConfig } from "@farmfe/core";
export default defineConfig({
// 在 plugins 中配置
plugins: [
['@farmfe/plugin-sass', { /** 插件选项 */ }]
],
});
在字符串中配置 Rust 插件包名称(或路径),并在对象中配置其选项。
编写 Rust 插件
有关详细信息,请参阅编写 Rust 插件。
插件 Hook 概述
Farm 提供了很多 rollup 风格的 hook,这些 hook 分为 build 阶段和 generate 阶段:
所有插件挂钩都接受一个名为 CompilationContext
的参数。 所有共享的编译信息都存储在 context
中。
Hook 共有三种(与 Rollup 相同):
first
:钩子串行执行,当钩子返回非空值时立即返回。 (null 在 JS 中表示 null 和 undefined,在 Rust 中表示 None)。serial
: 钩子串行执行,每个钩子的结果将传递到下一个钩子,使用最后一个钩子的结果作为最终结果。parallel
:钩子在线程池中并行执行,并且应该被 隔离。
有关完整的 Plugin Hooks
定义,请参阅Plugin Trait
name
- required:
true
- default:
fn name(&self) -> &str;
返回此插件的名称。 例子:
impl Plugin for MyPlugin {
fn name(&self) -> &str {
"MyPlugin"
}
}
priority
- required:
false
- default:
fn priority(&self) -> i32 {
100
}
定义该插件的优先级,值越大,该插件越早执行。 当插件具有相同优先级时,它们将按照与plugins
中注册的顺序相同的顺序执行。
默认情况下,所有自定义插件的优先级都是 100。有些内部插件的优先级是 99,比如 plugin-script
、plugin-css
,您可以覆盖默认优先级时内部插件的行为。 但是一些内部插件的优先级是101,比如plugin-resolve
,plugin-html
,如果你想覆盖默认行为,你应该 设置一个更大的优先级。
config
- required:
false
- hook type:
serial
- default:
fn config(&self, _config: &mut Config) -> Result<Option<()>> {
Ok(None)
}
在编译开始之前在config
钩子中修改配置。 Config 结构体的定义请参考Config。 例子:
impl Plugin for MyPlugin {
// 实现 config hook
fn config(&self, config: &mut Config) -> Result<Option<()>> {
// 设置 minify 为 false
config.input.insert("custom-entry", "./custom.html");
Ok(Some(()))
}
}
请注意, Rust Plugin
的 config
钩子是在 JS Plugin
的 config
和 configResolved
钩子之后调用的。
plugin_cache_loaded
- required:
false
- hook type:
serial
- default:
fn plugin_cache_loaded(
&self,
_cache: &Vec<u8>,
_context: &Arc<CompilationContext>,
) -> Result<Option<()>> {
Ok(None)
}
扩展插件的持久缓存加载。
当启用 持久缓存
时,在命中缓存时可能会跳过 加载
和 转换
挂钩。 如果您的插件依赖于以前的编译结果(例如,基于现有模块加载虚拟模块),您可能需要实现此钩子来加载插件的缓存信息,以确保缓存按预期工作。
例子:
#[cache_item]
struct CachedStaticAssets {
list: Vec<Resource>,
}
impl Plugin for StaticAssetsPlugin {
fn plugin_cache_loaded(
&self,
cache: &Vec<u8>,
context: &Arc<CompilationContext>,
) -> farmfe_core::error::Result<Option<()>> {
let cached_static_assets: CachedAssets = deserialize!(cache, CachedStaticAssets);
for asset in cached_static_assets.list {
if let ResourceOrigin::Module(m) = asset.origin {
context.emit_file(EmitFileParams {
resolved_path: m.to_string(),
name: asset.name,
content: asset.bytes,
resource_type: asset.resource_type,
});
}
}
Ok(Some(()))
}
}
注意:
deserialize
由farmfe_core
导出,它可以帮助您反序列化Vec<u8>
中的结构体或枚举。- 缓存的结构体或枚举必须是rkyv可序列化的,您可以使用
farmfe_core
公开的#[cache_item]
快速创建可缓存的结构体。
build_start
- required:
false
- hook type:
parallel
- default:
fn build_start(&self, _context: &Arc<CompilationContext>) -> Result<Option<()>> {
Ok(None)
}
在第一次编译开始之前调用。 您可以使用此挂钩来初始化插件的任何初始状态。
build_start
仅在第一次编译时调用一次。 如果你想在HMR
或Lazy Compilation
中更新ModuleGraph时做一些事情,你应该使用update_modules钩子。
resolve
- required:
false
- hook type:
first
- default:
fn resolve(
&self,
_param: &PluginResolveHookParam,
_context: &Arc<CompilationContext>,
_hook_context: &PluginHookContext,
) -> Result<Option<PluginResolveHookResult>> {
Ok(None)
}
/// 解析钩子的参数
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PluginResolveHookParam {
/// 我们想要解析的源,例如'./index'
pub source: String,
/// 解析 `specifier` 的起始位置,如果解析入口或解析 hmr 更新,则为 [None]。
pub importer: Option<ModuleId>,
/// 例如,[ResolveKind::Import] 用于静态导入 (`import a from './a'`)
pub kind: ResolveKind,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase", default)]
pub struct PluginResolveHookResult {
/// 解析路径,通常是绝对文件路径。
pub resolved_path: String,
/// 该模块是否应该被 external,如果为 true,则该模块不会出现在最终结果中
pub external: bool,
/// 是否具有副作用,影响 tree shake
pub side_effects: bool,
/// 从说明符解析的查询,例如,如果说明符是`./a.png?inline`,查询应该是`{ inline: "" }`
/// 如果你自定义插件,你的插件应该负责解析查询
/// 如果您只想像上面的示例一样进行正常的查询解析, [farmfe_toolkit::resolve::parse_query] 应该会有所帮助
pub query: Vec<(String, String)>,
/// 插件和钩子之间传递的元数据
pub meta: HashMap<String, String>,
}
从 importer
解析自定义 source
,例如从 a.ts
解析 ./b
:
import b from './b?raw';
// ...
那么解析参数将是:
let param = PluginResolveHookParam {
source: "./b",
importer: Some(ModuleId { relative_path: "a.ts", query_string: "" }),
kind: ResolveKind::Import
}
默认解析器的解析结果为:
let resolve_result = PluginResolveHookResult {
resolved_path: "/root/b.ts", // 解析模块的绝对路径
external: false, // 该模块应该包含在最终编译的资源中,并且不应该是外部的
side_effects: false, // 无副作用
query: vec![("raw", "")], // query
meta: HashMap::new()
}
HookContext
用于在您可以递归挂钩时传递状态,例如,您的插件在 resolve hook
中调用 context.plugin_driver.resolve
:
impl Plugin for MyPlugin {
fn resolve(
&self,
param: &farmfe_core::plugin::PluginResolveHookParam,
context: &Arc<CompilationContext>,
hook_context: &PluginHookContext,
) -> farmfe_core::error::Result<Option<farmfe_core::plugin::PluginResolveHookResult>> {
// 添加一个守卫以避免无限循环
if let Some(caller) = &hook_context.caller {
if caller.as_str() == "FarmPluginCss" {
return Ok(None);
}
}
if matches!(param.kind, ResolveKind::CssAtImport | ResolveKind::CssUrl) {
// 如果dep以'~'开头,则表示它来自node_modules。
// 否则它总是相对的
let source = if let Some(striped_source) = param.source.strip_suffix('~') {
striped_source.to_string()
} else if !param.source.starts_with('.') {
format!("./{}", param.source)
} else {
param.source.clone()
};
// 递归调用resolve
return context.plugin_driver.resolve(
&PluginResolveHookParam {
source,
..param.clone()
},
context,
&PluginHookContext {
caller: Some("FarmPluginCss".to_string()),
meta: Default::default(),
},
);
}
Ok(None)
}
}
在上面的示例中,我们调用 context.plugin_driver.resolve
并将 caller
作为参数传递,然后我们应该添加一个类似 if caller.as_str() == "FarmPluginCss"
的保护以避免无限循环。
注意:
- 默认情况下,您的
resolve hook
在Farm内部默认解析器之后执行,只有内部解析器无法解析的源才会传递给您的插件,这意味着如果您想覆盖默认解析器 ,您需要将插件的优先级设置为大于101
。 - 通常
resolved_path
是指向文件的真实绝对路径。 但是您仍然可以返回一个虚拟模块 id
,例如virtual:my-module
,但是对于虚拟模块,您需要实现load
钩子来自定义如何加载虚拟模块。 在 Farm 中,resolved_path + query = module_id
。 ResolveKind
表示导入类型
,示例值:ResolveKind::Require
(由 commonjs require 导入)、ResolveKind::CssImport
(由 css 的 import 语句导入)等。meta
可以在插件和钩子之间共享,您可以从任何插件中的load
、transform
和parse
钩子的参数中获取meta
。
load
- required:
false
- hook type:
first
- default:
fn load(
&self,
_param: &PluginLoadHookParam,
_context: &Arc<CompilationContext>,
_hook_context: &PluginHookContext,
) -> Result<Option<PluginLoadHookResult>> {
Ok(None)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginLoadHookParam<'a> {
/// 模块id字符串
pub module_id: String,
/// 来自解析钩子的解析路径
pub resolved_path: &'a str,
pub query: Vec<(String, String)>,
/// 插件和钩子之间传递的元数据
pub meta: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginLoadHookResult {
/// 模块的源内容
pub content: String,
/// 模块的类型,例如[ModuleType::Js]代表普通的javascript文件,
/// 通常以 `.js` 扩展名结尾
pub module_type: ModuleType,
pub source_map: Option<String>,
}