Skip to main content

webui_handler/plugin/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Handler plugin trait and built-in plugin implementations.
5//!
6//! Handler plugins write framework-specific hydration markers while shared
7//! completion work, such as component template emission, stays in handler core.
8
9pub mod fast;
10pub mod fast_v2;
11pub mod fast_v3;
12pub mod webui;
13
14use crate::{ResponseWriter, Result};
15use std::collections::HashSet;
16use webui_protocol::WebUIProtocol;
17
18/// Split WebUI component template payload used by SSR bootstrap emission.
19pub struct WebUiTemplatePayload<'a> {
20    /// Component custom-element tag name.
21    pub tag_name: &'a str,
22    /// JSON-safe template metadata object.
23    pub template_json: &'a str,
24    /// Component-local JavaScript condition closure array.
25    pub template_functions: &'a str,
26}
27
28/// Context passed to plugin-specific SSR bootstrap extension hooks.
29pub struct BootstrapExtensionContext<'a> {
30    /// Full protocol for plugins that need additional component metadata.
31    pub protocol: &'a WebUIProtocol,
32    /// Route-reachable component tags for this render.
33    pub components: &'a HashSet<String>,
34    /// Split WebUI template payloads collected for this render.
35    pub payloads: &'a [WebUiTemplatePayload<'a>],
36    /// CSP nonce for executable scripts, when configured.
37    pub nonce: Option<&'a str>,
38}
39
40/// A handler plugin that can inject additional content during rendering.
41///
42/// Plugins receive callbacks at key points in the rendering lifecycle:
43/// - **Scope management**: `push_scope` / `pop_scope` for component and loop boundaries
44/// - **Binding lifecycle**: `on_binding_start` / `on_binding_end` around signals
45/// - **For-loop lifecycle**: `on_for_start` / `on_for_end` around for-loop blocks
46/// - **If-condition lifecycle**: `on_if_start` / `on_if_end` around if-condition blocks
47/// - **Repeat items**: `on_repeat_item_start` / `on_repeat_item_end` per for-loop item
48/// - **Element data**: `on_element_data` for parser-produced hydration metadata
49/// - **Route state**: `write_route_component_state` for framework-specific opening-tag attributes
50///
51/// WebUI does not interpret what plugins write — it just calls the hooks.
52/// Each framework defines its own marker format.
53pub trait HandlerPlugin {
54    /// Enter a new scope (component or for-loop item boundary).
55    /// Typically resets per-scope counters.
56    fn push_scope(&mut self);
57
58    /// Exit the current scope, restoring the parent scope state.
59    fn pop_scope(&mut self);
60
61    /// Called before rendering a signal binding.
62    fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
63
64    /// Called after rendering a signal binding.
65    fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
66
67    /// Called before rendering a for-loop block.
68    /// Defaults to [`on_binding_start`](HandlerPlugin::on_binding_start).
69    fn on_for_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
70        self.on_binding_start(name, writer)
71    }
72
73    /// Called after rendering a for-loop block.
74    /// Defaults to [`on_binding_end`](HandlerPlugin::on_binding_end).
75    fn on_for_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
76        self.on_binding_end(name, writer)
77    }
78
79    /// Called before rendering an if-condition block.
80    /// Defaults to [`on_binding_start`](HandlerPlugin::on_binding_start).
81    fn on_if_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
82        self.on_binding_start(name, writer)
83    }
84
85    /// Called after rendering an if-condition block.
86    /// Defaults to [`on_binding_end`](HandlerPlugin::on_binding_end).
87    fn on_if_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
88        self.on_binding_end(name, writer)
89    }
90
91    /// Called before rendering a repeat item in a for loop.
92    fn on_repeat_item_start(&mut self, index: usize, writer: &mut dyn ResponseWriter)
93        -> Result<()>;
94
95    /// Called after rendering a repeat item.
96    fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;
97
98    /// Called when parser-produced element metadata is encountered.
99    fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()>;
100
101    /// Called when emitting a matched route component's opening tag.
102    /// Plugins can write framework-specific attributes before the closing `>`.
103    /// The default is a no-op.
104    fn write_route_component_state(
105        &self,
106        _state: &serde_json::Value,
107        _writer: &mut dyn ResponseWriter,
108    ) -> Result<()> {
109        Ok(())
110    }
111
112    /// Emit component templates collected during SSR.  The default emits
113    /// each template as-is (suitable for FAST `<f-template>` tags).  The
114    /// WebUI split-payload path uses [`HandlerPlugin::collect_template_payloads`]
115    /// instead.
116    fn emit_templates(
117        &self,
118        protocol: &WebUIProtocol,
119        components: &HashSet<String>,
120        _nonce: Option<&str>,
121        writer: &mut dyn ResponseWriter,
122    ) -> Result<()> {
123        emit_component_templates(protocol, components, writer)
124    }
125
126    /// Return split WebUI template payloads for the given components.
127    ///
128    /// The WebUI plugin overrides this so `lib.rs` can emit JSON metadata in an
129    /// inert data block and only emit condition closures as executable JS.
130    /// Returns `None` when templates are non-WebUI payloads (e.g. FAST
131    /// `<f-template>` tags).
132    fn collect_template_payloads<'a>(
133        &self,
134        _protocol: &'a WebUIProtocol,
135        _components: &HashSet<String>,
136    ) -> Option<Vec<WebUiTemplatePayload<'a>>> {
137        None
138    }
139
140    /// Slice-based counterpart to [`HandlerPlugin::emit_templates`].
141    ///
142    /// The streaming checkpoint path captures the exact component tags rendered
143    /// since the previous checkpoint as a borrowed `&[&str]`, avoiding the owned
144    /// `HashSet<String>` the ordinary body-end path builds. The default forwards
145    /// to [`emit_component_templates_slice`] (verbatim FAST `<f-template>`
146    /// emission).
147    fn emit_templates_slice(
148        &self,
149        protocol: &WebUIProtocol,
150        tags: &[&str],
151        _nonce: Option<&str>,
152        writer: &mut dyn ResponseWriter,
153    ) -> Result<()> {
154        emit_component_templates_slice(protocol, tags, writer)
155    }
156
157    /// Slice-based counterpart to [`HandlerPlugin::collect_template_payloads`].
158    ///
159    /// Consumes a borrowed `&[&str]` of component tags so the streaming path can
160    /// project per-checkpoint templates without materializing an owned set. The
161    /// default returns `None`.
162    fn collect_template_payloads_slice<'a>(
163        &self,
164        _protocol: &'a WebUIProtocol,
165        _tags: &[&str],
166    ) -> Option<Vec<WebUiTemplatePayload<'a>>> {
167        None
168    }
169
170    /// Emit plugin-specific executable SSR bootstrap code for the streaming
171    /// path, given only the already-collected template payloads.
172    ///
173    /// Unlike [`HandlerPlugin::emit_bootstrap_extension`], this takes no
174    /// `HashSet<String>` component set — the streaming checkpoint has already
175    /// projected the exact per-checkpoint payloads. The default is a no-op.
176    fn emit_bootstrap_extension_payloads(
177        &self,
178        _payloads: &[WebUiTemplatePayload<'_>],
179        _nonce: Option<&str>,
180        _writer: &mut dyn ResponseWriter,
181    ) -> Result<()> {
182        Ok(())
183    }
184
185    /// Emit plugin-specific executable SSR bootstrap code, if needed.
186    ///
187    /// The handler emits shared metadata as inert `#webui-data`; client
188    /// packages parse that data lazily. Plugins can still emit executable
189    /// side-channel data here, such as WebUI framework `templateFns` closures.
190    /// The default is a no-op for FAST plugins, which use `<f-template>` tags.
191    fn emit_bootstrap_extension(
192        &self,
193        _context: BootstrapExtensionContext<'_>,
194        _writer: &mut dyn ResponseWriter,
195    ) -> Result<()> {
196        Ok(())
197    }
198}
199
200/// Default template emission: write each non-empty template verbatim.
201/// Used by FAST parser plugins for `<f-template>` tags.
202pub(crate) fn emit_component_templates(
203    protocol: &WebUIProtocol,
204    components: &HashSet<String>,
205    writer: &mut dyn ResponseWriter,
206) -> Result<()> {
207    for name in components {
208        if let Some(template) = protocol
209            .components
210            .get(name)
211            .map(|component| component.template.as_str())
212            .filter(|template| !template.is_empty())
213        {
214            writer.write(template)?;
215        }
216    }
217    Ok(())
218}
219
220/// Slice-based counterpart to [`emit_component_templates`].
221///
222/// Writes each non-empty template verbatim for the borrowed component tags,
223/// used by the streaming checkpoint fallback path.
224pub(crate) fn emit_component_templates_slice(
225    protocol: &WebUIProtocol,
226    tags: &[&str],
227    writer: &mut dyn ResponseWriter,
228) -> Result<()> {
229    for &name in tags {
230        if let Some(template) = protocol
231            .components
232            .get(name)
233            .map(|component| component.template.as_str())
234            .filter(|template| !template.is_empty())
235        {
236            writer.write(template)?;
237        }
238    }
239    Ok(())
240}