#[cfg(engine)]
use crate::server::HtmlShell;
#[cfg(engine)]
use crate::utils::get_path_prefix_server;
use crate::{
error_views::ErrorViews,
i18n::{Locales, TranslationsManager},
plugins::{PluginAction, Plugins},
state::GlobalStateCreator,
stores::MutableStore,
template::{Entity, Forever, Template},
};
#[cfg(any(client, doc))]
use crate::{
error_views::{ErrorContext, ErrorPosition},
errors::ClientError,
};
use crate::{errors::PluginError, template::Capsule};
use crate::{stores::ImmutableStore, template::EntityMap};
use futures::Future;
#[cfg(any(client, doc))]
use std::marker::PhantomData;
#[cfg(engine)]
use std::pin::Pin;
#[cfg(any(client, doc))]
use std::rc::Rc;
use std::{any::TypeId, sync::Arc};
use std::{collections::HashMap, panic::PanicInfo};
use sycamore::prelude::Scope;
use sycamore::utils::hydrate::with_no_hydration_context;
use sycamore::web::{Html, SsrNode};
use sycamore::{
prelude::{component, view},
view::View,
};
static DFLT_INDEX_VIEW: &str = r#"
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root"></div>
</body>
</html>"#;
static DFLT_PSS_MAX_SIZE: usize = 25;
#[cfg(engine)]
pub(crate) enum Tm<T: TranslationsManager> {
Dummy(T),
Full(Pin<Box<dyn Future<Output = T>>>),
}
#[cfg(engine)]
impl<T: TranslationsManager> std::fmt::Debug for Tm<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tm").finish_non_exhaustive()
}
}
#[doc(hidden)]
pub trait TranslationsManagerGetter {
type Output: TranslationsManager;
fn call(&self) -> Box<dyn Future<Output = Self::Output>>;
}
impl<T, F, Fut> TranslationsManagerGetter for F
where
T: TranslationsManager,
F: Fn() -> Fut,
Fut: Future<Output = T> + 'static,
{
type Output = T;
fn call(&self) -> Box<dyn Future<Output = Self::Output>> {
Box::new(self())
}
}
pub struct PerseusAppBase<G: Html, M: MutableStore, T: TranslationsManager> {
pub(crate) root: String,
pub(crate) entities: EntityMap<G>,
#[cfg(client)]
pub(crate) error_views: Option<Rc<ErrorViews<G>>>,
#[cfg(engine)]
pub(crate) error_views: Option<Arc<ErrorViews<G>>>,
pub(crate) pss_max_size: usize,
#[cfg(engine)]
pub(crate) global_state_creator: Arc<GlobalStateCreator>,
pub(crate) locales: Locales,
#[cfg(engine)]
pub(crate) static_aliases: HashMap<String, String>,
#[cfg(engine)]
pub(crate) plugins: Arc<Plugins>,
#[cfg(client)]
pub(crate) plugins: Rc<Plugins>,
#[cfg(engine)]
pub(crate) immutable_store: ImmutableStore,
pub(crate) index_view: String,
#[cfg(engine)]
pub(crate) mutable_store: M,
#[cfg(engine)]
pub(crate) translations_manager: Tm<T>,
#[cfg(engine)]
pub(crate) static_dir: String,
#[cfg(any(client, doc))]
#[allow(clippy::type_complexity)] pub(crate) panic_handler: Option<Box<dyn Fn(&PanicInfo) + Send + Sync + 'static>>,
#[cfg(any(client, doc))]
#[allow(clippy::type_complexity)]
pub(crate) panic_handler_view: Arc<
dyn Fn(Scope, ClientError, ErrorContext, ErrorPosition) -> (View<SsrNode>, View<G>)
+ Send
+ Sync,
>,
#[cfg(any(client, doc))]
_marker: PhantomData<(M, T)>,
}
impl<G: Html, M: MutableStore, T: TranslationsManager> std::fmt::Debug for PerseusAppBase<G, M, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("PerseusAppBase");
debug
.field("root", &self.root)
.field("entities", &self.entities)
.field("error_views", &self.error_views)
.field("pss_max_size", &self.pss_max_size)
.field("locale", &self.locales)
.field("plugins", &self.plugins)
.field("index_view", &self.index_view);
#[cfg(any(client, doc))]
{
return debug
.field(
"panic_handler",
&self
.panic_handler
.as_ref()
.map(|_| "dyn Fn(&PanicInfo) + Send + Sync + 'static"),
)
.finish_non_exhaustive();
}
#[cfg(engine)]
{
return debug
.field("global_state_creator", &self.global_state_creator)
.field("mutable_store", &self.mutable_store)
.field("translations_manager", &self.translations_manager)
.field("static_dir", &self.static_dir)
.field("static_aliases", &self.static_aliases)
.field("immutable_store", &self.immutable_store)
.finish_non_exhaustive();
}
}
}
impl<G: Html, T: TranslationsManager> PerseusAppBase<G, FsMutableStore, T> {
#[cfg(engine)]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::new_with_mutable_store(FsMutableStore::new("./dist/mutable".to_string()))
}
#[cfg(any(client, doc))]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::new_wasm()
}
}
impl<G: Html, M: MutableStore> PerseusAppBase<G, M, FsTranslationsManager> {
pub fn locales_lit_and_translations_manager(mut self, locales: Locales) -> Self {
#[cfg(engine)]
let using_i18n = locales.using_i18n;
self.locales = locales;
#[cfg(engine)]
{
if using_i18n {
let all_locales: Vec<String> = self
.locales
.get_all()
.iter()
.cloned()
.cloned()
.collect();
let tm_fut = FsTranslationsManager::new(
crate::i18n::DFLT_TRANSLATIONS_DIR.to_string(),
all_locales,
crate::i18n::TRANSLATOR_FILE_EXT.to_string(),
);
self.translations_manager = Tm::Full(Box::pin(tm_fut));
} else {
self.translations_manager = Tm::Dummy(FsTranslationsManager::new_dummy());
}
}
self
}
pub fn locales_and_translations_manager(self, default: &str, other: &[&str]) -> Self {
let locales = Locales {
default: default.to_string(),
other: other.iter().map(|s| s.to_string()).collect(),
using_i18n: true,
};
self.locales_lit_and_translations_manager(locales)
}
}
impl<G: Html, M: MutableStore, T: TranslationsManager> PerseusAppBase<G, M, T> {
#[allow(unused_variables)]
pub fn new_with_mutable_store(mutable_store: M) -> Self {
Self {
root: "root".to_string(),
entities: HashMap::new(),
error_views: None,
pss_max_size: DFLT_PSS_MAX_SIZE,
#[cfg(engine)]
global_state_creator: Arc::new(GlobalStateCreator::default()),
locales: Locales {
default: "xx-XX".to_string(),
other: Vec::new(),
using_i18n: false,
},
#[cfg(engine)]
static_aliases: HashMap::new(),
#[cfg(engine)]
plugins: Arc::new(Plugins::new()),
#[cfg(any(client, doc))]
plugins: Rc::new(Plugins::new()),
#[cfg(engine)]
immutable_store: ImmutableStore::new("./dist".to_string()),
#[cfg(engine)]
mutable_store,
#[cfg(engine)]
translations_manager: Tm::Dummy(T::new_dummy()),
index_view: DFLT_INDEX_VIEW.to_string(),
#[cfg(engine)]
static_dir: "./static".to_string(),
#[cfg(any(client, doc))]
panic_handler: None,
#[cfg(any(client, doc))]
panic_handler_view: ErrorViews::unlocalized_development_default().take_panic_handler(),
#[cfg(any(client, doc))]
_marker: PhantomData,
}
}
#[cfg(any(client, doc))]
#[doc(hidden)]
fn new_wasm() -> Self {
Self {
root: "root".to_string(),
entities: HashMap::new(),
error_views: None,
pss_max_size: DFLT_PSS_MAX_SIZE,
locales: Locales {
default: "xx-XX".to_string(),
other: Vec::new(),
using_i18n: false,
},
plugins: Rc::new(Plugins::new()),
index_view: DFLT_INDEX_VIEW.to_string(),
panic_handler: None,
panic_handler_view: ErrorViews::unlocalized_development_default().take_panic_handler(),
_marker: PhantomData,
}
}
pub fn root(mut self, val: &str) -> Self {
self.root = val.to_string();
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn static_dir(mut self, val: &str) -> Self {
#[cfg(engine)]
{
self.static_dir = val.to_string();
}
self
}
pub fn templates(mut self, val: Vec<Template<G>>) -> Self {
for template in val.into_iter() {
self = self.template(template);
}
self
}
pub fn template(self, val: impl Into<Forever<Template<G>>>) -> Self {
self.template_ref(val)
}
pub fn template_ref<H: Html>(mut self, val: impl Into<Forever<Template<H>>>) -> Self {
assert_eq!(
TypeId::of::<G>(),
TypeId::of::<H>(),
"mismatched render backends"
);
let val = val.into();
let val: Forever<Template<G>> = unsafe { std::mem::transmute(val) };
let entity: Forever<Entity<G>> = match val {
Forever::Owned(capsule) => capsule.inner.into(),
Forever::StaticRef(capsule_ref) => (&capsule_ref.inner).into(),
};
let path = entity.get_path();
self.entities.insert(path, entity);
self
}
pub fn capsule<P: Clone + 'static>(self, val: impl Into<Forever<Capsule<G, P>>>) -> Self {
self.capsule_ref(val)
}
pub fn capsule_ref<H: Html, P: Clone + 'static>(
mut self,
val: impl Into<Forever<Capsule<H, P>>>,
) -> Self {
assert_eq!(
TypeId::of::<G>(),
TypeId::of::<H>(),
"mismatched render backends"
);
let val = val.into();
if val.fallback.is_none() {
panic!(
"capsule '{}' has no fallback (please register one)",
val.inner.get_path()
)
}
let val: Forever<Capsule<G, P>> = unsafe { std::mem::transmute(val) };
let entity: Forever<Entity<G>> = match val {
Forever::Owned(capsule) => capsule.inner.into(),
Forever::StaticRef(capsule_ref) => (&capsule_ref.inner).into(),
};
let path = entity.get_path();
self.entities.insert(path, entity);
self
}
#[allow(unused_mut)]
pub fn error_views(mut self, mut val: ErrorViews<G>) -> Self {
#[cfg(any(client, doc))]
{
let panic_handler = val.take_panic_handler();
self.error_views = Some(Rc::new(val));
self.panic_handler_view = panic_handler;
}
#[cfg(engine)]
{
self.error_views = Some(Arc::new(val));
}
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn global_state_creator(mut self, val: GlobalStateCreator) -> Self {
#[cfg(engine)]
{
self.global_state_creator = Arc::new(val);
}
self
}
pub fn locales(mut self, default: &str, other: &[&str]) -> Self {
self.locales = Locales {
default: default.to_string(),
other: other.iter().map(|s| s.to_string()).collect(),
using_i18n: true,
};
self
}
pub fn locales_lit(mut self, val: Locales) -> Self {
self.locales = val;
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn translations_manager(mut self, val: impl Future<Output = T> + 'static) -> Self {
#[cfg(engine)]
{
self.translations_manager = Tm::Full(Box::pin(val));
}
self
}
pub fn disable_i18n(mut self) -> Self {
self.locales = Locales {
default: "xx-XX".to_string(),
other: Vec::new(),
using_i18n: false,
};
#[cfg(engine)]
{
self.translations_manager = Tm::Dummy(T::new_dummy());
}
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn static_aliases(mut self, val: HashMap<String, String>) -> Self {
#[cfg(engine)]
{
self.static_aliases = val;
}
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn static_alias(mut self, url: &str, resource: &str) -> Self {
#[cfg(engine)]
self.static_aliases
.insert(url.to_string(), resource.to_string());
self
}
pub fn plugins(mut self, val: Plugins) -> Self {
#[cfg(any(client, doc))]
{
self.plugins = Rc::new(val);
}
#[cfg(engine)]
{
self.plugins = Arc::new(val);
}
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn mutable_store(mut self, val: M) -> Self {
#[cfg(engine)]
{
self.mutable_store = val;
}
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn immutable_store(mut self, val: ImmutableStore) -> Self {
#[cfg(engine)]
{
self.immutable_store = val;
}
self
}
pub fn index_view_str(mut self, val: &str) -> Self {
self.index_view = val.to_string();
self
}
pub fn index_view<'a>(mut self, f: impl Fn(Scope) -> View<SsrNode> + 'a) -> Self {
let html_str = sycamore::render_to_string(|cx| with_no_hydration_context(|| f(cx)));
self.index_view = html_str;
self
}
pub fn pss_max_size(mut self, val: usize) -> Self {
self.pss_max_size = val;
self
}
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn panic_handler(mut self, val: impl Fn(&PanicInfo) + Send + Sync + 'static) -> Self {
#[cfg(any(client, doc))]
{
self.panic_handler = Some(Box::new(val));
}
self
}
pub fn get_root(&self) -> Result<String, PluginError> {
let root = self
.plugins
.control_actions
.settings_actions
.set_app_root
.run((), self.plugins.get_plugin_data())?
.unwrap_or_else(|| self.root.to_string());
Ok(root)
}
#[cfg(engine)]
pub fn get_index_view_str(&self) -> String {
format!("<!DOCTYPE html>\n{}", self.index_view)
}
#[cfg(engine)]
pub(crate) async fn get_html_shell(
index_view_str: String,
root: &str,
render_cfg: &HashMap<String, String>,
plugins: &Plugins,
) -> Result<HtmlShell, PluginError> {
let mut html_shell =
HtmlShell::new(index_view_str, root, render_cfg, &get_path_prefix_server());
let shell_str = plugins
.control_actions
.settings_actions
.html_shell_actions
.set_shell
.run((), plugins.get_plugin_data())?
.unwrap_or(html_shell.shell);
html_shell.shell = shell_str;
let hsf_actions = &plugins
.functional_actions
.settings_actions
.html_shell_actions;
html_shell.head_before_boundary.push(
hsf_actions
.add_to_head_before_boundary
.run((), plugins.get_plugin_data())?
.values()
.flatten()
.cloned()
.collect(),
);
html_shell.scripts_before_boundary.push(
hsf_actions
.add_to_scripts_before_boundary
.run((), plugins.get_plugin_data())?
.values()
.flatten()
.cloned()
.collect(),
);
html_shell.head_after_boundary.push(
hsf_actions
.add_to_head_after_boundary
.run((), plugins.get_plugin_data())?
.values()
.flatten()
.cloned()
.collect(),
);
html_shell.scripts_after_boundary.push(
hsf_actions
.add_to_scripts_after_boundary
.run((), plugins.get_plugin_data())?
.values()
.flatten()
.cloned()
.collect(),
);
html_shell.before_content.push(
hsf_actions
.add_to_before_content
.run((), plugins.get_plugin_data())?
.values()
.flatten()
.cloned()
.collect(),
);
html_shell.after_content.push(
hsf_actions
.add_to_after_content
.run((), plugins.get_plugin_data())?
.values()
.flatten()
.cloned()
.collect(),
);
Ok(html_shell)
}
pub fn get_locales(&self) -> Result<Locales, PluginError> {
let locales = self.locales.clone();
let locales = self
.plugins
.control_actions
.settings_actions
.set_locales
.run(locales.clone(), self.plugins.get_plugin_data())?
.unwrap_or(locales);
Ok(locales)
}
#[cfg(engine)]
pub async fn get_translations_manager(self) -> T {
match self.translations_manager {
Tm::Dummy(tm) => tm,
Tm::Full(tm) => tm.await,
}
}
#[cfg(engine)]
pub fn get_immutable_store(&self) -> Result<ImmutableStore, PluginError> {
let immutable_store = self.immutable_store.clone();
let immutable_store = self
.plugins
.control_actions
.settings_actions
.set_immutable_store
.run(immutable_store.clone(), self.plugins.get_plugin_data())?
.unwrap_or(immutable_store);
Ok(immutable_store)
}
#[cfg(engine)]
pub fn get_static_aliases(&self) -> Result<HashMap<String, String>, PluginError> {
let mut static_aliases = self.static_aliases.clone();
let extra_static_aliases = self
.plugins
.functional_actions
.settings_actions
.add_static_aliases
.run((), self.plugins.get_plugin_data())?;
for (_plugin_name, aliases) in extra_static_aliases {
let new_aliases: HashMap<String, String> = aliases
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
static_aliases.extend(new_aliases);
}
let mut scoped_static_aliases = HashMap::new();
for (url, path) in static_aliases {
let new_path = if path.starts_with('/') {
panic!(
"it's a security risk to include absolute paths in `static_aliases` ('{}'), please make this relative to the project directory",
path
);
} else if path.starts_with("../") {
panic!("it's a security risk to include paths outside the current directory in `static_aliases` ('{}')", path);
} else {
path.to_string()
};
scoped_static_aliases.insert(url, new_path);
}
Ok(scoped_static_aliases)
}
#[cfg(any(client, doc))]
#[allow(clippy::type_complexity)]
pub fn take_panic_handlers(
&mut self,
) -> (
Option<Box<dyn Fn(&PanicInfo) + Send + Sync + 'static>>,
Arc<
dyn Fn(Scope, ClientError, ErrorContext, ErrorPosition) -> (View<SsrNode>, View<G>)
+ Send
+ Sync,
>,
) {
let panic_handler_view = std::mem::replace(
&mut self.panic_handler_view,
Arc::new(|_, _, _, _| unreachable!()),
);
let general_panic_handler = self.panic_handler.take();
(general_panic_handler, panic_handler_view)
}
}
#[component]
#[allow(non_snake_case)]
pub fn PerseusRoot<G: Html>(cx: Scope) -> View<G> {
view! { cx,
div(id = "root")
}
}
use crate::i18n::FsTranslationsManager;
use crate::stores::FsMutableStore;
pub type PerseusApp<G> = PerseusAppBase<G, FsMutableStore, FsTranslationsManager>;
pub type PerseusAppWithMutableStore<G, M> = PerseusAppBase<G, M, FsTranslationsManager>;
pub type PerseusAppWithTranslationsManager<G, T> = PerseusAppBase<G, FsMutableStore, T>;
pub type PerseusAppWithMutableStoreAndTranslationsManager<G, M, T> = PerseusAppBase<G, M, T>;