- 1
//! Serving an embedded SPA bundle, shared by the operations console - 2
//! (`/admin`) and the workspace client (`/app`). - 3
//! - 4
//! This exists as one module because the naive version of it shipped - 5
//! broken three times. `include_dir`'s `Dir::files()` is NOT recursive — - 6
//! unlike `Dir::get_file`'s lookup — so a route-registration loop that - 7
//! calls it once on the top-level directory never descends into - 8
//! `assets/`. Every file is correctly embedded, `index.html` correctly - 9
//! references the right hashed names, and every one of those requests - 10
//! 404s from axum's router before the (correct) recursive lookup ever - 11
//! runs. The failure reads as "the page loads but nothing on it works", - 12
//! because the shell always renders and the JS never arrives. - 13
//! - 14
//! That is a mistake worth making once, in one place, with a test. - 15
- 16
use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE}; - 17
use axum::response::IntoResponse; - 18
use axum::routing::get; - 19
use include_dir::Dir; - 20
- 21
pub(crate) fn mime_for(path: &str) -> &'static str { - 22
match path.rsplit('.').next() { - 23
Some("html") => "text/html; charset=utf-8", - 24
Some("js") => "text/javascript; charset=utf-8", - 25
Some("css") => "text/css; charset=utf-8", - 26
Some("svg") => "image/svg+xml", - 27
Some("json") => "application/json", - 28
Some("webmanifest") => "application/manifest+json", - 29
Some("png") => "image/png", - 30
Some("jpg" | "jpeg") => "image/jpeg", - 31
Some("webp") => "image/webp", - 32
Some("ico") => "image/x-icon", - 33
Some("woff2") => "font/woff2", - 34
_ => "application/octet-stream", - 35
} - 36
} - 37
- 38
pub(crate) fn serve_file(dir: &'static Dir<'static>, path: &str) -> axum::response::Response { - 39
match dir.get_file(path) { - 40
Some(file) => { - 41
// Hashed asset filenames are immutable; index.html must always - 42
// revalidate so deploys pick up new hashes. - 43
let cache = if path.starts_with("assets/") { - 44
"public, max-age=31536000, immutable" - 45
} else { - 46
"no-cache" - 47
}; - 48
( - 49
[(CONTENT_TYPE, mime_for(path)), (CACHE_CONTROL, cache)], - 50
file.contents(), - 51
) - 52
.into_response() - 53
} - 54
None => (axum::http::StatusCode::NOT_FOUND, "not found").into_response(), - 55
} - 56
} - 57
- 58
/// Register a route for every file under `dir`, recursing into - 59
/// subdirectories. `prefix` is the URL root, e.g. `/admin` or `/app`. - 60
pub(crate) fn register_files( - 61
mut router: axum::Router<crate::AppState>, - 62
dir: &'static Dir<'static>, - 63
root: &'static Dir<'static>, - 64
prefix: &str, - 65
) -> axum::Router<crate::AppState> { - 66
for file in dir.files() { - 67
let path = file.path().to_string_lossy().to_string(); - 68
let uri = format!("{prefix}/{path}"); - 69
router = router.route(&uri, get(move || async move { serve_file(root, &path) })); - 70
} - 71
for sub in dir.dirs() { - 72
router = register_files(router, sub, root, prefix); - 73
} - 74
router - 75
} - 76
- 77
/// Every asset `index.html` references must be embedded AND routable. - 78
/// - 79
/// Two distinct layers, both of which have been broken in shipped - 80
/// releases: v0.8.1 embedded a stale bundle whose hashed names no longer - 81
/// matched, and v0.8.1–v0.8.3 registered no routes under `assets/` at all. - 82
#[cfg(test)] - 83
pub(crate) fn referenced_assets(dir: &'static Dir<'static>, prefix: &str) -> Vec<String> { - 84
let index = dir - 85
.get_file("index.html") - 86
.and_then(|f| f.contents_utf8()) - 87
.unwrap_or_default(); - 88
let needle = format!("{prefix}/assets/"); - 89
index - 90
.split(['"', '\'']) - 91
.filter(|s| s.starts_with(&needle)) - 92
.map(String::from) - 93
.collect() - 94
} - 95
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.