trusty_memory/ui_assets.rs
1//! The embedded Svelte SPA, kept in the binary with nothing serving it yet
2//! (#6286).
3//!
4//! Why the assets stay: ADR-0032 moves this dashboard onto `trusty-console`,
5//! which mounts a sibling daemon's SPA by vendoring its `rust_embed` assets —
6//! the disposition #6287 settled for trusty-analyze. That mount is a
7//! fast-follow, and deleting `ui/dist/` now would mean restoring it then. The
8//! files are git-tracked and `build.rs` regenerates them, so keeping the embed
9//! costs binary size and nothing else.
10//!
11//! Why the handler is gone: `static_handler` was an axum fallback route and
12//! `serve_embedded` built an `axum::response::Response`. Neither can exist
13//! without the listener this crate no longer binds, and keeping them would have
14//! meant keeping an axum dependency for code no request can reach.
15//!
16//! What is left: [`WebAssets`] and [`asset`], which answer "what is in the
17//! bundle" and "give me these bytes and their content type" — the two questions
18//! a console-side mount asks, with no framework in either.
19//!
20//! Test: `embedded_bundle_carries_an_index_html`.
21
22use rust_embed::RustEmbed;
23
24/// Embedded UI assets produced by `pnpm build` in `ui/`.
25///
26/// `build.rs` runs the Vite build before compilation, so the folder is always
27/// populated. In this monorepo the UI lives inside the crate rather than at the
28/// repo root, which is why the path is not the upstream `../../ui/dist/`.
29#[derive(RustEmbed)]
30#[folder = "$CARGO_MANIFEST_DIR/ui/dist/"]
31pub struct WebAssets;
32
33/// One embedded file: its bytes and the content type to serve it as.
34#[derive(Debug, Clone)]
35pub struct Asset {
36 /// File contents.
37 pub bytes: Vec<u8>,
38 /// MIME type guessed from the extension.
39 pub content_type: String,
40}
41
42/// Look up one embedded asset, defaulting an empty path to the SPA shell.
43///
44/// Why: hash-based routing lives client-side, so a mount serves `index.html`
45/// for any path that is not a real file — but that fallback decision belongs to
46/// whoever owns the route, not here. This answers only whether the file exists.
47/// What: `None` when the path is not in the bundle; otherwise the bytes plus
48/// the `mime_guess` content type.
49/// Test: `embedded_bundle_carries_an_index_html`.
50pub fn asset(path: &str) -> Option<Asset> {
51 let path = if path.is_empty() { "index.html" } else { path };
52 let file = WebAssets::get(path)?;
53 Some(Asset {
54 content_type: mime_guess::from_path(path)
55 .first_or_octet_stream()
56 .to_string(),
57 bytes: file.data.into_owned(),
58 })
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 /// Why: the bundle is built by `build.rs` and embedded at compile time, so
66 /// an empty `ui/dist/` produces a binary that compiles and serves nothing.
67 /// The shell is what every SPA route falls back to, so its presence is the
68 /// one thing worth asserting about the embed itself.
69 /// Test: itself.
70 #[test]
71 fn embedded_bundle_carries_an_index_html() {
72 let shell = asset("").expect("the SPA shell must be embedded");
73 assert!(!shell.bytes.is_empty(), "index.html must not be empty");
74 assert!(
75 shell.content_type.starts_with("text/html"),
76 "expected text/html, got {}",
77 shell.content_type
78 );
79 }
80}