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 /// Emit plugin-specific executable SSR bootstrap code, if needed.
141 ///
142 /// The handler emits shared metadata as inert `#webui-data`; client
143 /// packages parse that data lazily. Plugins can still emit executable
144 /// side-channel data here, such as WebUI framework `templateFns` closures.
145 /// The default is a no-op for FAST plugins, which use `<f-template>` tags.
146 fn emit_bootstrap_extension(
147 &self,
148 _context: BootstrapExtensionContext<'_>,
149 _writer: &mut dyn ResponseWriter,
150 ) -> Result<()> {
151 Ok(())
152 }
153}
154
155/// Default template emission: write each non-empty template verbatim.
156/// Used by FAST parser plugins for `<f-template>` tags.
157pub(crate) fn emit_component_templates(
158 protocol: &WebUIProtocol,
159 components: &HashSet<String>,
160 writer: &mut dyn ResponseWriter,
161) -> Result<()> {
162 for name in components {
163 if let Some(template) = protocol
164 .components
165 .get(name)
166 .map(|component| component.template.as_str())
167 .filter(|template| !template.is_empty())
168 {
169 writer.write(template)?;
170 }
171 }
172 Ok(())
173}