Plugins #

WebUI provides a framework-agnostic plugin system that extends both the parser (build time) and the handler (render time). Plugins let framework authors customize WebUI's behavior - component discovery, attribute filtering, hydration marker injection - without modifying WebUI internals.

How Plugins Work #

The plugin system operates at two stages:

Build time (Parser Plugin)         Runtime (Handler Plugin)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Skip framework attrs     โ”‚       โ”‚ Inject hydration markers โ”‚
โ”‚ Track components         โ”‚  โ”€โ”€โ”€โ–บ โ”‚ Manage scope counters    โ”‚
โ”‚ Emit opaque Plugin data  โ”‚       โ”‚ Process Plugin data      โ”‚
โ”‚ Inject content at </body>โ”‚       โ”‚ Wrap bindings/repeats    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Parser plugins emit opaque binary data into Plugin protocol fragments. Handler plugins receive that data at render time via on_element_data. WebUI never interprets this data - each plugin pair defines its own contract.

Using Plugins via the CLI #

Pass --plugin <NAME> to webui build or webui serve:

# Build with the @microsoft/fast-element 3.x plugin
webui build ./my-app --out ./dist --plugin=fast-v3

# Dev server with the @microsoft/fast-element 3.x plugin
webui serve ./my-app --state ./data/state.json --plugin=fast-v3

When --plugin=fast-v3 is specified:

  • Build: The FastV3ParserPlugin is loaded during parsing
  • Start: Both FastV3ParserPlugin and FastV3HydrationPlugin are loaded

--plugin=fast-v2 remains available for deprecated @microsoft/fast-element 2.x compatibility. --plugin=fast is a deprecated alias for fast-v2, kept so existing apps do not silently switch marker formats.

Using Plugins with Handlers #

RustNode.jsFFI (C API)
use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
use webui::WebUIHandler;

let handler = WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new()));
handler.handle(&protocol, &state, &options, &mut writer)?;
import { renderStream } from '@microsoft/webui';

renderStream(protocolData, state, (chunk) => res.write(chunk), { plugin: 'fast-v3' });
void *handler = webui_handler_create_with_plugin("fast-v3");
char *html = webui_handler_render(handler, protocol_data, protocol_len, state_json, "index.html", "/");

Built-in FAST Plugins #

fast-v3 provides server-side rendering support for @microsoft/fast-element 3.x components with client-side hydration.

Deprecated compatibility identifiers remain available:

PluginStatusMarker format
fast-v3Recommended for @microsoft/fast-element 3.xCompact @microsoft/fast-element 3.x markers
fast-v2Deprecated compatibilityLegacy @microsoft/fast-element 2.x markers
fastDeprecated alias for fast-v2Legacy @microsoft/fast-element 2.x markers

No plugin is enabled by default. Select fast-v3 explicitly when migrating examples or apps to @microsoft/fast-element 3.x.

Parser Side () #

