microsoft-webui 0.0.25

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

use super::graph::AssetGraphPlan;
use super::json::{push_json_string, push_u64};
use super::payload::RenderedComponent;
use super::ComponentAssetFile;
use crate::{AssetFileNameTemplate, WebUIError};

const ASSET_TYPE: &str = "webui-component-asset";
const ASSET_VERSION: u64 = 2;
const COMPONENT_ASSET_EXT: &str = "webui.js";

pub(super) struct RenderedOutput {
    pub name: String,
    pub bytes: usize,
    pub root: Option<String>,
    pub components: Vec<(String, usize)>,
    pub required_components: Vec<String>,
    pub external_components: Vec<String>,
    pub dynamic_components: Vec<String>,
    pub imports: Vec<String>,
}

pub(super) struct PendingAsset {
    pub logical_name: String,
    pub root: Option<String>,
    pub components: Vec<usize>,
    pub required_components: Vec<usize>,
    pub external_components: Vec<usize>,
    pub imports: Vec<ResolvedImport>,
}

pub(super) struct ResolvedImport {
    pub file_name: String,
    pub components: Vec<usize>,
}

pub(super) struct RenderedAsset {
    pub file: ComponentAssetFile,
    pub output: Option<RenderedOutput>,
}

pub(super) fn render_asset(
    pending: &PendingAsset,
    plan: &AssetGraphPlan,
    payloads: &[Option<RenderedComponent<'_>>],
    file_name_template: &AssetFileNameTemplate,
    emit_metafile: bool,
) -> Result<RenderedAsset, WebUIError> {
    let mut js = String::with_capacity(estimate_asset_size(pending, plan, payloads));
    let mut attribution = if emit_metafile {
        vec![0usize; pending.components.len()]
    } else {
        Vec::new()
    };
    js.push_str("const asset={\"type\":\"");
    js.push_str(ASSET_TYPE);
    js.push_str("\",\"version\":");
    push_u64(&mut js, ASSET_VERSION);
    js.push_str(",\"kind\":\"");
    js.push_str(if pending.root.is_some() {
        "root"
    } else {
        "chunk"
    });
    js.push('"');
    if let Some(root) = &pending.root {
        js.push_str(",\"root\":");
        push_json_string(&mut js, root, "component asset root")?;
    }
    js.push_str(",\"components\":[");
    push_component_tags(&mut js, &pending.components, plan, &mut attribution)?;
    js.push_str("],\"requiredComponents\":[");
    push_component_id_array(&mut js, &pending.required_components, plan)?;
    js.push_str("],\"externalComponents\":[");
    push_component_id_array(&mut js, &pending.external_components, plan)?;
    js.push_str("],\"imports\":[");
    push_imports(&mut js, &pending.imports, plan)?;
    js.push_str("],\"templateStyles\":[");
    push_styles(&mut js, &pending.components, payloads, &mut attribution)?;
    js.push_str("],\"templates\":{");
    push_templates(
        &mut js,
        &pending.components,
        plan,
        payloads,
        &mut attribution,
    )?;
    js.push('}');
    if pending.components.iter().any(|id| {
        payloads
            .get(*id)
            .and_then(Option::as_ref)
            .is_some_and(|payload| payload.functions.is_some())
    }) {
        js.push_str(",\"templateFunctions\":{");
        push_functions(
            &mut js,
            &pending.components,
            plan,
            payloads,
            &mut attribution,
        )?;
        js.push('}');
    }
    js.push_str("};\nexport default asset;\n");

    let name =
        file_name_template.resolve(&pending.logical_name, COMPONENT_ASSET_EXT, js.as_bytes());
    let output = emit_metafile.then(|| {
        let mut dynamic_components: Vec<String> = pending
            .imports
            .iter()
            .flat_map(|import| import.components.iter())
            .map(|component| plan.component_names[*component].to_string())
            .collect();
        dynamic_components.sort_unstable();
        dynamic_components.dedup();
        RenderedOutput {
            bytes: js.len(),
            root: pending.root.clone(),
            components: pending
                .components
                .iter()
                .enumerate()
                .map(|(index, id)| (plan.component_names[*id].to_string(), attribution[index]))
                .collect(),
            required_components: component_names(&pending.required_components, plan),
            external_components: component_names(&pending.external_components, plan),
            dynamic_components,
            imports: pending
                .imports
                .iter()
                .map(|import| import.file_name.clone())
                .collect(),
            name: name.clone(),
        }
    });
    Ok(RenderedAsset {
        file: ComponentAssetFile { name, content: js },
        output,
    })
}

fn estimate_asset_size(
    pending: &PendingAsset,
    plan: &AssetGraphPlan,
    payloads: &[Option<RenderedComponent<'_>>],
) -> usize {
    let mut size = 256 + pending.logical_name.len();
    for component in &pending.components {
        size += plan.component_names[*component].len() * 3 + 16;
        if let Some(payload) = payloads.get(*component).and_then(Option::as_ref) {
            size += payload.template.len();
            size += payload.style.as_ref().map_or(0, String::len);
            size += payload.functions.map_or(0, str::len);
        }
    }
    for import in &pending.imports {
        size += import.file_name.len() * 2 + 96;
    }
    size
}

fn push_component_tags(
    out: &mut String,
    components: &[usize],
    plan: &AssetGraphPlan,
    attribution: &mut [usize],
) -> Result<(), WebUIError> {
    for (index, component) in components.iter().copied().enumerate() {
        let start = out.len();
        if index > 0 {
            out.push(',');
        }
        push_json_string(out, plan.component_names[component], "component tag")?;
        add_attribution(attribution, index, out.len() - start);
    }
    Ok(())
}

fn push_component_id_array(
    out: &mut String,
    components: &[usize],
    plan: &AssetGraphPlan,
) -> Result<(), WebUIError> {
    for (index, component) in components.iter().copied().enumerate() {
        if index > 0 {
            out.push(',');
        }
        push_json_string(out, plan.component_names[component], "component tag")?;
    }
    Ok(())
}

fn push_imports(
    out: &mut String,
    imports: &[ResolvedImport],
    plan: &AssetGraphPlan,
) -> Result<(), WebUIError> {
    for (index, import) in imports.iter().enumerate() {
        if index > 0 {
            out.push(',');
        }
        out.push_str("{\"components\":[");
        push_component_id_array(out, &import.components, plan)?;
        out.push_str("],\"href\":new URL(");
        let mut relative = String::with_capacity(import.file_name.len() + 2);
        relative.push_str("./");
        relative.push_str(&import.file_name);
        push_json_string(out, &relative, "component asset import")?;
        out.push_str(",import.meta.url).href,\"load\":()=>import(");
        push_json_string(out, &relative, "component asset import")?;
        out.push_str(")}");
    }
    Ok(())
}

fn push_styles(
    out: &mut String,
    components: &[usize],
    payloads: &[Option<RenderedComponent<'_>>],
    attribution: &mut [usize],
) -> Result<(), WebUIError> {
    let mut written = 0usize;
    for (index, component) in components.iter().copied().enumerate() {
        let payload = payload(payloads, component)?;
        let Some(style) = &payload.style else {
            continue;
        };
        let start = out.len();
        if written > 0 {
            out.push(',');
        }
        out.push_str(style);
        add_attribution(attribution, index, out.len() - start);
        written += 1;
    }
    Ok(())
}

fn push_templates(
    out: &mut String,
    components: &[usize],
    plan: &AssetGraphPlan,
    payloads: &[Option<RenderedComponent<'_>>],
    attribution: &mut [usize],
) -> Result<(), WebUIError> {
    for (index, component) in components.iter().copied().enumerate() {
        let payload = payload(payloads, component)?;
        let start = out.len();
        if index > 0 {
            out.push(',');
        }
        push_json_string(out, plan.component_names[component], "component tag")?;
        out.push(':');
        out.push_str(&payload.template);
        add_attribution(attribution, index, out.len() - start);
    }
    Ok(())
}

fn push_functions(
    out: &mut String,
    components: &[usize],
    plan: &AssetGraphPlan,
    payloads: &[Option<RenderedComponent<'_>>],
    attribution: &mut [usize],
) -> Result<(), WebUIError> {
    let mut written = 0usize;
    for (index, component) in components.iter().copied().enumerate() {
        let payload = payload(payloads, component)?;
        let Some(functions) = &payload.functions else {
            continue;
        };
        let start = out.len();
        if written > 0 {
            out.push(',');
        }
        push_json_string(out, plan.component_names[component], "component tag")?;
        out.push(':');
        out.push_str(functions);
        add_attribution(attribution, index, out.len() - start);
        written += 1;
    }
    Ok(())
}

fn payload<'p, 'd>(
    payloads: &'p [Option<RenderedComponent<'d>>],
    component: usize,
) -> Result<&'p RenderedComponent<'d>, WebUIError> {
    payloads
        .get(component)
        .and_then(Option::as_ref)
        .ok_or_else(|| {
            WebUIError::InvalidBuildOptions(
                "component asset graph references a missing rendered payload".to_string(),
            )
        })
}

#[inline]
fn add_attribution(attribution: &mut [usize], index: usize, bytes: usize) {
    if let Some(value) = attribution.get_mut(index) {
        *value += bytes;
    }
}

fn component_names(components: &[usize], plan: &AssetGraphPlan) -> Vec<String> {
    components
        .iter()
        .map(|component| plan.component_names[*component].to_string())
        .collect()
}