1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
#[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,
};
/// The default index view, because some simple apps won't need anything fancy
/// here. The user should be able to provide the smallest possible amount of
/// information for their app to work.
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>"#;
/// The default number of pages the page state store will allow before evicting
/// the oldest. Note: we don't allow an infinite number in development here
/// because that does actually get quite problematic after a few hours of
/// constant reloading and HSR (as in Firefox decides that opening the DevTools
/// is no longer allowed).
// TODO What's a sensible value here?
static DFLT_PSS_MAX_SIZE: usize = 25;
/// The different types of translations managers that can be stored. This allows
/// us to store dummy translations managers directly, without holding futures.
/// If this stores a full translations manager though, it will store it as a
/// `Future`, which is later evaluated.
#[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()
}
}
/// An automatically implemented trait for asynchronous functions that return
/// instances of `TranslationsManager`. This is needed so we can store the
/// 'promise' of getting a translations manager in future by executing a stored
/// asynchronous function (because we don't want to take in the actual value,
/// which would require asynchronous initialization functions, which we
/// can't have in environments like the browser).
#[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())
}
}
/// The options for constructing a Perseus app. This `struct` will tie
/// together all your code, declaring to Perseus where your templates,
/// error pages, static content, etc. are.
///
/// # Memory leaks
///
/// This `struct` internally stores all templates and capsules as static
/// references, since they will definitionally be required for the lifetime
/// of the app, and since this enables support for capsules created and
/// managed through a `lazy_static!`, a very convenient and efficient pattern.
///
/// However, this does mean that the methods on this `struct` for adding
/// templates and capsules perform `Box::leak` calls internally, creating
/// deliberate memory leaks. This would be ...
pub struct PerseusAppBase<G: Html, M: MutableStore, T: TranslationsManager> {
/// The HTML ID of the root `<div>` element into which Perseus will be
/// injected.
pub(crate) root: String,
/// A list of all the templates and capsules that the app uses.
pub(crate) entities: EntityMap<G>,
/// The app's error pages.
#[cfg(client)]
pub(crate) error_views: Option<Rc<ErrorViews<G>>>,
#[cfg(engine)]
pub(crate) error_views: Option<Arc<ErrorViews<G>>>,
/// The maximum size for the page state store.
pub(crate) pss_max_size: usize,
/// The global state creator for the app.
// This is wrapped in an `Arc` so we can pass it around on the engine-side (which is solely for
// Actix's benefit...)
#[cfg(engine)]
pub(crate) global_state_creator: Arc<GlobalStateCreator>,
/// The internationalization information for the app.
pub(crate) locales: Locales,
/// The static aliases the app serves.
#[cfg(engine)]
pub(crate) static_aliases: HashMap<String, String>,
/// The plugins the app uses.
#[cfg(engine)]
pub(crate) plugins: Arc<Plugins>,
#[cfg(client)]
pub(crate) plugins: Rc<Plugins>,
/// The app's immutable store.
#[cfg(engine)]
pub(crate) immutable_store: ImmutableStore,
/// The HTML template that'll be used to render the app into. This must be
/// static, but can be generated or sourced in any way. Note that this MUST
/// contain a `<div>` with the `id` set to whatever the value of `self.root`
/// is.
pub(crate) index_view: String,
/// The app's mutable store.
#[cfg(engine)]
pub(crate) mutable_store: M,
/// The app's translations manager, expressed as a function yielding a
/// `Future`. This is only ever needed on the server-side, and can't be set
/// up properly on the client-side because we can't use futures in the
/// app initialization in Wasm.
#[cfg(engine)]
pub(crate) translations_manager: Tm<T>,
/// The location of the directory to use for static assets that will placed
/// under the URL `/.perseus/static/`. By default, this is the `static/`
/// directory at the root of your project. Note that the directory set
/// here will only be used if it exists.
#[cfg(engine)]
pub(crate) static_dir: String,
/// A handler for panics on the browser-side.
#[cfg(any(client, doc))]
#[allow(clippy::type_complexity)] // TODO Really?
pub(crate) panic_handler: Option<Box<dyn Fn(&PanicInfo) + Send + Sync + 'static>>,
/// A duplicate of the app's error handling function intended for panic
/// handling. This must be extracted as an owned value and provided in a
/// thread-safe manner to the panic hook system.
///
/// This is in an `Arc` because panic hooks are `Fn`s, not `FnOnce`s.
#[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,
>,
// We need this on the client-side to account for the unused type parameters
#[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 {
// We have to do the commons, and then the target-gates separately (otherwise
// Rust uses the dummy methods)
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();
}
}
}
// The usual implementation in which the default mutable store is used
// We don't need to have a similar one for the default translations manager
// because things are completely generic there
impl<G: Html, T: TranslationsManager> PerseusAppBase<G, FsMutableStore, T> {
/// Creates a new instance of a Perseus app using the default
/// filesystem-based mutable store (see [`FsMutableStore`]). For most apps,
/// this will be sufficient. Note that this initializes the translations
/// manager as a dummy (see [`FsTranslationsManager`]), and
/// adds no templates or error pages.
///
/// In development, you can get away with defining no error pages, but
/// production apps (e.g. those created with `perseus deploy`) MUST set
/// their own custom error pages.
///
/// This is asynchronous because it creates a translations manager in the
/// background.
// It makes no sense to implement `Default` on this, so we silence Clippy deliberately
#[cfg(engine)]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::new_with_mutable_store(FsMutableStore::new("./dist/mutable".to_string()))
}
/// Creates a new instance of a Perseus app using the default
/// filesystem-based mutable store (see [`FsMutableStore`]). For most apps,
/// this will be sufficient. Note that this initializes the translations
/// manager as a dummy (see [`FsTranslationsManager`]), and
/// adds no templates or error pages.
///
/// In development, you can get away with defining no error pages, but
/// production apps (e.g. those created with `perseus deploy`) MUST set
/// their own custom error pages.
///
/// This is asynchronous because it creates a translations manager in the
/// background.
// It makes no sense to implement `Default` on this, so we silence Clippy deliberately
#[cfg(any(client, doc))]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::new_wasm()
}
}
// If one's using the default translations manager, caching should be handled
// automatically for them
impl<G: Html, M: MutableStore> PerseusAppBase<G, M, FsTranslationsManager> {
/// The same as `.locales_and_translations_manager()`, but this accepts a
/// literal [`Locales`] `struct`, which means this can be used when you're
/// using [`FsTranslationsManager`] but when you don't know if your app is
/// using i18n or not (almost always middleware).
pub fn locales_lit_and_translations_manager(mut self, locales: Locales) -> Self {
#[cfg(engine)]
let using_i18n = locales.using_i18n;
self.locales = locales;
// We only handle the translations manager on the server-side (it doesn't exist
// on the client-side)
#[cfg(engine)]
{
// If we're using i18n, do caching stuff
// If not, use a dummy translations manager
if using_i18n {
// By default, all translations are cached
let all_locales: Vec<String> = self
.locales
.get_all()
.iter()
// We have a `&&String` at this point, hence the double clone
.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
}
/// Sets the internationalization information for an app using the default
/// translations manager ([`FsTranslationsManager`]). This handles locale
/// caching and the like automatically for you, though you could
/// alternatively use `.locales()` and `.translations_manager()`
/// independently to customize various behaviors. This takes the same
/// arguments as `.locales()`, so the first argument is the default
/// locale (used as a fallback for users with no locale preferences set in
/// their browsers), and the second is a list of other locales supported.
///
/// If you're not using i18n, you don't need to call this function. If you
/// for some reason do have to though (e.g. overriding some other
/// preferences in middleware), use `.disable_i18n()`, not this, as
/// you're very likely to shoot yourself in the foot! (If i18n is disabled,
/// the default locale MUST be set to `xx-XX`, for example.)
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)
}
}
// The base implementation, generic over the mutable store and translations
// manager
impl<G: Html, M: MutableStore, T: TranslationsManager> PerseusAppBase<G, M, T> {
/// Creates a new instance of a Perseus app, with the default options and a
/// customizable [`MutableStore`], using the default dummy
/// [`FsTranslationsManager`] by default (though this can be changed).
#[allow(unused_variables)]
pub fn new_with_mutable_store(mutable_store: M) -> Self {
Self {
root: "root".to_string(),
// We do initialize with no templates, because an app without templates is in theory
// possible (and it's more convenient to call `.template()` for each one)
entities: HashMap::new(),
// We do offer default error views, but they'll panic if they're called for production
// building
error_views: None,
pss_max_size: DFLT_PSS_MAX_SIZE,
#[cfg(engine)]
global_state_creator: Arc::new(GlobalStateCreator::default()),
// By default, we'll disable i18n (as much as I may want more websites to support more
// languages...)
locales: Locales {
default: "xx-XX".to_string(),
other: Vec::new(),
using_i18n: false,
},
// By default, we won't serve any static content outside the `static/` directory
#[cfg(engine)]
static_aliases: HashMap::new(),
// By default, we won't use any plugins
#[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()),
// Many users won't need anything fancy in the index view, so we provide a default
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,
}
}
/// Internal function for Wasm initialization. This should never be called
/// by the user!
#[cfg(any(client, doc))]
#[doc(hidden)]
fn new_wasm() -> Self {
Self {
root: "root".to_string(),
// We do initialize with no templates, because an app without templates is in theory
// possible (and it's more convenient to call `.template()` for each one)
entities: HashMap::new(),
// We do offer default error pages, but they'll panic if they're called for production
// building
error_views: None,
pss_max_size: DFLT_PSS_MAX_SIZE,
// By default, we'll disable i18n (as much as I may want more websites to support more
// languages...)
locales: Locales {
default: "xx-XX".to_string(),
other: Vec::new(),
using_i18n: false,
},
// By default, we won't use any plugins
plugins: Rc::new(Plugins::new()),
// Many users won't need anything fancy in the index view, so we provide a default
index_view: DFLT_INDEX_VIEW.to_string(),
panic_handler: None,
panic_handler_view: ErrorViews::unlocalized_development_default().take_panic_handler(),
_marker: PhantomData,
}
}
// Setters (these all consume `self`)
/// Sets the HTML ID of the `<div>` element at which to insert Perseus.
/// In your index view, this should use [`PerseusRoot`].
///
/// *Note:* if you're using string HTML, the `<div>` with this ID MUST look
/// *exactly* like this: `<div id="some-id-here"></div>`, spacing and
/// all!
pub fn root(mut self, val: &str) -> Self {
self.root = val.to_string();
self
}
/// Sets the location of the directory storing static assets to be hosted
/// under the URL `/.perseus/static/`. By default, this is `static/` at
/// the root of your project.
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn static_dir(mut self, val: &str) -> Self {
#[cfg(engine)]
{
self.static_dir = val.to_string();
}
self
}
/// Sets all the app's templates. This takes a vector of templates.
///
/// Usually, it's preferred to run `.template()` once for each template,
/// rather than manually constructing this more inconvenient type.
pub fn templates(mut self, val: Vec<Template<G>>) -> Self {
for template in val.into_iter() {
self = self.template(template);
}
self
}
/// Adds a single new template to the app. This expects the output of
/// a function that is generic over `G: Html`. If you have something with a
/// predetermined type, like a `lazy_static!` that's using
/// `PerseusNodeType`, you should use `.template_ref()` instead. For
/// more information on the differences between the function and referrence
/// patterns, see the book.
///
/// See [`Template`] for further details.
pub fn template(self, val: impl Into<Forever<Template<G>>>) -> Self {
self.template_ref(val)
}
/// Adds a single new template to the app. This can accept either an owned
/// [`Template`] or a static reference to one, as might be created by a
/// `lazy_static!`. The latter would force you to specify the rendering
/// backend type (`G`) manually, using a smart alias like
/// `PerseusNodeType`. This method performs internal type coercions to make
/// statics work neatly.
///
/// If your templates come from functions like `get_template`, that are
/// generic over `G: Html`, you can use `.template()`, to avoid having
/// to specify `::<G>` manually.
///
/// See [`Template`] for further details, and the book for further details
/// on the differences between the function and reference patterns.
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();
// SAFETY: We asserted that `G == H` above.
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
}
// TODO
// /// Sets all the app's capsules. This takes a vector of capsules.
// ///
// /// Usually, it's preferred to run `.capsule()` once for each capsule,
// /// rather than manually constructing this more inconvenient type.
// pub fn capsules(mut self, val: Vec<Capsule<G>>) -> Self {
// for capsule in val.into_iter() {
// self = self.capsule(capsule);
// }
// self
// }
/// Adds a single new capsule to the app. Like `.template()`, this expects
/// the output of a function that is generic over `G: Html`. If you have
/// something with a predetermined type, like a `lazy_static!` that's
/// using `PerseusNodeType`, you should use `.capsule_ref()`
/// instead. For more information on the differences between the function
/// and reference patterns, see the book.
///
/// See [`Capsule`] for further details.
pub fn capsule<P: Clone + 'static>(self, val: impl Into<Forever<Capsule<G, P>>>) -> Self {
self.capsule_ref(val)
}
/// Adds a single new capsule to the app. This behaves like
/// `.template_ref()`, but for capsules.
///
/// See [`Capsule`] for further details.
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();
// Enforce that capsules must have defined fallbacks
if val.fallback.is_none() {
panic!(
"capsule '{}' has no fallback (please register one)",
val.inner.get_path()
)
}
// SAFETY: We asserted that `G == H` above.
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
}
/// Sets the app's error views. See [`ErrorViews`] for further details.
// Internally, this will extract a copy of the main handler for panic
// usage. Note that the default value of this is extracted from the default
// error views.
#[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
}
/// Sets the app's [`GlobalStateCreator`].
#[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
}
/// Sets the locales information for the app. The first argument is the
/// default locale (used as a fallback for users with no locale preferences
/// set in their browsers), and the second is a list of other locales
/// supported.
///
/// Note that this does not update the translations manager, which must be
/// done separately (if you're using [`FsTranslationsManager`], the default,
/// you can use `.locales_and_translations_manager()` to set both at
/// once).
///
/// If you're not using i18n, you don't need to call this function. If you
/// for some reason do have to though (e.g. overriding some other
/// preferences in middleware), use `.disable_i18n()`, not this, as
/// you're very likely to shoot yourself in the foot! (If i18n is disabled,
/// the default locale MUST be set to `xx-XX`, for example.)
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
}
/// Sets the locales information directly based on an instance of
/// [`Locales`]. Usually, end users will use `.locales()` instead for a
/// friendlier interface.
pub fn locales_lit(mut self, val: Locales) -> Self {
self.locales = val;
self
}
/// Sets the translations manager. If you're using the default translations
/// manager ([`FsTranslationsManager`]), you can use
/// `.locales_and_translations_manager()` to set this automatically
/// based on the locales information. This takes a `Future<Output = T>`,
/// where `T` is your translations manager's type.
///
/// The reason that this takes a `Future` is to avoid the use of `.await` in
/// your app definition code, which must be synchronous due to constraints
/// of Perseus' browser-side systems. When your code is run on the
/// server, the `Future` will be `.await`ed on, but in Wasm, it will be
/// discarded and ignored, since the translations manager isn't needed in
/// Wasm.
///
/// This is generally intended for use with custom translations manager or
/// specific use-cases with the default (mostly to do with custom caching
/// behavior).
#[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
}
/// Explicitly disables internationalization. You shouldn't ever need to
/// call this, as it's the default, but you may want to if you're writing
/// middleware that doesn't support i18n.
pub fn disable_i18n(mut self) -> Self {
self.locales = Locales {
default: "xx-XX".to_string(),
other: Vec::new(),
using_i18n: false,
};
// All translations manager must implement this function, which is designed for
// this exact purpose
#[cfg(engine)]
{
self.translations_manager = Tm::Dummy(T::new_dummy());
}
self
}
/// Sets all the app's static aliases. This takes a map of URLs (e.g.
/// `/file`) to resource paths, relative to the project directory (e.g.
/// `style.css`). Generally, calling `.static_alias()` many times is
/// preferred.
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn static_aliases(mut self, val: HashMap<String, String>) -> Self {
#[cfg(engine)]
{
self.static_aliases = val;
}
self
}
/// Adds a single static alias. This takes a URL path (e.g. `/file`)
/// followed by a path to a resource (which must be within the project
/// directory, e.g. `style.css`).
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn static_alias(mut self, url: &str, resource: &str) -> Self {
#[cfg(engine)]
// We don't elaborate the alias to an actual filesystem path until the getter
self.static_aliases
.insert(url.to_string(), resource.to_string());
self
}
/// Sets the plugins that the app will use. See [`Plugins`] for
/// further details.
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
}
/// Sets the [`MutableStore`] for the app to use, which you would change for
/// some production server environments if you wanted to store build
/// artifacts that can change at runtime in a place other than on the
/// filesystem (created for serverless functions specifically).
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn mutable_store(mut self, val: M) -> Self {
#[cfg(engine)]
{
self.mutable_store = val;
}
self
}
/// Sets the [`ImmutableStore`] for the app to use. You should almost never
/// need to change this unless you're not working with the CLI.
#[allow(unused_variables)]
#[allow(unused_mut)]
pub fn immutable_store(mut self, val: ImmutableStore) -> Self {
#[cfg(engine)]
{
self.immutable_store = val;
}
self
}
/// Sets the index view as a string. This should be used if you're using an
/// `index.html` file or the like.
///
/// Note: if possible, you should switch to using `.index_view()`, which
/// uses a Sycamore view rather than an HTML string.
pub fn index_view_str(mut self, val: &str) -> Self {
self.index_view = val.to_string();
self
}
/// Sets the index view using a Sycamore view, which avoids the need to
/// write any HTML by hand whatsoever. Note that this must contain a
/// `<head>` and `<body>` at a minimum.
///
/// Warning: this view can't be reactive (yet). It will be rendered to a
/// static string, which won't be hydrated.
// The lifetime of the provided function doesn't need to be static, because we
// render using it and then we're done with it
pub fn index_view<'a>(mut self, f: impl Fn(Scope) -> View<SsrNode> + 'a) -> Self {
// We need to render the index view without any hydration IDs (which would break
// the HTML shell's interpolation mechanisms)
let html_str = sycamore::render_to_string(|cx| with_no_hydration_context(|| f(cx)));
self.index_view = html_str;
self
}
/// Sets the maximum number of pages that can have their states stored in
/// the page state store before the oldest will be evicted. If your app is
/// taking up a substantial amount of memory in the browser because your
/// page states are fairly large, making this smaller may help.
///
/// By default, this is set to 25. Higher values may lead to memory
/// difficulties in both development and production, and the poor user
/// experience of a browser that's substantially slowed down.
///
/// WARNING: any setting applied here will impact HSR in development! (E.g.
/// setting this to 1 would mean your position would only be
/// saved for the most recent page.)
pub fn pss_max_size(mut self, val: usize) -> Self {
self.pss_max_size = val;
self
}
/// Sets the browser-side panic handler for your app. This is a function
/// that will be executed if your app panics (which should never be caused
/// by Perseus unless something is seriously wrong, it's much more likely
/// to come from your code, or third-party code).
///
/// In the case of a panic, Perseus will automatically try to render a full
/// popup error to explain the situation to the user before terminating,
/// but, since it's impossible to use the plugins in the case of a
/// panic, this function is provided as an alternative in case you want
/// to perform other work, like sending a crash report.
///
/// This function **must not** panic itself, because Perseus renders the
/// message *after* your handler is executed. If it panics, that popup
/// will never get to the user, leading to very poor UX. That said,
/// don't stress about calling things like `web_sys::window().unwrap()`,
/// because, if that fails, then trying to render a popup will
/// *definitely* fail anyway. Perseus will attempt to write an error
/// message to the console before this, just in case anything panics.
///
/// Note that there is no access within this function to Sycamore, page
/// state, global state, or translators. Assume that your code has
/// completely imploded when you write this function. Anything more advanced
/// should be left to your error views system, when it handles
/// `ClientError::Panic`.
///
/// This has no default value.
#[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
}
// Getters
/// Gets the HTML ID of the `<div>` at which to insert Perseus.
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)
}
// /// Gets the directory containing static assets to be hosted under the URL
// /// `/.perseus/static/`.
// // TODO Plugin action for this?
// #[cfg(engine)]
// pub fn get_static_dir(&self) -> String {
// self.static_dir.to_string()
// }
/// Gets the index view as a string, without generating an HTML shell (pass
/// this into `::get_html_shell()` to do that).
///
/// Note that this automatically adds `<!DOCTYPE html>` to the start of the
/// HTML shell produced, which can only be overridden with a control plugin
/// (though you should really never do this in Perseus, which targets
/// HTML on the web).
#[cfg(engine)]
pub fn get_index_view_str(&self) -> String {
// We have to add an HTML document type declaration, otherwise the browser could
// think it's literally anything! (This shouldn't be a problem, but it could be
// in 100 years...)
format!("<!DOCTYPE html>\n{}", self.index_view)
}
/// Gets an HTML shell from an index view string. This is broken out so that
/// it can be executed after the app has been built (which requires getting
/// the translations manager, consuming `self`). As inconvenient as this
/// is, it's necessitated, otherwise exporting would try to access the built
/// app before it had actually been built.
#[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> {
// Construct an HTML shell
let mut html_shell =
HtmlShell::new(index_view_str, root, render_cfg, &get_path_prefix_server());
// Apply the myriad plugin actions to the HTML shell (replacing the whole thing
// first if need be)
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;
// For convenience, we alias the HTML shell functional actions
let hsf_actions = &plugins
.functional_actions
.settings_actions
.html_shell_actions;
// These all return `Vec<String>`, so the code is almost identical for all the
// places for flexible interpolation
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)
}
// /// Gets the map of entities (i.e. templates and capsules combined).
// pub fn get_entities_map(&self) -> EntityMap<G> {
// // This is cheap to clone
// self.entities.clone()
// }
// /// Gets the [`ErrorViews`] used in the app. This returns an `Rc`.
// #[cfg(any(client, doc))]
// pub fn get_error_views(&self) -> Rc<ErrorViews<G>> {
// self.error_views.clone()
// }
// /// Gets the [`ErrorViews`] used in the app. This returns an `Arc`.
// #[cfg(engine)]
// pub fn get_atomic_error_views(&self) -> Arc<ErrorViews<G>> {
// self.error_views.clone()
// }
// /// Gets the maximum number of pages that can be stored in the page state
// /// store before the oldest are evicted.
// pub fn get_pss_max_size(&self) -> usize {
// self.pss_max_size
// }
// /// Gets the [`GlobalStateCreator`]. This can't be directly modified by
// /// plugins because of reactive type complexities.
// #[cfg(engine)]
// pub fn get_global_state_creator(&self) -> Arc<GlobalStateCreator> {
// self.global_state_creator.clone()
// }
/// Gets the locales information.
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)
}
/// Gets the server-side [`TranslationsManager`]. Like the mutable store,
/// this can't be modified by plugins due to trait complexities.
///
/// This involves evaluating the future stored for the translations manager,
/// and so this consumes `self`.
#[cfg(engine)]
pub async fn get_translations_manager(self) -> T {
match self.translations_manager {
Tm::Dummy(tm) => tm,
Tm::Full(tm) => tm.await,
}
}
/// Gets the [`ImmutableStore`].
#[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)
}
// /// Gets the [`MutableStore`]. This can't be modified by plugins due to
// /// trait complexities, so plugins should instead expose a function that
// /// the user can use to manually set it.
// #[cfg(engine)]
// pub fn get_mutable_store(&self) -> M {
// self.mutable_store.clone()
// }
// /// Gets the plugins registered for the app.
// #[cfg(engine)]
// pub fn get_plugins(&self) -> Arc<Plugins> {
// self.plugins.clone()
// }
// /// Gets the plugins registered for the app.
// #[cfg(any(client, doc))]
// pub fn get_plugins(&self) -> Rc<Plugins> {
// self.plugins.clone()
// }
/// Gets the static aliases. This will check all provided resource paths to
/// ensure they don't reference files outside the project directory, due to
/// potential security risks in production (we don't want to
/// accidentally serve an arbitrary in a production environment where a path
/// may point to somewhere evil, like an alias to `/etc/passwd`).
#[cfg(engine)]
pub fn get_static_aliases(&self) -> Result<HashMap<String, String>, PluginError> {
let mut static_aliases = self.static_aliases.clone();
// This will return a map of plugin name to another map of static aliases that
// that plugin produced
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 {
// We need to move this from being scoped to the app to being scoped for
// `.perseus/` TODO Make sure this works properly on Windows (seems
// to..)
let new_path = if path.starts_with('/') {
// Absolute paths are a security risk and are disallowed
// The reason for this is that they could point somewhere completely different
// on a production server (like an alias to `/etc/passwd`)
// Allowing these would also inevitably cause head-scratching in production,
// it's much easier to disallow these
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("../") {
// Anything outside this directory is a security risk as well
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)
}
/// Takes the user-set panic handlers out and returns them as an owned
/// tuple, allowing them to be used in an actual panic hook.
///
/// # Future panics
/// If this is called more than once, the view panic handler will panic when
/// called.
#[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)
}
}
/// The component that represents the entrypoint at which Perseus will inject
/// itself. You can use this with the `.index_view()` method of
/// [`PerseusAppBase`] to avoid having to create the entrypoint `<div>`
/// manually.
#[component]
#[allow(non_snake_case)]
pub fn PerseusRoot<G: Html>(cx: Scope) -> View<G> {
view! { cx,
// Since we render the index view with no hydration IDs, this conforms
// to the expectations of the HTML shell
div(id = "root")
}
}
use crate::i18n::FsTranslationsManager;
use crate::stores::FsMutableStore;
/// An alias for the usual kind of Perseus app, which uses the filesystem-based
/// mutable store and translations manager. See [`PerseusAppBase`] for details.
pub type PerseusApp<G> = PerseusAppBase<G, FsMutableStore, FsTranslationsManager>;
/// An alias for a Perseus app that uses a custom mutable store type. See
/// [`PerseusAppBase`] for details.
pub type PerseusAppWithMutableStore<G, M> = PerseusAppBase<G, M, FsTranslationsManager>;
/// An alias for a Perseus app that uses a custom translations manager type. See
/// [`PerseusAppBase`] for details.
pub type PerseusAppWithTranslationsManager<G, T> = PerseusAppBase<G, FsMutableStore, T>;
/// An alias for a fully customizable Perseus app that can accept a custom
/// mutable store and a custom translations manager. Alternatively, you could
/// just use [`PerseusAppBase`] directly.
pub type PerseusAppWithMutableStoreAndTranslationsManager<G, M, T> = PerseusAppBase<G, M, T>;