API du plugin
Les résolveurs intégrés gèrent les sources /__bext/*, les partiels du système de fichiers, les rendus V8 et les récupérations de proxy. Pour les sources spécifiques à un domaine (contenu CMS, lignes de base de données, API tierces), vous écrivez un résolveur personnalisé. L'API du plugin rend cela déclaratif via la correspondance de modèles et les hooks de cycle de vie.
use bext_core::include::{IncludeResolver, IncludeDirective, IncludeContext, PluginResolver};
let cms_resolver = PluginResolver::new("/cms/*", |directive, ctx| {
let slug = directive.src.strip_prefix("/cms/")?;
let locale = ctx.vary_value("locale").unwrap_or("en");
cms_client::fetch_block(slug, locale).ok()
});
chain_builder.with_plugin(Arc::new(cms_resolver));
Désormais, <bext-include src="/cms/homepage-hero"> est acheminé vers votre résolveur CMS. Mise en cache, authentification, variation – tout fonctionne car le résolveur se trouve dans le même pipeline.
Enregistrement du résolveur#
PluginResolver::new(
pattern, // glob string: "/cms/*", "/products/*", "/api/feed/**"
handler, // Fn(&IncludeDirective, &IncludeContext) -> Option<String>
)
Le gestionnaire est appelé pour toute inclusion dont src correspond au modèle. Renvoie Some(html) en cas de succès, None à transmettre au résolveur suivant.
Spécificité du modèle#
Les résolveurs de plugins sont classés par spécificité de modèle (la plupart des caractères littéraux en premier). Les modèles plus spécifiques s'exécutent avant les modèles moins spécifiques :
/cms/posts/* ← 10 literal chars, runs first
/cms/* ← 4 literal chars
/* ← 0 literal chars, runs last
Cela vous permet de superposer des gestionnaires spécialisés au-dessus d'un fourre-tout.
Syntaxe du modèle#
Même syntaxe globale que les règles de configuration :
*correspond à tous les caractères sauf/.**correspond à tous les séparateurs de chemin.- Les caractères littéraux correspondent littéralement.
| Modèle | Matchs |
|---|---|
/cms/* |
/cms/post-1, /cms/hero |
/cms/** |
/cms/post-1, /cms/posts/2025/jan |
/api/v*/users |
/api/v1/users, /api/v2/users |
Les résolveurs de plugins se situent entre les résolveurs d'authentification/expérience et les résolveurs de contenu intégrés dans la chaîne. Cela signifie que l'authentification et la réécriture des sources des expériences se produisent automatiquement avant que votre résolveur ne soit appelé - vous n'avez pas besoin de les réimplémenter.
Crochets de cycle de vie#
Les plugins peuvent observer et transformer la résolution en quatre points :
pub trait IncludeHook: Send + Sync {
fn on_resolve(&self, directive: &mut IncludeDirective, ctx: &IncludeContext);
fn on_resolved(&self, directive: &IncludeDirective, html: &mut String, ctx: &IncludeContext);
fn on_cache_hit(&self, directive: &IncludeDirective, ctx: &IncludeContext);
fn on_error(&self, directive: &IncludeDirective, error: &IncludeError, ctx: &IncludeContext);
}
| Crochet | Quand | Mutable? |
|---|---|---|
on_resolve |
Avant la résolution | directif |
on_resolved |
Après résolution, avant cache | HTML |
on_cache_hit |
En cas d'accès au cache (ignore la résolution) | en lecture seule |
on_error |
En cas d'échec de résolution | en lecture seule |
Les crochets se fixent sur un PluginResolver spécifique :
let resolver = PluginResolver::new("/cms/*", handler)
.with_hook(Arc::new(AnalyticsHook))
.with_hook(Arc::new(MinifierHook));
Ou attaché dynamiquement :
resolver.add_hook(Arc::new(DebugHook));
Modèles de crochet#
Analyse/observabilité :
struct AnalyticsHook {
metrics: Arc<MetricsClient>,
}
impl IncludeHook for AnalyticsHook {
fn on_resolved(&self, d: &IncludeDirective, _html: &mut String, _ctx: &IncludeContext) {
self.metrics.increment("include.resolved", &[("src", d.src.as_str())]);
}
fn on_error(&self, d: &IncludeDirective, err: &IncludeError, _ctx: &IncludeContext) {
self.metrics.increment("include.error", &[
("src", d.src.as_str()),
("kind", err.kind()),
]);
}
}
Transformation de sortie :
struct MinifierHook;
impl IncludeHook for MinifierHook {
fn on_resolved(&self, _d: &IncludeDirective, html: &mut String, _ctx: &IncludeContext) {
*html = html_minifier::minify(html);
}
}
Validation /application CSP :
struct CspHook {
allowed_patterns: Vec<String>,
}
impl IncludeHook for CspHook {
fn on_resolve(&self, d: &mut IncludeDirective, _ctx: &IncludeContext) {
let permitted = self.allowed_patterns.iter().any(|p| glob_match(p, &d.src));
if !permitted {
tracing::warn!(src = %d.src, "include blocked by CSP policy");
d.src = "/_partials/csp-blocked.html".into();
}
}
}
Injection d'attributs :
struct AnalyticsAttributeHook;
impl IncludeHook for AnalyticsAttributeHook {
fn on_resolved(&self, d: &IncludeDirective, html: &mut String, _ctx: &IncludeContext) {
// Add data-analytics-src to the root element.
if let Some(pos) = html.find('>') {
html.insert_str(pos, &format!(" data-analytics-src=\"{}\"", d.src));
}
}
}
Types d'erreurs#
Le hook on_error reçoit une erreur structurée :
pub enum IncludeError {
Unresolved, // no resolver produced a result
Forbidden, // blocked by security policy
Timeout, // resolution took too long
CircuitOpen, // circuit breaker skipped resolution
Custom(String), // resolver-specific error
}
Les erreurs sont uniquement destinées à l'observabilité : le pipeline gère automatiquement les chaînes de secours en fonction des attributs d'inclusion.
Intégration de la chaîne#
Enregistrez les plugins via le générateur de chaîne :
use bext_core::include::{ChainBuilder, PluginResolver};
let chain = ChainBuilder::new()
.with_cache(cache)
.with_auth(true)
.with_plugin(Arc::new(cms_resolver))
.with_plugin(Arc::new(products_resolver))
.with_plugin(Arc::new(legacy_api_resolver))
.with_file(file_resolver)
.with_fallback(true)
.build();
Les résolveurs de plug-in se situent entre les résolveurs d'authentification/expérience et les résolveurs de contenu intégrés. Cela signifie que les plugins bénéficient automatiquement de l’authentification et de la réécriture des expériences, sans avoir à les réimplémenter.
Systèmes de plugins externes#
L'API du plugin fonctionne avec les environnements d'exécution des plugins WASM et V8 de bext. Un plugin WASM peut enregistrer des résolveurs d'inclusion via le manifeste du plugin :
# plugin.toml
[plugin]
name = "cms-includes"
version = "1.0.0"
[include]
resolvers = [
{ pattern = "/cms/*", handler = "resolve_cms" },
{ pattern = "/products/*", handler = "resolve_product" },
]
hooks = ["on_resolved"]
[include.config]
cms_api_url = { type = "string", required = true }
cms_api_key = { type = "string", required = true, secret = true }
[plugin.permissions]
network = ["api.contentful.com"] # allowed outbound hosts
filesystem = []
cache = true
realtime = false
Le module WASM du plugin exporte les fonctions resolve_cms et resolve_product. bext charge le plug-in au démarrage, enregistre les résolveurs avec leurs modèles et achemine les inclusions correspondant au runtime WASM.
Voir Construire un plugin pour le guide complet de création du plugin.
Les plugins WASM et V8 s'exécutent sous les autorisations sandbox déclarées. Un plugin qui tente d'extraire un hôte ne figurant pas dans sa liste blanche network obtient une erreur d'autorisation au moment de l'exécution - et non une non-opération silencieuse. Déclarez tous les hôtes requis dans [plugin.permissions] avant le déploiement.
Autorisations du bac à sable#
Les résolveurs de plugins s'exécutent avec les autorisations déclarées :
[plugin.permissions]
network = ["api.cms.example.com", "api.products.example.com"]
filesystem = [] # no FS access
cache = true # can use bext.cache
realtime = false # cannot publish to channels
secrets = ["cms_key"] # can read specific secrets
Le bac à sable les applique au niveau d'exécution WASM/V8 : un plugin qui tente d'extraire un hôte non autorisé obtient une erreur d'autorisation, pas un échec silencieux.
Inscription dynamique#
Les plugins peuvent enregistrer des résolveurs au moment de l'exécution (pas seulement au démarrage) :
#[bext_plugin::on_server_start]
fn init(ctx: &PluginContext) {
let resolver = PluginResolver::new("/cms/*", handle_cms);
ctx.register_include_resolver(Arc::new(resolver));
}
#[bext_plugin::on_reload]
fn on_config_reload(ctx: &PluginContext) {
// Unregister old, register new — for hot-reloadable patterns.
}
C'est ainsi que vous créez des éléments tels que "Les éditeurs CMS peuvent ajouter de nouveaux modèles d'inclusion via une interface utilisateur d'administration sans déploiement".
Test des résolveurs de plugins#
Les résolveurs de plugins ne sont que des implémentations de IncludeResolver. Testez-les directement :
#[test]
fn test_cms_resolver() {
let r = PluginResolver::new("/cms/*", |d, _ctx| {
Some(format!("[cms:{}]", d.src.strip_prefix("/cms/").unwrap()))
});
let d = IncludeDirective {
src: "/cms/hero".into(),
attrs: Default::default(),
fallback_content: None,
};
let ctx = IncludeContext::new();
assert_eq!(r.resolve(&d, &ctx).as_deref(), Some("[cms:hero]"));
}
Pour les tests d'intégration, construisez une petite chaîne avec votre résolveur et exercez le pipeline complet :
let chain = ChainBuilder::new()
.with_auth(false)
.with_plugin(my_resolver)
.with_fallback(true)
.build();
let out = resolve_all(html, &chain, &IncludeContext::new());
Exemples pratiques#
Contentful / Sanity / Strapi inclut un résolveur#
let cms = PluginResolver::new("/cms/*", move |d, ctx| {
let slug = d.src.strip_prefix("/cms/")?;
let locale = ctx.vary_value("locale").unwrap_or("en");
let client = CMSClient::get();
client.render_block(slug, locale).ok()
});
Fiche produit de la base de données#
let products = PluginResolver::new("/products/*", move |d, _ctx| {
let sku = d.src.strip_prefix("/products/")?;
let product = db::fetch_product(sku).ok()?;
Some(render_product_card(&product))
});
API tierce avec mise en forme des réponses#
let stripe_widget = PluginResolver::new("/stripe/pricing*", move |_d, ctx| {
let user_id = ctx.session.as_ref()?.user_id.as_ref()?;
let customer = stripe::get_customer(user_id).ok()?;
Some(render_pricing_for(&customer))
});
Routage de l'API héritée#
let legacy = PluginResolver::new("/legacy-api/*", |d, _ctx| {
let path = d.src.strip_prefix("/legacy-api/")?;
let url = format!("https://old-api.internal.example.com/{}", path);
http::get(&url).ok().and_then(|r| r.text().ok())
});
Voir aussi#
- Aperçu — système d'inclusion de niveau supérieur
- Construire un plugin — guide complet des plugins
- Système de plugins — Architecture du plug-in de bext
- Capacités — interfaces partagées pour les plugins d'échange