use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderMode {
Phase1ClientMount,
Phase2SsrReconcile,
}
#[derive(Debug)]
pub struct PageConfig<'a> {
pub title: &'a str,
pub route_pattern: &'a str,
pub manifest: &'a AssetManifest,
pub config_script: Option<&'a str>,
pub config_json: Option<&'a serde_json::Value>,
pub body_class: Option<&'a str>,
pub personality_css: Option<&'a str>,
pub body_prefix: Option<&'a str>,
pub render_mode: RenderMode,
pub ir_module: Option<&'a forma_ir::parser::IrModule>,
pub slots: Option<&'a forma_ir::slot::SlotData>,
}
#[non_exhaustive]
#[derive(Debug)]
pub struct PageOutput {
pub html: String,
pub csp: String,
pub nonce: crate::csp::CspNonce,
}
impl PageOutput {
pub fn headers(&self) -> Vec<(axum::http::HeaderName, axum::http::HeaderValue)> {
let mut headers = vec![
(
axum::http::header::CONTENT_TYPE,
axum::http::HeaderValue::from_static("text/html; charset=utf-8"),
),
(
axum::http::header::CACHE_CONTROL,
axum::http::HeaderValue::from_static("no-store"),
),
];
if let Ok(value) = axum::http::HeaderValue::from_str(&self.csp) {
headers.push((axum::http::header::CONTENT_SECURITY_POLICY, value));
}
headers.extend_from_slice(document_security_headers());
headers
}
}
pub const NOSNIFF: (&str, &str) = ("x-content-type-options", "nosniff");
pub fn document_security_headers() -> &'static [(axum::http::HeaderName, axum::http::HeaderValue)] {
use axum::http::{HeaderName, HeaderValue};
static HEADERS: std::sync::OnceLock<Vec<(HeaderName, HeaderValue)>> =
std::sync::OnceLock::new();
HEADERS.get_or_init(|| {
vec![
(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
),
(
HeaderName::from_static("referrer-policy"),
HeaderValue::from_static("strict-origin-when-cross-origin"),
),
(
HeaderName::from_static("permissions-policy"),
HeaderValue::from_static("geolocation=(), camera=(), microphone=()"),
),
(
HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
),
]
})
}
pub fn asset_security_headers() -> &'static [(axum::http::HeaderName, axum::http::HeaderValue)] {
use axum::http::{HeaderName, HeaderValue};
static HEADERS: std::sync::OnceLock<Vec<(HeaderName, HeaderValue)>> =
std::sync::OnceLock::new();
HEADERS.get_or_init(|| {
vec![
(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
),
(
HeaderName::from_static("cross-origin-resource-policy"),
HeaderValue::from_static("same-origin"),
),
]
})
}
#[derive(Debug, Clone, Deserialize)]
pub struct AssetManifest {
pub version: u32,
pub build_hash: String,
pub assets: HashMap<String, String>,
pub routes: HashMap<String, RouteAssets>,
#[serde(default)]
pub wasm: Option<WasmAssets>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RouteAssets {
pub js: Vec<String>,
pub css: Vec<String>,
pub fonts: Vec<String>,
pub total_size_br: u64,
pub budget_warn_threshold: u64,
#[serde(default)]
pub ir: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WasmAssets {
pub loader: String,
pub binary: String,
}
impl AssetManifest {
pub fn resolve<'a>(&'a self, logical_name: &'a str) -> &'a str {
self.assets
.get(logical_name)
.map(|s| s.as_str())
.unwrap_or(logical_name)
}
pub fn route(&self, pattern: &str) -> Option<&RouteAssets> {
self.routes.get(pattern)
}
}