Skip to content

Commit 78688c1

Browse files
committed
feat(routing): ✨ switched to template-based routing
This simplifies routing significantly and couples it fully to the templates system. BREAKING CHANGE: `define_app!` no longer takes routing paths, just templates Closes #12.
1 parent f7ec1aa commit 78688c1

File tree

15 files changed

+241
-220
lines changed

15 files changed

+241
-220
lines changed

examples/basic/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Perseus Starter App</title>
88
<!-- Importing this runs Perseus -->
9-
<script src="/.perseus/bundle.js" defer></script>
9+
<script type="module" src="/.perseus/main.js" defer></script>
1010
</head>
1111
<body>
1212
<div id="root"></div>

examples/cli/.perseus/src/lib.rs

+35-35
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use app::{get_error_pages, get_locales, get_routes, APP_ROUTE};
1+
use app::{get_error_pages, get_locales, get_templates_map, APP_ROOT};
22
use perseus::router::{RouteInfo, RouteVerdict};
3-
use perseus::{app_shell, detect_locale, ClientTranslationsManager, DomNode};
3+
use perseus::shell::get_render_cfg;
4+
use perseus::{app_shell, create_app_route, detect_locale, ClientTranslationsManager, DomNode};
45
use std::cell::RefCell;
56
use std::rc::Rc;
6-
use sycamore::context::{ContextProvider, ContextProviderProps};
77
use sycamore::prelude::{template, StateHandle};
88
use sycamore_router::{HistoryIntegration, Router, RouterProps};
99
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
@@ -18,7 +18,7 @@ pub fn run() -> Result<(), JsValue> {
1818
.unwrap()
1919
.document()
2020
.unwrap()
21-
.query_selector(APP_ROUTE)
21+
.query_selector(APP_ROOT)
2222
.unwrap()
2323
.unwrap();
2424

@@ -27,42 +27,42 @@ pub fn run() -> Result<(), JsValue> {
2727
Rc::new(RefCell::new(ClientTranslationsManager::new(&get_locales())));
2828
// Get the error pages in an `Rc` so we aren't creating hundreds of them
2929
let error_pages = Rc::new(get_error_pages());
30-
// Get the routes in an `Rc` as well
31-
let routes = Rc::new(get_routes::<DomNode>());
30+
31+
// Create the router we'll use for this app, based on the user's app definition
32+
create_app_route! {
33+
name => AppRoute,
34+
// The render configuration is injected verbatim into the HTML shell, so it certainly should be present
35+
render_cfg => get_render_cfg().expect("render configuration invalid or not injected"),
36+
templates => get_templates_map(),
37+
locales => get_locales()
38+
}
3239

3340
sycamore::render_to(
3441
|| {
3542
template! {
36-
// We provide the routes in context (can't provide them directly because of Sycamore trait constraints)
37-
// BUG: context doesn't exist when link clicked first time, works second time...
38-
ContextProvider(ContextProviderProps {
39-
value: Rc::clone(&routes),
40-
children: || template! {
41-
Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle<RouteVerdict<DomNode>>| {
42-
match route.get().as_ref() {
43-
// Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell!
44-
RouteVerdict::Found(RouteInfo {
45-
path,
46-
template_fn,
47-
locale
48-
}) => app_shell(
49-
path.clone(),
50-
template_fn.clone(),
51-
locale.clone(),
52-
// We give the app shell a translations manager and let it get the `Rc<Translator>` itself (because it can do async safely)
53-
Rc::clone(&translations_manager),
54-
Rc::clone(&error_pages)
55-
),
56-
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
57-
// Those all go to the same system that redirects to the appropriate locale
58-
RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()),
59-
// We handle the 404 for the user for convenience
60-
// To get a translator here, we'd have to go async and dangerously check the URL
61-
RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None),
62-
}
63-
}))
43+
Router(RouterProps::new(HistoryIntegration::new(), move |route: StateHandle<AppRoute<DomNode>>| {
44+
match &route.get().as_ref().0 {
45+
// Perseus' custom routing system is tightly coupled to the template system, and returns exactly what we need for the app shell!
46+
RouteVerdict::Found(RouteInfo {
47+
path,
48+
template,
49+
locale
50+
}) => app_shell(
51+
path.clone(),
52+
template.clone(),
53+
locale.clone(),
54+
// We give the app shell a translations manager and let it get the `Rc<Translator>` itself (because it can do async safely)
55+
Rc::clone(&translations_manager),
56+
Rc::clone(&error_pages)
57+
),
58+
// If the user is using i18n, then they'll want to detect the locale on any paths missing a locale
59+
// Those all go to the same system that redirects to the appropriate locale
60+
RouteVerdict::LocaleDetection(path) => detect_locale(path.clone(), get_locales()),
61+
// We handle the 404 for the user for convenience
62+
// To get a translator here, we'd have to go async and dangerously check the URL
63+
RouteVerdict::NotFound => get_error_pages().get_template_for_page("", &404, "not found", None),
6464
}
65-
})
65+
}))
6666
}
6767
},
6868
&root,

