microsoft-webui 0.0.24

Programmatic API for the WebUI build-time rendering framework
Documentation
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

//! Static component asset graph rendering for CDN-loadable ESM modules.

mod graph;
mod json;
mod metafile;
mod payload;
mod render;
mod serialize;
mod traversal;

use std::collections::HashSet;
use webui_protocol::WebUIProtocol;

use crate::{AssetFileNameTemplate, WebUIError};

/// A rendered static component asset root or shared chunk.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentAssetFile {
    /// Output filename for the ESM asset.
    pub name: String,
    /// JavaScript module content.
    pub content: String,
}

/// Rendered component asset graph.
#[derive(Debug)]
pub struct ComponentAssetGraph {
    /// Root and shared chunk ESM files.
    pub files: Vec<ComponentAssetFile>,
    /// Optional esbuild-compatible metafile JSON.
    pub metafile: Option<String>,
    entry_fragments: Vec<String>,
    entry_components: Vec<String>,
}

impl ComponentAssetGraph {
    pub(crate) fn retain_entry_protocol(&self, protocol: &mut WebUIProtocol) {
        if self.files.is_empty() {
            return;
        }
        protocol
            .fragments
            .retain(|name, _| self.entry_fragments.binary_search(name).is_ok());
        protocol
            .components
            .retain(|name, _| self.entry_components.binary_search(name).is_ok());
    }
}

/// Render a static component asset graph.
///
/// Entry-reachable components remain external prerequisites. Components used
/// by one requested root stay inline, while components with an identical
/// multi-root consumer set are emitted once in a shared chunk.
///
/// # Errors
///
/// Returns [`WebUIError`] when the entry/root graph is invalid, contains a
/// route, lacks compiled template metadata, or produces colliding filenames.
#[must_use = "component asset graph files must be written or otherwise consumed"]
pub fn render_component_assets(
    protocol: &WebUIProtocol,
    entry: &str,
    roots: &[String],
    file_name_template: &str,
    emit_metafile: bool,
) -> Result<ComponentAssetGraph, WebUIError> {
    if roots.is_empty() {
        if emit_metafile {
            return Err(WebUIError::InvalidBuildOptions(
                "metafile requires at least one component_asset_root".to_string(),
            ));
        }
        return Ok(ComponentAssetGraph {
            files: Vec::new(),
            metafile: None,
            entry_fragments: Vec::new(),
            entry_components: Vec::new(),
        });
    }

    let file_name_template =
        AssetFileNameTemplate::try_new(file_name_template.to_string(), "asset_file_name_template")
            .map_err(|error| WebUIError::InvalidBuildOptions(error.to_string()))?;
    let plan = graph::plan_component_assets(protocol, entry, roots)?;
    let rendered =
        render::render_component_asset_graph(protocol, &plan, &file_name_template, emit_metafile)?;
    validate_unique_asset_file_names(&rendered.files)?;
    let metafile = if emit_metafile {
        Some(metafile::render_metafile(protocol, &rendered.outputs)?)
    } else {
        None
    };

    Ok(ComponentAssetGraph {
        files: rendered.files,
        metafile,
        entry_fragments: plan.entry_fragments,
        entry_components: plan.entry_components,
    })
}

fn validate_unique_asset_file_names(files: &[ComponentAssetFile]) -> Result<(), WebUIError> {
    let mut names = HashSet::with_capacity(files.len());
    for file in files {
        if !names.insert(file.name.as_str()) {
            return Err(WebUIError::InvalidBuildOptions(format!(
                "component asset filename collision for '{}'. Adjust --asset-file-name-template to include [name] or another unique component-specific segment.",
                file.name
            )));
        }
    }
    Ok(())
}