Skip to main content

webui/
component_assets.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Static component asset graph rendering for CDN-loadable ESM modules.
5
6mod graph;
7mod json;
8mod metafile;
9mod payload;
10mod render;
11mod serialize;
12mod traversal;
13
14use std::collections::HashSet;
15use webui_protocol::WebUIProtocol;
16
17use crate::{AssetFileNameTemplate, WebUIError};
18
19/// A rendered static component asset root or shared chunk.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ComponentAssetFile {
22    /// Output filename for the ESM asset.
23    pub name: String,
24    /// JavaScript module content.
25    pub content: String,
26}
27
28/// Rendered component asset graph.
29#[derive(Debug)]
30pub struct ComponentAssetGraph {
31    /// Root and shared chunk ESM files.
32    pub files: Vec<ComponentAssetFile>,
33    /// Optional esbuild-compatible metafile JSON.
34    pub metafile: Option<String>,
35    entry_fragments: Vec<String>,
36    entry_components: Vec<String>,
37}
38
39impl ComponentAssetGraph {
40    pub(crate) fn retain_entry_protocol(&self, protocol: &mut WebUIProtocol) {
41        if self.files.is_empty() {
42            return;
43        }
44        protocol
45            .fragments
46            .retain(|name, _| self.entry_fragments.binary_search(name).is_ok());
47        protocol
48            .components
49            .retain(|name, _| self.entry_components.binary_search(name).is_ok());
50    }
51}
52
53/// Render a static component asset graph.
54///
55/// Entry-reachable components remain external prerequisites. Components used
56/// by one requested root stay inline, while components with an identical
57/// multi-root consumer set are emitted once in a shared chunk.
58///
59/// # Errors
60///
61/// Returns [`WebUIError`] when the entry/root graph is invalid, contains a
62/// route, lacks compiled template metadata, or produces colliding filenames.
63#[must_use = "component asset graph files must be written or otherwise consumed"]
64pub fn render_component_assets(
65    protocol: &WebUIProtocol,
66    entry: &str,
67    roots: &[String],
68    file_name_template: &str,
69    emit_metafile: bool,
70) -> Result<ComponentAssetGraph, WebUIError> {
71    if roots.is_empty() {
72        if emit_metafile {
73            return Err(WebUIError::InvalidBuildOptions(
74                "metafile requires at least one component_asset_root".to_string(),
75            ));
76        }
77        return Ok(ComponentAssetGraph {
78            files: Vec::new(),
79            metafile: None,
80            entry_fragments: Vec::new(),
81            entry_components: Vec::new(),
82        });
83    }
84
85    let file_name_template =
86        AssetFileNameTemplate::try_new(file_name_template.to_string(), "asset_file_name_template")
87            .map_err(|error| WebUIError::InvalidBuildOptions(error.to_string()))?;
88    let plan = graph::plan_component_assets(protocol, entry, roots)?;
89    let rendered =
90        render::render_component_asset_graph(protocol, &plan, &file_name_template, emit_metafile)?;
91    validate_unique_asset_file_names(&rendered.files)?;
92    let metafile = if emit_metafile {
93        Some(metafile::render_metafile(protocol, &rendered.outputs)?)
94    } else {
95        None
96    };
97
98    Ok(ComponentAssetGraph {
99        files: rendered.files,
100        metafile,
101        entry_fragments: plan.entry_fragments,
102        entry_components: plan.entry_components,
103    })
104}
105
106fn validate_unique_asset_file_names(files: &[ComponentAssetFile]) -> Result<(), WebUIError> {
107    let mut names = HashSet::with_capacity(files.len());
108    for file in files {
109        if !names.insert(file.name.as_str()) {
110            return Err(WebUIError::InvalidBuildOptions(format!(
111                "component asset filename collision for '{}'. Adjust --asset-file-name-template to include [name] or another unique component-specific segment.",
112                file.name
113            )));
114        }
115    }
116    Ok(())
117}