diff --git a/examples/basic/index.html b/examples/basic/index.html index dc31105f88..d9b3c52d9d 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -6,7 +6,7 @@ Perseus Starter App - +
diff --git a/examples/cli/.perseus/src/lib.rs b/examples/cli/.perseus/src/lib.rs index 29cad665f6..eeba820ca7 100644 --- a/examples/cli/.perseus/src/lib.rs +++ b/examples/cli/.perseus/src/lib.rs @@ -1,9 +1,9 @@ -use app::{get_error_pages, get_locales, get_routes, APP_ROUTE}; +use app::{get_error_pages, get_locales, get_templates_map, APP_ROOT}; use perseus::router::{RouteInfo, RouteVerdict}; -use perseus::{app_shell, detect_locale, ClientTranslationsManager, DomNode}; +use perseus::shell::get_render_cfg; +use perseus::{app_shell, create_app_route, detect_locale, ClientTranslationsManager, DomNode}; use std::cell::RefCell; use std::rc::Rc; -use sycamore::context::{ContextProvider, ContextProviderProps}; use sycamore::prelude::{template, StateHandle}; use sycamore_router::{HistoryIntegration, Router, RouterProps}; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; @@ -18,7 +18,7 @@ pub fn run() -> Result<(), JsValue> { .unwrap() .document() .unwrap() - .query_selector(APP_ROUTE) + .query_selector(APP_ROOT) .unwrap() .unwrap(); @@ -27,42 +27,42 @@ pub fn run() -> Result<(), JsValue> { Rc::new(RefCell::new(ClientTranslationsManager::new(&get_locales()))); // Get the error pages in an `Rc` so we aren't creating hundreds of them let error_pages = Rc::new(get_error_pages()); - // Get the routes in an `Rc` as well - let routes = Rc::new(get_routes::()); + + // Create the router we'll use for this app, based on the user's app definition + create_app_route! { + name => AppRoute, + // The render configuration is injected verbatim into the HTML shell, so it certainly should be present + render_cfg => get_render_cfg().expect("render configuration invalid or not injected"), + templates => get_templates_map(), + locales => get_locales() + } sycamore::render_to( || { template! { - // We provide the routes in context (can't provide them directly because of Sycamore trait constraints) - // BUG: context doesn't exist when link clicked first time, works second time... - ContextProvider(ContextProviderProps { - value: Rc::clone(&routes), - children: || template! { - Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle>| { - match route.get().as_ref() { - // Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell! - RouteVerdict::Found(RouteInfo { - path, - template_fn, - locale - }) => app_shell( - path.clone(), - template_fn.clone(), - locale.clone(), - // We give the app shell a translations manager and let it get the `Rc` itself (because it can do async safely) - Rc::clone(&translations_manager), - Rc::clone(&error_pages) - ), - // If the user is using i18n, then they'll want to detect the locale on any paths missing a locale - // Those all go to the same system that redirects to the appropriate locale - RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()), - // We handle the 404 for the user for convenience - // To get a translator here, we'd have to go async and dangerously check the URL - RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None), - } - })) + Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle>| { + match &route.get().as_ref().0 { + // Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell! + RouteVerdict::Found(RouteInfo { + path, + template, + locale + }) => app_shell( + path.clone(), + template.clone(), + locale.clone(), + // We give the app shell a translations manager and let it get the `Rc` itself (because it can do async safely) + Rc::clone(&translations_manager), + Rc::clone(&error_pages) + ), + // If the user is using i18n, then they'll want to detect the locale on any paths missing a locale + // Those all go to the same system that redirects to the appropriate locale + RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()), + // We handle the 404 for the user for convenience + // To get a translator here, we'd have to go async and dangerously check the URL + RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None), } - }) + })) } }, &root, diff --git a/examples/cli/index.html b/examples/cli/index.html index dc31105f88..d9b3c52d9d 100644 --- a/examples/cli/index.html +++ b/examples/cli/index.html @@ -6,7 +6,7 @@ Perseus Starter App - +
diff --git a/examples/cli/src/lib.rs b/examples/cli/src/lib.rs index 09bffda71c..6207e20556 100644 --- a/examples/cli/src/lib.rs +++ b/examples/cli/src/lib.rs @@ -7,8 +7,8 @@ define_app! { root: "#root", error_pages: crate::error_pages::get_error_pages(), templates: [ - "/" => crate::pages::index::get_page::(), - "/about" => crate::pages::about::get_page::() + crate::pages::index::get_page::(), + crate::pages::about::get_page::() ], locales: { default: "en-US", diff --git a/examples/i18n/src/lib.rs b/examples/i18n/src/lib.rs index 9e2f07188e..27c0ad1428 100644 --- a/examples/i18n/src/lib.rs +++ b/examples/i18n/src/lib.rs @@ -7,9 +7,8 @@ define_app! { root: "#root", error_pages: crate::error_pages::get_error_pages(), templates: [ - "/about" => crate::templates::about::get_template::(), - // Note that the index page comes last, otherwise locale detection for `/about` matches `about` as a locale - "/" => crate::templates::index::get_template::() + crate::templates::about::get_template::(), + crate::templates::index::get_template::() ], locales: { default: "en-US", diff --git a/examples/showcase/index.html b/examples/showcase/index.html index 45e62a9131..d7afe211a6 100644 --- a/examples/showcase/index.html +++ b/examples/showcase/index.html @@ -6,7 +6,7 @@ Perseus Showcase App - +
diff --git a/examples/showcase/src/lib.rs b/examples/showcase/src/lib.rs index aa84da47b9..b22f536bfa 100644 --- a/examples/showcase/src/lib.rs +++ b/examples/showcase/src/lib.rs @@ -7,15 +7,15 @@ define_app! { root: "#root", error_pages: crate::error_pages::get_error_pages(), templates: [ - "/" => crate::templates::index::get_template::(), - "/about" => crate::templates::about::get_template::(), - "/post/new" => crate::templates::new_post::get_template::(), + crate::templates::index::get_template::(), + crate::templates::about::get_template::(), + crate::templates::new_post::get_template::(), // BUG: Sycamore doesn't support dynamic paths before dynamic segments (https://github.com/sycamore-rs/sycamore/issues/228) - "/post/" => crate::templates::post::get_template::(), - "/ip" => crate::templates::ip::get_template::(), - "/time" => crate::templates::time_root::get_template::(), - "/timeisr/" => crate::templates::time::get_template::(), - "/amalgamation" => crate::templates::amalgamation::get_template::() + crate::templates::post::get_template::(), + crate::templates::ip::get_template::(), + crate::templates::time_root::get_template::(), + crate::templates::time::get_template::(), + crate::templates::amalgamation::get_template::() ], locales: { default: "en-US", diff --git a/packages/perseus-actix-web/Cargo.toml b/packages/perseus-actix-web/Cargo.toml index 0a7bc63f5e..c802324288 100644 --- a/packages/perseus-actix-web/Cargo.toml +++ b/packages/perseus-actix-web/Cargo.toml @@ -18,6 +18,7 @@ perseus = { path = "../perseus", version = "0.1.4" } actix-web = "3.3" actix-files = "0.5" urlencoding = "2.1" +serde = "1" serde_json = "1" error-chain = "0.12" futures = "0.3" diff --git a/packages/perseus-actix-web/src/configurer.rs b/packages/perseus-actix-web/src/configurer.rs index cce3dd577f..8e68d23a6c 100644 --- a/packages/perseus-actix-web/src/configurer.rs +++ b/packages/perseus-actix-web/src/configurer.rs @@ -1,8 +1,10 @@ use crate::page_data::page_data; use crate::translations::translations; use actix_files::NamedFile; -use actix_web::web; +use actix_web::{web, HttpResponse}; use perseus::{get_render_cfg, ConfigManager, Locales, SsrNode, TemplateMap, TranslationsManager}; +use std::collections::HashMap; +use std::fs; /// The options for setting up the Actix Web integration. This should be literally constructed, as nothing is optional. #[derive(Clone)] @@ -22,6 +24,17 @@ pub struct Options { pub locales: Locales, } +async fn render_conf( + render_conf: web::Data>, +) -> web::Json> { + web::Json(render_conf.get_ref().clone()) +} +/// This returns the HTML index file with the render configuration injected as a JS global variable. +async fn index(index_with_render_cfg: web::Data) -> HttpResponse { + HttpResponse::Ok() + .content_type("text/html") + .body(index_with_render_cfg.get_ref()) +} async fn js_bundle(opts: web::Data) -> std::io::Result { NamedFile::open(&opts.js_bundle) } @@ -31,9 +44,6 @@ async fn js_init(opts: web::Data) -> std::io::Result { async fn wasm_bundle(opts: web::Data) -> std::io::Result { NamedFile::open(&opts.wasm_bundle) } -async fn index(opts: web::Data) -> std::io::Result { - NamedFile::open(&opts.index) -} /// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments. pub async fn configurer( @@ -44,6 +54,18 @@ pub async fn configurer", + // It's safe to assume that something we just deserialized will serialize again in this case + &format!( + "\n", + serde_json::to_string(&render_cfg).unwrap() + ), + ); + move |cfg: &mut web::ServiceConfig| { cfg // We implant the render config in the app data for better performance, it's needed on every request @@ -51,14 +73,17 @@ pub async fn configurer), diff --git a/packages/perseus-actix-web/src/page_data.rs b/packages/perseus-actix-web/src/page_data.rs index 850ed9cf69..9f136bf292 100644 --- a/packages/perseus-actix-web/src/page_data.rs +++ b/packages/perseus-actix-web/src/page_data.rs @@ -2,18 +2,25 @@ use crate::conv_req::convert_req; use crate::Options; use actix_web::{http::StatusCode, web, HttpRequest, HttpResponse}; use perseus::{err_to_status_code, get_page, ConfigManager, TranslationsManager}; -use std::collections::HashMap; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct PageDataReq { + pub template_name: String, +} /// The handler for calls to `.perseus/page/*`. This will manage returning errors and the like. pub async fn page_data( req: HttpRequest, opts: web::Data, - render_cfg: web::Data>, config_manager: web::Data, translations_manager: web::Data, + web::Query(query_params): web::Query, ) -> HttpResponse { let templates = &opts.templates_map; let locale = req.match_info().query("locale"); + // TODO get the template name from the query + let template_name = query_params.template_name; // Check if the locale is supported if opts.locales.is_supported(locale) { let path = req.match_info().query("filename"); @@ -30,8 +37,8 @@ pub async fn page_data( let page_data = get_page( path, locale, + &template_name, http_req, - &render_cfg, templates, config_manager.get_ref(), translations_manager.get_ref(), diff --git a/packages/perseus/Cargo.toml b/packages/perseus/Cargo.toml index 4e24ccf3ae..d4dd2d31e0 100644 --- a/packages/perseus/Cargo.toml +++ b/packages/perseus/Cargo.toml @@ -31,6 +31,7 @@ http = "0.2" async-trait = "0.1" fluent-bundle = { version = "0.15", optional = true } unic-langid = { version = "0.9", optional = true } +js-sys = "0.3" [features] default = ["translator-fluent", "translator-dflt-fluent"] diff --git a/packages/perseus/src/macros.rs b/packages/perseus/src/macros.rs index 34e6bfad2a..65357a6241 100644 --- a/packages/perseus/src/macros.rs +++ b/packages/perseus/src/macros.rs @@ -104,7 +104,7 @@ macro_rules! define_app { root: $root_selector:literal, error_pages: $error_pages:expr, templates: [ - $($router_path:literal => $template:expr),+ + $($template:expr),+ ], // This deliberately enforces verbose i18n definition, and forces developers to consider i18n as integral locales: { @@ -117,20 +117,7 @@ macro_rules! define_app { $(,translations_manager: $translations_manager:expr)? } => { /// The CSS selector that will find the app root to render Perseus in. - pub const APP_ROUTE: &str = $root_selector; - - /// Gets the routes for the app in Perseus' custom abstraction over Sycamore's routing logic. This enables tight coupling of - /// the templates and the routing system. This can be used on the client or server side. - pub fn get_routes() -> $crate::router::Routes { - $crate::router::Routes::new( - vec![ - $( - ($router_path.to_string(), $template) - ),+ - ], - get_locales() - ) - } + pub const APP_ROOT: &str = $root_selector; /// Gets the config manager to use. This allows the user to conveniently test production managers in development. If nothing is /// given, the filesystem will be used. diff --git a/packages/perseus/src/router.rs b/packages/perseus/src/router.rs index ab80f9ad5b..e97c76497c 100644 --- a/packages/perseus/src/router.rs +++ b/packages/perseus/src/router.rs @@ -1,120 +1,111 @@ use crate::Locales; use crate::Template; -use std::rc::Rc; -use sycamore::context::use_context; +use crate::TemplateMap; +use std::collections::HashMap; use sycamore::prelude::GenericNode; -use sycamore_router::{Route, RoutePath, Segment}; -/// A representation of routes in a Perseus app. This is used internally to match routes. Because this can't be passed directly to -/// the `RouteVerdict`'s `match_route` function, it should be provided in context instead (through an `Rc`). -pub struct Routes { - /// The routes in the app, stored as an *ordered* list of key-value pairs, mapping routing path (e.g. `/post/`) to template. - /// These will be matched by a loop, so more specific routes should go first in the vector. Even if we're using i18n, this still - /// stores a routing path without the locale, which is added in during parsing as necessary. - routes: Vec<(Vec, Template)>, - /// Whether or not the user is using i18n, which significantly impacts how we match routes (will there be a locale in front of - /// everything). - locales: Locales, -} -impl Routes { - /// Creates a new instance of the routes. This takes a vector of key-value pairs of routing path to template functions. - pub fn new(raw_routes: Vec<(String, Template)>, locales: Locales) -> Self { - let routes: Vec<(Vec, Template)> = raw_routes - .iter() - .map(|(router_path_str_raw, template_fn)| { - // Handle the landing page (because match systems don't tolerate empty strings well) - if router_path_str_raw == "/" { - return (Vec::new(), template_fn.clone()); - } - - // Remove leading/trailing `/`s to avoid empty elements (which stuff up path matching) - let mut router_path_str = router_path_str_raw.clone(); - if router_path_str.starts_with('/') { - router_path_str.remove(0); - } - if router_path_str.ends_with('/') { - router_path_str.remove(router_path_str.len() - 1); - } +/// Determines the template to use for the given path by checking against the render configuration. This houses the central routing +/// algorithm of Perseus, which is based fully on the fact that we know about every single page except those rendered with ISR, and we +/// can infer about them based on template root path domains. If that domain system is violated, this routing algorithm will not +/// behave as expected whatsoever (as far as routing goes, it's undefined behaviour)! +pub fn get_template_for_path<'a, G: GenericNode>( + raw_path: &str, + render_cfg: &HashMap, + templates: &'a TemplateMap, +) -> Option<&'a Template> { + let mut path = raw_path; + // If the path is empty, we're looking for the special `index` page + if path.is_empty() { + path = "index"; + } - let router_path_parts = router_path_str.split('/'); - let router_path: Vec = router_path_parts - .map(|part| { - // TODO possibly use Actix Web like syntax here instead and propose to @lukechu10? - // We need to create a segment out of this part, we'll parse Sycamore's syntax - // We don't actually need Regex here, so we don't bloat with it - // If you're familiar with Sycamore's routing system, we don't need to worry about capturing these segments in Perseus because we just return the actual path directly - /* Variants (in tested order): - - segment that captures many parameters - - parameter that captures a single element - - stuff verbatim stuff - */ - if part.starts_with('<') && part.ends_with("..>") { - Segment::DynSegments - } else if part.starts_with('<') && part.ends_with('>') { - Segment::DynParam - } else { - Segment::Param(part.to_string()) - } - }) - .collect(); - // Turn the router path into a vector of `Segment`s - (router_path, template_fn.clone()) - }) - .collect(); + // Match the path to one of the templates + let mut template_name = String::new(); + // We'll try a direct match first + if let Some(template_root_path) = render_cfg.get(path) { + template_name = template_root_path.to_string(); + } + // Next, an ISR match (more complex), which we only want to run if we didn't get an exact match above + if template_name.is_empty() { + // We progressively look for more and more specificity of the path, adding each segment + // That way, we're searching forwards rather than backwards, which is more efficient + let path_segments: Vec<&str> = path.split('/').collect(); + for (idx, _) in path_segments.iter().enumerate() { + // Make a path out of this and all the previous segments + let path_to_try = path_segments[0..(idx + 1)].join("/") + "/*"; - Self { routes, locales } + // If we find something, keep going until we don't (maximise specificity) + if let Some(template_root_path) = render_cfg.get(&path_to_try) { + template_name = template_root_path.to_string(); + } else { + break; + } + } + } + // If we still have nothing, then the page doesn't exist + if template_name.is_empty() { + return None; } - /// Matches the given route to an instance of `RouteVerdict`. - pub fn match_route(&self, raw_path: &[&str]) -> RouteVerdict { - let path: Vec<&str> = raw_path.to_vec(); - let path_joined = path.join("/"); // This should not have a leading forward slash, it's used for asset fetching by the app shell - let mut verdict = RouteVerdict::NotFound; - // There are different logic chains if we're using i18n, so we fork out early - if self.locales.using_i18n { - for (segments, template_fn) in &self.routes { - let route_path_without_locale = RoutePath::new(segments.to_vec()); - let route_path_with_locale = RoutePath::new({ - let mut vec = vec![Segment::DynParam]; - vec.extend(segments.to_vec()); - vec - }); + // Get the template to use (the `Option` this returns is perfect) if it exists + templates.get(&template_name) +} - // First, we'll see if the path matches a translated route - // If that fails, we'll see if it matches an untranslated route, which becomes a locale detector - if route_path_with_locale.match_path(&path).is_some() { - verdict = RouteVerdict::Found(RouteInfo { - // The asset fetching process deals with the locale separately, and doesn't need a leading `/` - path: path[1..].to_vec().join("/"), - template_fn: template_fn.clone(), - locale: path[0].to_string(), - }); - break; - } else if route_path_without_locale.match_path(&path).is_some() { - // We've now matched that it fits without the locale, which means the user is trying to - verdict = RouteVerdict::LocaleDetection(path_joined); - break; - } - } - } else { - for (segments, template_fn) in &self.routes { - let route_path = RoutePath::new(segments.to_vec()); +/// Matches the given path to a `RouteVerdict`. This takes a `TemplateMap` to match against, the render configuration to index, and it +/// needs to know if i18n is being used. The path this takes should be raw, it may or may not have a locale, but should be split into +/// segments by `/`, with empty ones having been removed. +pub fn match_route( + path_slice: &[&str], + render_cfg: HashMap, + templates: TemplateMap, + locales: Locales, +) -> RouteVerdict { + let path_vec: Vec<&str> = path_slice.to_vec(); + let path_joined = path_vec.join("/"); // This should not have a leading forward slash, it's used for asset fetching by the app shell - // We're not using i18n, so we can just match the path directly - if route_path.match_path(&path).is_some() { - verdict = RouteVerdict::Found(RouteInfo { - path: path_joined, - template_fn: template_fn.clone(), - // Every page uses the default locale if we aren't using i18n (translators won't be used anyway) - locale: self.locales.default.to_string(), - }); - break; - } - } + let verdict; + // There are different logic chains if we're using i18n, so we fork out early + if locales.using_i18n && !path_slice.is_empty() { + let locale = path_slice[0]; + // Check if the 'locale' is supported (otherwise it may be the first section of an uni18ned route) + if locales.is_supported(locale) { + // We'll assume this has already been i18ned (if one of your routes has the same name as a supported locale, ffs) + let path_without_locale = path_slice[1..].to_vec().join("/"); + // Get the template to use + let template = get_template_for_path(&path_without_locale, &render_cfg, &templates); + verdict = match template { + Some(template) => RouteVerdict::Found(RouteInfo { + locale: locale.to_string(), + // This will be used in asset fetching from the server + path: path_without_locale, + template: template.clone(), + }), + None => RouteVerdict::NotFound, + }; + } else { + // If the locale isn't supported, we assume that it's part of a route that still needs a locale (we'll detect the user's preferred) + // This will result in a redirect, and the actual template to use will be determined after that + // We'll just pass through the path to be redirected to (after it's had a locale placed in front) + verdict = RouteVerdict::LocaleDetection(path_joined) } - - verdict + } else if locales.using_i18n { + // If we're here, then we're using i18n, but we're at the root path, which is a locale detection point + verdict = RouteVerdict::LocaleDetection(path_joined); + } else { + // Get the template to use + let template = get_template_for_path(&path_joined, &render_cfg, &templates); + verdict = match template { + Some(template) => RouteVerdict::Found(RouteInfo { + locale: locales.default.to_string(), + // This will be used in asset fetching from the server + path: path_joined, + template: template.clone(), + }), + None => RouteVerdict::NotFound, + }; } + + verdict } /// Information about a route, which, combined with error pages and a client-side translations manager, allows the initialization of @@ -122,8 +113,8 @@ impl Routes { pub struct RouteInfo { /// The actual path of the route. pub path: String, - /// The template that will render the template. The app shell will derive pros and a translator to pass to the template function. - pub template_fn: Template, + /// The template that will be used. The app shell will derive pros and a translator to pass to the template function. + pub template: Template, /// The locale for the template to be rendered in. pub locale: String, } @@ -139,11 +130,26 @@ pub enum RouteVerdict { /// The given route maps to the locale detector, which will redirect the user to the attached path (in the appropriate locale). LocaleDetection(String), } -impl Route for RouteVerdict { - fn match_route(path: &[&str]) -> Self { - // Get an instance of `Routes` by context - let routes = use_context::>>(); - // Match the path using that - routes.match_route(path) - } + +/// Creates an app-specific routing `struct`. Sycamore expects an `enum` to do this, so we create a `struct` that behaves similarly. If +/// we don't do this, we can't get the information necessary for routing into the `enum` at all (context and global variables don't suit +/// this particular case). +#[macro_export] +macro_rules! create_app_route { + { + name => $name:ident, + render_cfg => $render_cfg:expr, + templates => $templates:expr, + locales => $locales:expr + } => { + /// The route type for the app, with all routing logic inbuilt through the generation macro. + struct $name($crate::router::RouteVerdict); + impl ::sycamore_router::Route for $name { + fn match_route(path: &[&str]) -> Self { + let verdict = $crate::router::match_route(path, $render_cfg, $templates, $locales); + // BUG Sycamore doesn't call the route verdict matching logic for some reason, but we get to this point + Self(verdict) + } + } + }; } diff --git a/packages/perseus/src/serve.rs b/packages/perseus/src/serve.rs index ddfcf03dfb..1bfe750b5d 100644 --- a/packages/perseus/src/serve.rs +++ b/packages/perseus/src/serve.rs @@ -162,8 +162,8 @@ pub async fn get_page( // This must not contain the locale raw_path: &str, locale: &str, + template_name: &str, req: Request, - render_cfg: &HashMap, templates: &TemplateMap, config_manager: &impl ConfigManager, translations_manager: &impl TranslationsManager, @@ -182,39 +182,11 @@ pub async fn get_page( // Remove `/` from the path by encoding it as a URL (that's what we store) and add the locale let path_encoded = format!("{}-{}", locale, urlencoding::encode(path).to_string()); - // Match the path to one of the templates - let mut template_name = String::new(); - // We'll try a direct match first - if let Some(template_root_path) = render_cfg.get(path) { - template_name = template_root_path.to_string(); - } - // Next, an ISR match (more complex), which we only want to run if we didn't get an exact match above - if template_name.is_empty() { - // We progressively look for more and more specificity of the path, adding each segment - // That way, we're searching forwards rather than backwards, which is more efficient - let path_segments: Vec<&str> = path.split('/').collect(); - for (idx, _) in path_segments.iter().enumerate() { - // Make a path out of this and all the previous segments - let path_to_try = path_segments[0..(idx + 1)].join("/") + "/*"; - - // If we find something, keep going until we don't (maximise specificity) - if let Some(template_root_path) = render_cfg.get(&path_to_try) { - template_name = template_root_path.to_string(); - } else { - break; - } - } - } - - // If we still have nothing, then the page doesn't exist - if template_name.is_empty() { - bail!(ErrorKind::PageNotFound(path.to_string())) - } - // Get the template to use - let template = templates.get(&template_name); + let template = templates.get(template_name); let template = match template { Some(template) => template, + // This shouldn't happen because the client should already have performed checks against the render config, but it's handled anyway None => bail!(ErrorKind::PageNotFound(path.to_string())), }; diff --git a/packages/perseus/src/shell.rs b/packages/perseus/src/shell.rs index 4687ec47f1..9b1496dcee 100644 --- a/packages/perseus/src/shell.rs +++ b/packages/perseus/src/shell.rs @@ -13,7 +13,9 @@ use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{Request, RequestInit, RequestMode, Response}; -pub(crate) async fn fetch(url: &str) -> Result> { +/// Fetches the given resource. This should NOT be used by end users, but it's required by the CLI. +#[doc(hidden)] +pub async fn fetch(url: &str) -> Result> { let js_err_handler = |err: JsValue| ErrorKind::JsErr(format!("{:?}", err)); let mut opts = RequestInit::new(); opts.method("GET").mode(RequestMode::Cors); @@ -53,6 +55,27 @@ pub(crate) async fn fetch(url: &str) -> Result> { } } +/// Gets the render configuration from the JS global variable `__PERSEUS_RENDER_CFG`, which should be inlined by the server. This will +/// return `None` on any error (not found, serialization failed, etc.), which should reasonably lead to a `panic!` in the caller. +pub fn get_render_cfg() -> Option> { + let val_opt = web_sys::window().unwrap().get("__PERSEUS_RENDER_CFG"); + let js_obj = match val_opt { + Some(js_obj) => js_obj, + None => return None, + }; + // The object should only actually contain the string value that was injected + let cfg_str = match js_obj.as_string() { + Some(cfg_str) => cfg_str, + None => return None, + }; + let render_cfg = match serde_json::from_str::>(&cfg_str) { + Ok(render_cfg) => render_cfg, + Err(_) => return None, + }; + + Some(render_cfg) +} + /// The callback to a template the user must provide for error pages. This is passed the status code, the error message, the URL of the /// problematic asset, and a translator if one is available . Many error pages are generated when a translator is not available or /// couldn't be instantiated, so you'll need to rely on symbols or the like in these cases. @@ -132,7 +155,7 @@ pub fn app_shell( // Spawn a Rust futures thread in the background to fetch the static HTML/JSON wasm_bindgen_futures::spawn_local(cloned!((container) => async move { // Get the static page data - let asset_url = format!("/.perseus/page/{}/{}", locale, path.to_string()); + let asset_url = format!("/.perseus/page/{}/{}?template_name={}", locale, path.to_string(), template.get_path()); // If this doesn't exist, then it's a 404 (we went here by explicit navigation, but it may be an unservable ISR page or the like) let page_data_str = fetch(&asset_url).await; match page_data_str {