During webui build --plugin=fast-v3, the parser plugin:

  • Skips framework attributes: @click, f-ref, f-slotted, f-children are removed from the protocol (they're handled client-side)
  • Counts dynamic bindings: Emits binding counts per element as Plugin fragments for the handler
  • Tracks components: Records all custom elements discovered during parsing
  • Injects <f-template> wrappers: At </body>, injects template wrappers for each component with FAST syntax conversion
  • Uses @microsoft/fast-element 3.x runtime APIs: Client code enables hydration with enableHydration() from @microsoft/fast-element/hydration.js and registers declarative templates with declarativeTemplate(), observerMap(), and define()

fast-v3 uses a distinct parser plugin implementation. Deprecated fast-v2 and fast use the separate FastV2ParserPlugin implementation for @microsoft/fast-element 2.x compatibility.

Syntax Conversion #

The plugin converts WebUI template syntax to FAST equivalents inside <f-template> blocks:

WebUI SyntaxFAST Syntax
<if condition="EXPR"><f-when value="{EXPR}">
<for each="item in items"><f-repeat value="{items}">
{{expr}} in :attr values{expr}

Handler Side () #

During rendering with --plugin=fast-v3, the handler plugin injects HTML comment markers that @microsoft/fast-element 3.x client-side hydration uses to locate and re-hydrate dynamic content:

ContextStart MarkerEnd Marker
Signal / If / For<!--fe:b--><!--fe:/b-->
For-loop item<!--fe:r--><!--fe:/r-->

For attribute bindings, data attributes are emitted instead:

TypeAttribute
Dynamic bindingsdata-fe="COUNT"

COUNT is the number of dynamic element bindings. The plugin maintains per-scope binding counters that reset when entering components or loop items.

Deprecated @microsoft/fast-element 2.x Handler () #

During rendering with --plugin=fast-v2 or deprecated --plugin=fast, the handler emits the legacy @microsoft/fast-element 2.x marker format:

ContextStart MarkerEnd Marker
Signal / If / For<!--fe-b$$start$$INDEX$$NAME$$fe-b--><!--fe-b$$end$$INDEX$$NAME$$fe-b-->
For-loop item<!--fe-repeat$$start$$INDEX$$fe-repeat--><!--fe-repeat$$end$$INDEX$$fe-repeat-->

For attribute bindings:

TypeAttribute
Single dynamic bindingdata-fe-b-INDEX
Multiple dynamic bindingsdata-fe-c-INDEX-COUNT

Use this only for existing @microsoft/fast-element 2.x output compatibility. New and migrated FAST apps should use fast-v3.

Built-in Plugin: WebUI Framework #

The webui plugin provides server-side rendering support for WebUI Framework components with automatic hydration.

Parser Side () #

During webui build --plugin=webui, the parser plugin:

  • Skips framework attributes: @click, @keydown, w-ref, and other event/ref bindings are removed from the protocol (handled client-side)
  • Emits binding metadata: 12-byte Plugin fragments encoding [binding_count, event_start, event_count] per element
  • Tracks components: Records custom elements for template metadata generation
  • Compiles templates: Generates optimized metadata as raw JS IIFE strings registered in window.__webui.templates (wrapped in <script> for SSR, evaluated directly for SPA navigation)

Handler Side () #

During rendering with --plugin=webui, the handler injects lightweight comment markers for structural boundaries:

ContextMarkerExample
Repeat block<!--wr--> / <!--/wr-->Wraps the entire <for> loop
Repeat item<!--wi-->Before each loop iteration
Conditional block<!--wc--> / <!--/wc-->Wraps the <if> block content

Text bindings, attribute bindings, and event handlers need no SSR markers - the client resolves them from compiled metadata path indices.

Using the WebUI Plugin #

# Build with WebUI Framework hydration
webui build ./src --out ./dist --plugin=webui

# Dev server with WebUI Framework
webui serve ./src --state ./data/state.json --plugin=webui --watch
// Rust handler
use webui_handler::plugin::webui::WebUIHydrationPlugin;
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));

Writing Custom Plugins #

To create a custom plugin, implement the ParserPlugin and/or HandlerPlugin traits:

ParserPlugin Trait #

pub trait ParserPlugin {
    /// Called before parsing begins for a fragment.
    fn start_fragment(&mut self, fragment_id: &str) {}

    /// Called when a component template has been fully processed.
    fn register_component_template(
        &mut self,
        tag_name: &str,
        component: &Component,
        processed_template: &str,
    ) -> Result<()>;

    /// Decide how a framework-owned attribute should be handled.
    fn classify_attribute(&mut self, attr_name: &str) -> AttributeAction;

    /// Called after all attributes on an element are processed.
    /// Return opaque bytes to emit as a Plugin protocol fragment.
    fn finish_element(&mut self, binding_attribute_count: u32) -> Option<Vec<u8>>;

    /// Consume the plugin and return captured build artifacts.
    fn into_artifacts(self: Box<Self>) -> ParserPluginArtifacts {
        ParserPluginArtifacts::None
    }
}

HandlerPlugin Trait #

pub trait HandlerPlugin {
    /// Enter a new scope (component or loop item).
    fn push_scope(&mut self);
    /// Leave the current scope.
    fn pop_scope(&mut self);

    /// Called before/after a signal binding.
    fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;

    /// Called before/after for-loop and if-condition blocks.
    fn on_for_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_for_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_if_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_if_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;

    /// Called before/after each item in a for-loop.
    fn on_repeat_item_start(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;
    fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;

    /// Process opaque data from a Plugin protocol fragment.
    fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()>;

    /// Write framework-specific route component state attributes.
    fn write_route_component_state(
        &self,
        state: &serde_json::Value,
        writer: &mut dyn ResponseWriter,
    ) -> Result<()>;
}

Next Steps #