examples/cli/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Perseus Starter App</title>
88
<!-- Importing this runs Perseus -->
9-
<script src="/.perseus/bundle.js" defer></script>
9+
<script type="module" src="/.perseus/main.js" defer></script>
1010
</head>
1111
<body>
1212
<div id="root"></div>

examples/cli/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ define_app! {
77
root: "#root",
88
error_pages: crate::error_pages::get_error_pages(),
99
templates: [
10-
"/" => crate::pages::index::get_page::<G>(),
11-
"/about" => crate::pages::about::get_page::<G>()
10+
crate::pages::index::get_page::<G>(),
11+
crate::pages::about::get_page::<G>()
1212
],
1313
locales: {
1414
default: "en-US",

examples/i18n/src/lib.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ define_app! {
77
root: "#root",
88
error_pages: crate::error_pages::get_error_pages(),
99
templates: [
10-
"/about" => crate::templates::about::get_template::<G>(),
11-
// Note that the index page comes last, otherwise locale detection for `/about` matches `about` as a locale
12-
"/" => crate::templates::index::get_template::<G>()
10+
crate::templates::about::get_template::<G>(),
11+
crate::templates::index::get_template::<G>()
1312
],
1413
locales: {
1514
default: "en-US",

examples/showcase/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Perseus Showcase App</title>
88
<!-- Importing this runs Perseus -->
9-
<script src="/.perseus/bundle.js" defer></script>
9+
<script type="module" src="/.perseus/main.js" defer></script>
1010
</head>
1111
<body>
1212
<div id="root"></div>

examples/showcase/src/lib.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ define_app! {
77
root: "#root",
88
error_pages: crate::error_pages::get_error_pages(),
99
templates: [
10-
"/" => crate::templates::index::get_template::<G>(),
11-
"/about" => crate::templates::about::get_template::<G>(),
12-
"/post/new" => crate::templates::new_post::get_template::<G>(),
10+
crate::templates::index::get_template::<G>(),
11+
crate::templates::about::get_template::<G>(),
12+
crate::templates::new_post::get_template::<G>(),
1313
// BUG: Sycamore doesn't support dynamic paths before dynamic segments (https://github.com/sycamore-rs/sycamore/issues/228)
14-
"/post/<slug..>" => crate::templates::post::get_template::<G>(),
15-
"/ip" => crate::templates::ip::get_template::<G>(),
16-
"/time" => crate::templates::time_root::get_template::<G>(),
17-
"/timeisr/<slug>" => crate::templates::time::get_template::<G>(),
18-
"/amalgamation" => crate::templates::amalgamation::get_template::<G>()
14+
crate::templates::post::get_template::<G>(),
15+
crate::templates::ip::get_template::<G>(),
16+
crate::templates::time_root::get_template::<G>(),
17+
crate::templates::time::get_template::<G>(),
18+
crate::templates::amalgamation::get_template::<G>()
1919
],
2020
locales: {
2121
default: "en-US",

packages/perseus-actix-web/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ perseus = { path = "../perseus", version = "0.1.4" }
1818
actix-web = "3.3"
1919
actix-files = "0.5"
2020
urlencoding = "2.1"
21+
serde = "1"
2122
serde_json = "1"
2223
error-chain = "0.12"
2324
futures = "0.3"

packages/perseus-actix-web/src/configurer.rs

+29-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::page_data::page_data;
22
use crate::translations::translations;
33
use actix_files::NamedFile;
4-
use actix_web::web;
4+
use actix_web::{web, HttpResponse};
55
use perseus::{get_render_cfg, ConfigManager, Locales, SsrNode, TemplateMap, TranslationsManager};
6+
use std::collections::HashMap;
7+
use std::fs;
68

79
/// The options for setting up the Actix Web integration. This should be literally constructed, as nothing is optional.
810
#[derive(Clone)]
@@ -22,6 +24,17 @@ pub struct Options {
2224
pub locales: Locales,
2325
}
2426

27+
async fn render_conf(
28+
render_conf: web::Data<HashMap<String, String>>,
29+
) -> web::Json<HashMap<String, String>> {
30+
web::Json(render_conf.get_ref().clone())
31+
}
32+
/// This returns the HTML index file with the render configuration injected as a JS global variable.
33+
async fn index(index_with_render_cfg: web::Data<String>) -> HttpResponse {
34+
HttpResponse::Ok()
35+
.content_type("text/html")
36+
.body(index_with_render_cfg.get_ref())
37+
}
2538
async fn js_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
2639
NamedFile::open(&opts.js_bundle)
2740
}
@@ -31,9 +44,6 @@ async fn js_init(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
3144
async fn wasm_bundle(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
3245
NamedFile::open(&opts.wasm_bundle)
3346
}
34-
async fn index(opts: web::Data<Options>) -> std::io::Result<NamedFile> {
35-
NamedFile::open(&opts.index)
36-
}
3747

3848
/// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments.
3949
pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'static>(
@@ -44,21 +54,36 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
4454
let render_cfg = get_render_cfg(&config_manager)
4555
.await
4656
.expect("Couldn't get render configuration!");
57+
// Get the index file and inject the render configuration into ahead of time
58+
// We do this by injecting a script that defines the render config as a global variable, which we put just before the close of the head
59+
let index_file = fs::read_to_string(&opts.index).expect("Couldn't get HTML index file!");
60+
let index_with_render_cfg = index_file.replace(
61+
"</head>",
62+
// It's safe to assume that something we just deserialized will serialize again in this case
63+
&format!(
64+
"<script>window.__PERSEUS_RENDER_CFG = '{}';</script>\n</head>",
65+
serde_json::to_string(&render_cfg).unwrap()
66+
),
67+
);
68+
4769
move |cfg: &mut web::ServiceConfig| {
4870
cfg
4971
// We implant the render config in the app data for better performance, it's needed on every request
5072
.data(render_cfg.clone())
5173
.data(config_manager.clone())
5274
.data(translations_manager.clone())
5375
.data(opts.clone())
76+
.data(index_with_render_cfg.clone())
5477
// TODO chunk JS and Wasm bundles
5578
// These allow getting the basic app code (not including the static data)
5679
// This contains everything in the spirit of a pseudo-SPA
5780
.route("/.perseus/main.js", web::get().to(js_init))
5881
.route("/.perseus/bundle.js", web::get().to(js_bundle))
5982
.route("/.perseus/bundle.wasm", web::get().to(wasm_bundle))
83+
.route("/.perseus/render_conf.json", web::get().to(render_conf))
6084
// This allows getting the static HTML/JSON of a page
6185
// We stream both together in a single JSON object so SSR works (otherwise we'd have request IDs and weird caching...)
86+
// A request to this should also provide the template name (routing should only be done once on the client) as a query parameter
6287
.route(
6388
"/.perseus/page/{locale}/{filename:.*}",
6489
web::get().to(page_data::<C, T>),

packages/perseus-actix-web/src/page_data.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@ use crate::conv_req::convert_req;
22
use crate::Options;
33
use actix_web::{http::StatusCode, web, HttpRequest, HttpResponse};
44
use perseus::{err_to_status_code, get_page, ConfigManager, TranslationsManager};
5-
use std::collections::HashMap;
5+
use serde::Deserialize;
6+
7+
#[derive(Deserialize)]
8+
pub struct PageDataReq {
9+
pub template_name: String,
10+
}
611

712
/// The handler for calls to `.perseus/page/*`. This will manage returning errors and the like.
813
pub async fn page_data<C: ConfigManager, T: TranslationsManager>(
914
req: HttpRequest,
1015
opts: web::Data<Options>,
11-
render_cfg: web::Data<HashMap<String, String>>,
1216
config_manager: web::Data<C>,
1317
translations_manager: web::Data<T>,
18+
web::Query(query_params): web::Query<PageDataReq>,
1419
) -> HttpResponse {
1520
let templates = &opts.templates_map;
1621
let locale = req.match_info().query("locale");
22+
// TODO get the template name from the query
23+
let template_name = query_params.template_name;
1724
// Check if the locale is supported
1825
if opts.locales.is_supported(locale) {
1926
let path = req.match_info().query("filename");
@@ -30,8 +37,8 @@ pub async fn page_data<C: ConfigManager, T: TranslationsManager>(
3037
let page_data = get_page(
3138
path,
3239
locale,
40+
&template_name,
3341
http_req,
34-
&render_cfg,
3542
templates,
3643
config_manager.get_ref(),
3744
translations_manager.get_ref(),

packages/perseus/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ http = "0.2"
3131
async-trait = "0.1"
3232
fluent-bundle = { version = "0.15", optional = true }
3333
unic-langid = { version = "0.9", optional = true }
34+
js-sys = "0.3"
3435

3536
[features]
3637
default = ["translator-fluent", "translator-dflt-fluent"]

packages/perseus/src/macros.rs

+2-15
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ macro_rules! define_app {
104104
root: $root_selector:literal,
105105
error_pages: $error_pages:expr,
106106
templates: [
107-
$($router_path:literal => $template:expr),+
107+
$($template:expr),+
108108
],
109109
// This deliberately enforces verbose i18n definition, and forces developers to consider i18n as integral
110110
locales: {
@@ -117,20 +117,7 @@ macro_rules! define_app {
117117
$(,translations_manager: $translations_manager:expr)?
118118
} => {
119119
/// The CSS selector that will find the app root to render Perseus in.
120-
pub const APP_ROUTE: &str = $root_selector;
121-
122-
/// Gets the routes for the app in Perseus' custom abstraction over Sycamore's routing logic. This enables tight coupling of
123-
/// the templates and the routing system. This can be used on the client or server side.
124-
pub fn get_routes<G: $crate::GenericNode>() -> $crate::router::Routes<G> {
125-
$crate::router::Routes::new(
126-
vec![
127-
$(
128-
($router_path.to_string(), $template)
129-
),+
130-
],
131-
get_locales()
132-
)
133-
}
120+
pub const APP_ROOT: &str = $root_selector;
134121

135122
/// Gets the config manager to use. This allows the user to conveniently test production managers in development. If nothing is
136123
/// given, the filesystem will be used.

0 commit comments

Comments
 (0)