use std::cmp::Ordering;
use std::collections::HashSet;
use webui_protocol::WebUIProtocol;
use super::traversal::{has_template_payload, CollectedClosure, GraphIndex, TraversalScratch};
use crate::WebUIError;
pub(super) struct AssetGraphPlan<'a> {
pub component_names: Vec<&'a str>,
pub roots: Vec<RootPlan>,
pub chunks: Vec<ChunkPlan>,
pub emitted_components: Vec<usize>,
pub entry_fragments: Vec<String>,
pub entry_components: Vec<String>,
}
pub(super) struct RootPlan {
pub root: String,
pub components: Vec<usize>,
pub required_components: Vec<usize>,
pub external_components: Vec<usize>,
pub chunks: Vec<usize>,
}
pub(super) struct ChunkPlan {
pub name: String,
pub components: Vec<usize>,
pub consumers: Vec<usize>,
}
pub(super) fn plan_component_assets<'a>(
protocol: &'a WebUIProtocol,
entry: &str,
roots: &[String],
) -> Result<AssetGraphPlan<'a>, WebUIError> {
let index = GraphIndex::new(protocol);
let roots = validate_roots(protocol, roots)?;
let mut canonical_roots = roots;
canonical_roots.sort_unstable();
let mut scratch = TraversalScratch::new(index.fragment_names.len());
let entry_closure = scratch.collect(protocol, &index, entry)?;
let mut entry_mask = vec![false; index.component_names.len()];
for component in &entry_closure.components {
entry_mask[*component] = true;
}
let mut root_closures = Vec::with_capacity(canonical_roots.len());
for root in &canonical_roots {
root_closures.push(scratch.collect(protocol, &index, root)?);
}
if canonical_roots.len() == 1 {
let required_components = std::mem::take(&mut root_closures[0].components);
let mut components = Vec::with_capacity(required_components.len());
let mut external_components = Vec::new();
for component in &required_components {
if entry_mask[*component] {
external_components.push(*component);
} else {
components.push(*component);
}
}
let emitted_components = components.clone();
return Ok(finalize_plan(
index,
entry_closure,
vec![RootPlan {
root: canonical_roots.swap_remove(0),
components,
required_components,
external_components,
chunks: Vec::new(),
}],
Vec::new(),
emitted_components,
));
}
let words = canonical_roots.len().div_ceil(u64::BITS as usize);
let matrix_len = index
.component_names
.len()
.checked_mul(words)
.ok_or_else(component_graph_too_large)?;
let mut consumers = vec![0u64; matrix_len];
for (root_id, closure) in root_closures.iter().enumerate() {
for component in &closure.components {
if !entry_mask[*component] {
set_consumer(&mut consumers, words, *component, root_id);
}
}
}
let mut local_components = vec![Vec::new(); canonical_roots.len()];
let mut shared_components = Vec::new();
for (component, is_entry_component) in entry_mask.iter().copied().enumerate() {
if is_entry_component {
continue;
}
let row = consumer_row(&consumers, words, component);
match consumer_count(row) {
0 => {}
1 => local_components[single_consumer(row)].push(component),
_ => shared_components.push(component),
}
}
shared_components.sort_unstable_by(|left, right| {
let rows =
consumer_row(&consumers, words, *left).cmp(consumer_row(&consumers, words, *right));
if rows == Ordering::Equal {
index.component_names[*left].cmp(index.component_names[*right])
} else {
rows
}
});
let mut chunks = group_shared_components(
&shared_components,
&consumers,
words,
&index.component_names,
);
chunks.sort_unstable_by(|left, right| left.name.cmp(&right.name));
let mut root_chunk_ids = vec![Vec::new(); canonical_roots.len()];
for (chunk_id, chunk) in chunks.iter().enumerate() {
for consumer in &chunk.consumers {
root_chunk_ids[*consumer].push(chunk_id);
}
}
let mut root_plans = Vec::with_capacity(canonical_roots.len());
for (root_id, root) in canonical_roots.into_iter().enumerate() {
let required_components = root_closures[root_id].components.clone();
let external_components = required_components
.iter()
.copied()
.filter(|component| entry_mask[*component])
.collect();
root_plans.push(RootPlan {
root,
components: std::mem::take(&mut local_components[root_id]),
required_components,
external_components,
chunks: std::mem::take(&mut root_chunk_ids[root_id]),
});
}
let mut emitted_components = Vec::new();
for root in &root_plans {
emitted_components.extend_from_slice(&root.components);
}
for chunk in &chunks {
emitted_components.extend_from_slice(&chunk.components);
}
emitted_components.sort_unstable();
emitted_components.dedup();
Ok(finalize_plan(
index,
entry_closure,
root_plans,
chunks,
emitted_components,
))
}
fn finalize_plan<'a>(
index: GraphIndex<'a>,
entry_closure: CollectedClosure,
roots: Vec<RootPlan>,
chunks: Vec<ChunkPlan>,
emitted_components: Vec<usize>,
) -> AssetGraphPlan<'a> {
let mut entry_fragments: Vec<String> = entry_closure
.fragments
.into_iter()
.map(|id| index.fragment_names[id].to_string())
.collect();
entry_fragments.sort_unstable();
let mut entry_components: Vec<String> = entry_closure
.components
.into_iter()
.map(|id| index.component_names[id].to_string())
.collect();
entry_components.sort_unstable();
AssetGraphPlan {
component_names: index.component_names,
roots,
chunks,
emitted_components,
entry_fragments,
entry_components,
}
}
fn group_shared_components(
components: &[usize],
consumers: &[u64],
words: usize,
component_names: &[&str],
) -> Vec<ChunkPlan> {
let mut chunks = Vec::new();
let mut start = 0usize;
while start < components.len() {
let row = consumer_row(consumers, words, components[start]);
let mut end = start + 1;
while end < components.len() && consumer_row(consumers, words, components[end]) == row {
end += 1;
}
let mut chunk_components = components[start..end].to_vec();
chunk_components.sort_unstable_by_key(|component| component_names[*component]);
let name = format!("chunk-{}", component_names[chunk_components[0]]);
chunks.push(ChunkPlan {
name,
components: chunk_components,
consumers: consumers_from_row(row),
});
start = end;
}
chunks
}
fn set_consumer(consumers: &mut [u64], words: usize, component: usize, root: usize) {
let offset = component * words + root / u64::BITS as usize;
consumers[offset] |= 1u64 << (root % u64::BITS as usize);
}
fn consumer_row(consumers: &[u64], words: usize, component: usize) -> &[u64] {
let start = component * words;
&consumers[start..start + words]
}
fn consumer_count(row: &[u64]) -> u32 {
row.iter().map(|word| word.count_ones()).sum()
}
fn single_consumer(row: &[u64]) -> usize {
for (word_index, word) in row.iter().copied().enumerate() {
if word != 0 {
return word_index * u64::BITS as usize + word.trailing_zeros() as usize;
}
}
0
}
fn consumers_from_row(row: &[u64]) -> Vec<usize> {
let mut consumers = Vec::with_capacity(consumer_count(row) as usize);
for (word_index, mut word) in row.iter().copied().enumerate() {
while word != 0 {
let bit = word.trailing_zeros() as usize;
consumers.push(word_index * u64::BITS as usize + bit);
word &= word - 1;
}
}
consumers
}
fn validate_roots(protocol: &WebUIProtocol, roots: &[String]) -> Result<Vec<String>, WebUIError> {
let mut seen = HashSet::with_capacity(roots.len());
let mut normalized = Vec::with_capacity(roots.len());
for raw in roots {
let tag = raw.trim();
validate_root(protocol, tag, &mut seen)?;
normalized.push(tag.to_string());
}
Ok(normalized)
}
fn validate_root(
protocol: &WebUIProtocol,
tag: &str,
seen: &mut HashSet<String>,
) -> Result<(), WebUIError> {
if tag.is_empty() {
return Err(WebUIError::InvalidBuildOptions(
"--emit-component-assets contains an empty component tag".to_string(),
));
}
if !is_component_tag_name(tag) {
return Err(WebUIError::InvalidBuildOptions(format!(
"--emit-component-assets component '{tag}' must be a lowercase kebab-case custom element tag"
)));
}
if !seen.insert(tag.to_string()) {
return Err(WebUIError::InvalidBuildOptions(format!(
"--emit-component-assets contains duplicate component <{tag}>"
)));
}
if !protocol.fragments.contains_key(tag) {
return Err(WebUIError::InvalidBuildOptions(format!(
"--emit-component-assets requested unknown component <{tag}>. Add a discovered {tag}.html component or remove it from the allowlist."
)));
}
if !protocol
.components
.get(tag)
.is_some_and(has_template_payload)
{
return Err(WebUIError::InvalidBuildOptions(format!(
"--emit-component-assets requested <{tag}>, but it has no compiled template metadata. Build with a plugin that emits component templates and ensure the component has a template."
)));
}
Ok(())
}
fn is_component_tag_name(tag: &str) -> bool {
let bytes = tag.as_bytes();
!bytes.is_empty()
&& bytes.contains(&b'-')
&& bytes[0].is_ascii_lowercase()
&& bytes[bytes.len() - 1].is_ascii_alphanumeric()
&& bytes
.iter()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'-')
}
#[cold]
#[inline(never)]
fn component_graph_too_large() -> WebUIError {
WebUIError::InvalidBuildOptions(
"component asset graph is too large to index safely".to_string(),
)
}