use std::convert::Infallible;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use bytes::Bytes;
use serde::Serialize;
use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::error::RendererError;
use crate::protocol::{HeadTag, PageData, PageMeta};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RendererMode {
Development,
Production,
}
#[derive(Debug, Clone)]
pub struct RuntimeRendererConfig {
pub mode: RendererMode,
pub js_entrypoint: PathBuf,
pub js_source_dir: PathBuf,
pub client_manifest_path: Option<PathBuf>,
pub project_root: Option<PathBuf>,
pub translations_dir: Option<PathBuf>,
pub default_locale: String,
pub fallback_locale: String,
pub vite_dev_server_origin: Option<String>,
pub start_vite_dev_server: bool,
pub version: String,
pub url: Option<String>,
pub asset_script_url: Option<String>,
pub title: String,
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedRuntimeRendererConfig {
pub mode: RendererMode,
pub js_entrypoint: PathBuf,
pub js_source_dir: PathBuf,
pub client_manifest_path: Option<PathBuf>,
pub project_root: PathBuf,
pub translations_dir: PathBuf,
pub default_locale: String,
pub fallback_locale: String,
pub vite_dev_server_origin: Option<String>,
pub start_vite_dev_server: bool,
pub version: String,
pub url: Option<String>,
pub asset_script_url: Option<String>,
pub title: String,
}
impl RuntimeRendererConfig {
pub(crate) fn normalize(self) -> Result<ResolvedRuntimeRendererConfig, RendererError> {
let cwd = std::env::current_dir()?;
let project_root = self
.project_root
.as_ref()
.map(|path| absolutize(&cwd, path))
.unwrap_or_else(|| cwd.clone());
let js_source_dir = resolve_js_source_dir(&project_root, &self.js_source_dir);
let js_entrypoint = absolutize(&project_root, &self.js_entrypoint);
let client_manifest_path = self
.client_manifest_path
.as_ref()
.map(|path| absolutize(&project_root, path));
let translations_dir = self
.translations_dir
.as_ref()
.map(|path| absolutize(&project_root, path))
.unwrap_or_else(|| project_root.join("locales"));
let version =
resolve_renderer_version(self.mode, &self.version, client_manifest_path.as_ref())?;
Ok(ResolvedRuntimeRendererConfig {
mode: self.mode,
js_entrypoint,
js_source_dir,
client_manifest_path,
project_root,
translations_dir,
default_locale: self.default_locale,
fallback_locale: self.fallback_locale,
vite_dev_server_origin: self.vite_dev_server_origin,
start_vite_dev_server: self.start_vite_dev_server,
version,
url: self.url,
asset_script_url: self.asset_script_url,
title: self.title,
})
}
pub(crate) fn development() -> Self {
Self {
mode: RendererMode::Development,
js_entrypoint: PathBuf::from("dist/server/entry-server.mjs"),
js_source_dir: PathBuf::from("app"),
client_manifest_path: Some(PathBuf::from("dist/client/.vite/manifest.json")),
project_root: None,
translations_dir: Some(PathBuf::from("locales")),
default_locale: "en".into(),
fallback_locale: "en".into(),
vite_dev_server_origin: Some("http://127.0.0.1:5173".into()),
start_vite_dev_server: false,
version: "dev".into(),
url: Some("/".into()),
asset_script_url: None,
title: "App".into(),
}
}
}
impl Default for RuntimeRendererConfig {
fn default() -> Self {
Self::development()
}
}
fn absolutize(base: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
base.join(path)
}
}
fn resolve_js_source_dir(project_root: &Path, configured: &Path) -> PathBuf {
let resolved = absolutize(project_root, configured);
if configured.is_absolute() || configured != Path::new("app") || resolved.exists() {
return resolved;
}
let app_dir = project_root.join("app");
if app_dir.exists() {
return app_dir;
}
resolved
}
#[derive(Debug, Clone)]
pub struct RenderedPage {
pub html: String,
pub head: Option<String>,
pub status: Option<u16>,
}
pub struct RenderedPageStream {
pub stream: UnboundedReceiverStream<Result<Bytes, Infallible>>,
pub status: Option<u16>,
}
#[derive(Debug, Clone)]
pub struct PageRoute {
pub page: PageData,
pub meta: PageMeta,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DirectRender;
#[derive(Debug, Clone, Copy, Default)]
pub struct DirectStream;
impl PageRoute {
pub fn set_props<T>(&mut self, props: T) -> Result<(), RendererError>
where
T: Serialize,
{
self.page.props = serde_json::to_value(props)?;
Ok(())
}
pub fn props<T>(mut self, props: T) -> Result<Self, RendererError>
where
T: Serialize,
{
self.set_props(props)?;
Ok(self)
}
pub fn locale(mut self, locale: impl Into<String>) -> Self {
self.page.locale = locale.into();
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.page.version = version.into();
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.meta.title = Some(title.into());
self
}
pub fn set_resources<T>(&mut self, resources: T) -> Result<(), RendererError>
where
T: Serialize,
{
self.meta.resources = serde_json::to_value(resources)?;
Ok(())
}
pub fn resources<T>(mut self, resources: T) -> Result<Self, RendererError>
where
T: Serialize,
{
self.set_resources(resources)?;
Ok(self)
}
pub fn set_context<T>(&mut self, context: T) -> Result<(), RendererError>
where
T: Serialize,
{
let context = serde_json::to_value(context)?;
let props = self.page.props.as_object_mut().ok_or_else(|| {
RendererError::Serialization(
"page props must be an object to attach page context".into(),
)
})?;
props.insert("_context".into(), context);
Ok(())
}
pub fn context<T>(mut self, context: T) -> Result<Self, RendererError>
where
T: Serialize,
{
self.set_context(context)?;
Ok(self)
}
pub fn head(mut self, head: Vec<HeadTag>) -> Self {
self.meta.head = head;
self
}
pub fn with_page(mut self, page: PageData) -> Self {
self.page = page;
self
}
}
fn resolve_renderer_version(
mode: RendererMode,
configured: &str,
client_manifest_path: Option<&PathBuf>,
) -> Result<String, RendererError> {
if mode == RendererMode::Development {
return Ok(configured.to_owned());
}
if !configured.trim().is_empty() && configured != "dev" {
return Ok(configured.to_owned());
}
let Some(manifest_path) = client_manifest_path else {
return Ok(configured.to_owned());
};
if !manifest_path.exists() {
return Ok(configured.to_owned());
}
let contents = std::fs::read(manifest_path)?;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
contents.hash(&mut hasher);
Ok(format!("{:x}", hasher.finish()))
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{RendererMode, RuntimeRendererConfig};
#[test]
fn default_config_points_to_app_dir() {
let config = RuntimeRendererConfig::development();
assert_eq!(config.mode, RendererMode::Development);
assert_eq!(config.js_source_dir, PathBuf::from("app"));
}
}