//! 把前端构建产物(dist/)嵌入本库。 //! //! `build.rs` 会调用 bun(无则 npm)执行前端构建,随后将 dist/ 下所有文件以 //! `include_bytes!` 生成到 `$OUT_DIR/dist_embed.rs`,由本文件引入后对外提供: //! //! - [`index()`]:`index.html`(SPA 入口) //! - [`file(path)`]:按请求路径取任意静态资源,如 `/assets/index-xxx.js` //! - [`exists(path)`] / [`paths()`]:资源存在性查询与清单 mod dist_embed { include!(concat!(env!("OUT_DIR"), "/dist_embed.rs")); } /// 一个嵌入在库中的 dist 静态资源。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Asset { /// 相对 dist/ 的路径(不含前导斜杠),如 `assets/index-xxx.js`。 pub path: &'static str, /// 按扩展名推断的 MIME type。 pub mime: &'static str, /// 文件内容。 pub content: &'static [u8], } /// SPA 入口 `index.html`。 pub fn index() -> Asset { Asset { path: "index.html", mime: "text/html; charset=utf-8", content: dist_embed::INDEX, } } /// 按请求路径查找嵌入的资源。 /// /// 接受 `/assets/x.js`、`assets/x.js`、带 `?query`/`#hash` 的路径; /// 根路径与 `index.html` 归一到 [`index()`];含 `..` 的越界路径返回 `None`。 pub fn file(path: &str) -> Option { let key = normalize(path)?; if key.is_empty() || key == "index.html" { return Some(index()); } dist_embed::FILES .iter() .find(|(p, _)| *p == key.as_str()) .map(|(p, content)| Asset { path: p, mime: mime_of(p), content, }) } pub fn exists(path: &str) -> bool { match normalize(path) { None => false, Some(key) => { key.is_empty() || key == "index.html" || dist_embed::FILES.iter().any(|(p, _)| *p == key.as_str()) } } } pub fn paths() -> &'static [&'static str] { dist_embed::PATHS } fn normalize(path: &str) -> Option { let path = path.split(['?', '#']).next().unwrap_or(path); let mut segs: Vec<&str> = Vec::new(); for seg in path.split('/') { match seg { "" | "." => {} ".." => return None, s => segs.push(s), } } Some(segs.join("/")) } fn mime_of(path: &str) -> &'static str { let ext = path .rsplit_once('.') .map_or("", |(_, e)| e) .to_ascii_lowercase(); match ext.as_str() { "html" | "htm" => "text/html; charset=utf-8", "js" | "mjs" => "text/javascript; charset=utf-8", "css" => "text/css; charset=utf-8", "json" | "map" => "application/json", "webmanifest" => "application/manifest+json", "svg" => "image/svg+xml", "png" => "image/png", "jpg" | "jpeg" => "image/jpeg", "gif" => "image/gif", "webp" => "image/webp", "avif" => "image/avif", "ico" => "image/x-icon", "wasm" => "application/wasm", "ttf" => "font/ttf", "otf" => "font/otf", "woff" => "font/woff", "woff2" => "font/woff2", "eot" => "application/vnd.ms-fontobject", "txt" => "text/plain; charset=utf-8", "xml" => "application/xml", "mp4" => "video/mp4", "webm" => "video/webm", "pdf" => "application/pdf", _ => "application/octet-stream", } } #[cfg(test)] mod tests { use super::*; #[test] fn index_is_html() { let a = index(); assert_eq!(a.mime, "text/html; charset=utf-8"); assert!( a.content .to_ascii_lowercase() .windows(5) .any(|w| w == b"