formawasm/component.rs
1//! Component-Model wrap layer.
2//!
3//! [`wrap_component`] takes the core-Wasm bytes produced by
4//! [`crate::lower_module`] together with the WIT source produced by
5//! [`crate::emit_wit`], embeds the corresponding world identity into
6//! the core module's `component-type` custom section, and runs
7//! [`wit_component::ComponentEncoder`] to emit a final Component-Model
8//! artifact.
9//!
10//! The WIT must declare exactly one world named [`crate::wit::WORLD_NAME`]
11//! — that's the contract `emit_wit` produces and `wrap_component`
12//! consumes.
13
14use thiserror::Error;
15use wit_component::{ComponentEncoder, StringEncoding, embed_component_metadata};
16use wit_parser::Resolve;
17
18use crate::wit::WORLD_NAME;
19
20/// Errors produced by [`wrap_component`].
21#[derive(Debug, Error)]
22#[non_exhaustive]
23pub enum ComponentWrapError {
24 /// The supplied WIT string failed to parse via `wit-parser`.
25 /// Should not happen for strings produced by [`crate::emit_wit`],
26 /// which round-trips its own output before returning.
27 #[error("WIT failed to parse: {reason}")]
28 WitParse {
29 /// `wit-parser`'s diagnostic rendered as a string.
30 reason: String,
31 },
32
33 /// The supplied WIT did not declare a world named
34 /// [`crate::wit::WORLD_NAME`].
35 #[error("WIT does not declare a world named '{WORLD_NAME}': {reason}")]
36 WorldMissing {
37 /// `wit-parser`'s diagnostic rendered as a string.
38 reason: String,
39 },
40
41 /// `wit_component::embed_component_metadata` failed to attach the
42 /// custom section to the core module.
43 #[error("failed to embed component-type metadata into core module: {reason}")]
44 EmbedFailed {
45 /// `wit-component`'s diagnostic rendered as a string.
46 reason: String,
47 },
48
49 /// `ComponentEncoder::module` rejected the core-Wasm bytes — most
50 /// likely because the embedded metadata refers to exports the
51 /// module does not actually provide.
52 #[error("ComponentEncoder rejected the core module: {reason}")]
53 EncoderRejected {
54 /// `wit-component`'s diagnostic rendered as a string.
55 reason: String,
56 },
57
58 /// `ComponentEncoder::encode` produced no bytes — the encoder ran
59 /// to completion but the result failed component-model validation.
60 #[error("ComponentEncoder failed to encode the component: {reason}")]
61 EncodeFailed {
62 /// `wit-component`'s diagnostic rendered as a string.
63 reason: String,
64 },
65}
66
67/// Wrap `core_bytes` into a Component-Model artifact.
68///
69/// `core_bytes` is consumed because `wit_component::embed_component_metadata`
70/// appends the custom section in place. `wit` must declare exactly
71/// one world named [`crate::wit::WORLD_NAME`].
72pub fn wrap_component(mut core_bytes: Vec<u8>, wit: &str) -> Result<Vec<u8>, ComponentWrapError> {
73 let mut resolve = Resolve::default();
74 let pkg = resolve
75 .push_str("formawasm-generated.wit", wit)
76 .map_err(|e| ComponentWrapError::WitParse {
77 reason: format!("{e:#}"),
78 })?;
79 let world = resolve
80 .select_world(&[pkg], Some(WORLD_NAME))
81 .map_err(|e| ComponentWrapError::WorldMissing {
82 reason: format!("{e:#}"),
83 })?;
84
85 embed_component_metadata(&mut core_bytes, &resolve, world, StringEncoding::UTF8).map_err(
86 |e| ComponentWrapError::EmbedFailed {
87 reason: format!("{e:#}"),
88 },
89 )?;
90
91 let mut encoder = ComponentEncoder::default()
92 .validate(true)
93 .module(&core_bytes)
94 .map_err(|e| ComponentWrapError::EncoderRejected {
95 reason: format!("{e:#}"),
96 })?;
97 encoder
98 .encode()
99 .map_err(|e| ComponentWrapError::EncodeFailed {
100 reason: format!("{e:#}"),
101 })
102}