Skip to content

Commit

Permalink
feat: redesigned app shell with support for hydration (#177)
Browse files Browse the repository at this point in the history
* feat: made perseus router hydrate on properly on initial page loads

This improves performance substantially, especially on mobile and
bandwidth-restricted devices, since interactivity requires no external
network requests for fetching translations.

* refactor: cleaned up unused imports

Also moved some internal functions into more sensible places now that
the app shell has been forked into subsequent/initial loads.

* feat: added support for route announcer with new router

This doesn't work yet, because the router state is still in limbo.

* fix: prevented `/SERVER` url from appearing in dev

This was possible if HSR took effect before interactivity.

* fix: fixed router state with new router systems

* fix(i18n): fixed locale redirection

* chore: removed old dev logs

* fix: fixed examples and appeased clippy

* test: fixed checkpoints in integration tests

* test: fixed `router_entry` checkpoints

BREAKING CHANGE:
- Added `ErrorLoaded { path }` case to `RouterLoadState` (which must now be matched)
- Removed `page_visible testing` checkpoint (use `page_interactive` instead)
- `router_entry checkpoint` is now only fired on subsequent loads
  • Loading branch information
arctic-hen7 authored Aug 19, 2022
1 parent 5d75f9d commit d407727
Show file tree
Hide file tree
Showing 37 changed files with 1,191 additions and 859 deletions.
4 changes: 2 additions & 2 deletions examples/core/basic/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {

// The greeting was passed through using build state
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Hello World!");
// For some reason, retrieving the inner HTML or text of a `<title>` doens't
Expand All @@ -23,7 +23,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Make sure the hardcoded text there exists
let text = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(text, "About.");
Expand Down
4 changes: 2 additions & 2 deletions examples/core/custom_server/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
assert!(url.as_ref().starts_with("http://localhost:8080"));

wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Hello World!");

Expand All @@ -18,7 +18,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Make sure the hardcoded text there exists
let text = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(text, "About.");
Expand Down
8 changes: 4 additions & 4 deletions examples/core/freezing_and_thawing/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080"));
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);

// Check the initials
let mut page_state = c.find(Locator::Id("page_state")).await?;
Expand All @@ -31,7 +31,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
// Switch to the about page to demonstrate route restoration as well
c.find(Locator::Id("about-link")).await?.click().await?;
c.current_url().await?;
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Now press the freeze button and get the frozen app
c.find(Locator::Id("freeze_button")).await?.click().await?;
let frozen_app = c.find(Locator::Id("frozen_app")).await?.text().await?;
Expand All @@ -43,7 +43,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080"));
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
// Check that the empty initials are restored
assert_eq!(
c.find(Locator::Id("page_state")).await?.text().await?,
Expand Down Expand Up @@ -74,7 +74,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
// And go back to the index page to check everything fully
c.find(Locator::Id("index-link")).await?.click().await?;
c.current_url().await?;
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Verify that everything has been correctly restored
assert_eq!(
c.find(Locator::Id("page_state")).await?.text().await?,
Expand Down
4 changes: 2 additions & 2 deletions examples/core/global_state/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080"));
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);

// The initial text should be "Hello World!"
let mut greeting = c.find(Locator::Css("p")).await?;
Expand All @@ -26,7 +26,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);

let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Hello World! Extra text.");
Expand Down
8 changes: 4 additions & 4 deletions examples/core/idb_freezing/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080"));
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);

// Check the initials
let mut page_state = c.find(Locator::Id("page_state")).await?;
Expand All @@ -31,7 +31,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
// Switch to the about page to demonstrate route restoration as well
c.find(Locator::Id("about-link")).await?.click().await?;
c.current_url().await?;
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Now press the freeze button (this will save to IDB, so we don't have to worry
// about saving the output and typing it in later)
c.find(Locator::Id("freeze_button")).await?.click().await?;
Expand All @@ -43,7 +43,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080"));
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
// Check that the empty initials are restored
assert_eq!(
c.find(Locator::Id("page_state")).await?.text().await?,
Expand All @@ -70,7 +70,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
// And go back to the index page to check everything fully
c.find(Locator::Id("index-link")).await?.click().await?;
c.current_url().await?;
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Verify that everything has been correctly restored
assert_eq!(
c.find(Locator::Id("page_state")).await?.text().await?,
Expand Down
4 changes: 2 additions & 2 deletions examples/core/index_view/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {

// The greeting was passed through using build state
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Hello World!");
// Check the footer, the symbol of the index view having worked
Expand All @@ -22,7 +22,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Make sure the hardcoded text there exists
let text = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(text, "About.");
Expand Down
4 changes: 2 additions & 2 deletions examples/core/js_interop/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {

// The greeting was passed through using build state
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Hello World!");
// For some reason, retrieving the inner HTML or text of a `<title>` doens't
Expand All @@ -23,7 +23,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Make sure the hardcoded text there exists
let text = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(text, "About.");
Expand Down
4 changes: 2 additions & 2 deletions examples/core/plugins/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {

// The greeting was passed through using build state
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Hello World!");
// For some reason, retrieving the inner HTML or text of a `<title>` doens't
Expand All @@ -25,7 +25,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
// Make sure the hardcoded text there exists
let text = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(text, "Hey from a plugin!");
Expand Down
3 changes: 3 additions & 0 deletions examples/core/router_state/src/templates/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ pub fn router_state_page<G: Html>(cx: Scope) -> View<G> {
path,
} => format!("Loading {} (template: {}).", path, template_name),
RouterLoadState::Server => "We're on the server.".to_string(),
// Since this code is running in a page, it's a little pointless to handle an error page,
// which would replace this page (we wouldn't be able to display anything if this happened)
RouterLoadState::ErrorLoaded { .. } => unreachable!(),
});

view! { cx,
Expand Down
6 changes: 3 additions & 3 deletions examples/core/rx_state/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080"));
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);

// The initial greeting should be to an empty string
let mut greeting = c.find(Locator::Css("p")).await?;
Expand All @@ -26,12 +26,12 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);
c.find(Locator::Id("index-link")).await?.click().await?;
let url = c.current_url().await?;
assert!(url.as_ref().starts_with("http://localhost:8080"));
wait_for_checkpoint!("initial_state_not_present", 0, c);
wait_for_checkpoint!("page_visible", 1, c);
wait_for_checkpoint!("page_interactive", 1, c);

let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Greetings, Test User!");
Expand Down
4 changes: 2 additions & 2 deletions examples/core/state_generation/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async fn revalidation(c: &mut Client) -> Result<(), fantoccini::error::CmdError>
// be different
std::thread::sleep(std::time::Duration::from_secs(5));
c.refresh().await?;
wait_for_checkpoint!("router_entry", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let new_text = c.find(Locator::Css("p")).await?.text().await?;
assert_ne!(text, new_text);

Expand All @@ -135,7 +135,7 @@ async fn revalidation_and_incremental_generation(
// be different
std::thread::sleep(std::time::Duration::from_secs(5));
c.refresh().await?;
wait_for_checkpoint!("router_entry", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let new_text = c.find(Locator::Css("p")).await?.text().await?;
assert_ne!(text, new_text);

Expand Down
2 changes: 1 addition & 1 deletion examples/core/unreactive/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {

// The greeting was passed through using build state
wait_for_checkpoint!("initial_state_present", 0, c);
wait_for_checkpoint!("page_visible", 0, c);
wait_for_checkpoint!("page_interactive", 0, c);
let greeting = c.find(Locator::Css("p")).await?.text().await?;
assert_eq!(greeting, "Hello World!");
// For some reason, retrieving the inner HTML or text of a `<title>` doens't
Expand Down
15 changes: 14 additions & 1 deletion packages/perseus-actix-web/src/initial_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub async fn initial_load<M: MutableStore, T: TranslationsManager>(
};

// Run the routing algorithms on the path to figure out which template we need
// (this *does* check if the locale is supported)
let verdict = match_route_atomic(&path_slice, render_cfg.get_ref(), templates, &opts.locales);
match verdict {
// If this is the outcome, we know that the locale is supported and the like
Expand Down Expand Up @@ -99,11 +100,23 @@ pub async fn initial_load<M: MutableStore, T: TranslationsManager>(
return html_err(err_to_status_code(&err), &fmt_err(&err));
}
};
// Get the translations to interpolate into the page
let translations = translations_manager
.get_translations_str_for_locale(locale)
.await;
let translations = match translations {
Ok(translations) => translations,
// We know for sure that this locale is supported, so there's been an internal
// server error if it can't be found
Err(err) => {
return html_err(500, &fmt_err(&err));
}
};

let final_html = html_shell
.get_ref()
.clone()
.page_data(&page_data, &global_state)
.page_data(&page_data, &global_state, &translations)
.to_string();

let mut http_res = HttpResponse::Ok();
Expand Down
14 changes: 13 additions & 1 deletion packages/perseus-axum/src/initial_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,23 @@ pub async fn initial_load_handler<M: MutableStore, T: TranslationsManager>(
return html_err(err_to_status_code(&err), &fmt_err(&err));
}
};
// Get the translations to interpolate into the page
let translations = translations_manager
.get_translations_str_for_locale(locale)
.await;
let translations = match translations {
Ok(translations) => translations,
// We know for sure that this locale is supported, so there's been an internal
// server error if it can't be found
Err(err) => {
return html_err(500, &fmt_err(&err));
}
};

let final_html = html_shell
.as_ref()
.clone()
.page_data(&page_data, &global_state)
.page_data(&page_data, &global_state, &translations)
.to_string();

// http_res.content_type("text/html");
Expand Down
6 changes: 3 additions & 3 deletions packages/perseus-macro/src/template_rx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// add it to the page state store
let state_arg = &fn_args[1];
let rx_props_ty = match state_arg {
FnArg::Typed(PatType { ty, .. }) => make_mid(&**ty),
FnArg::Typed(PatType { ty, .. }) => make_mid(ty),
FnArg::Receiver(_) => unreachable!(),
};
// There's also a second argument for the global state, which we'll deserialize
Expand All @@ -163,7 +163,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// variable (this should be fine?)
let global_state_arg = &fn_args[2];
let (global_state_arg_pat, global_state_rx) = match global_state_arg {
FnArg::Typed(PatType { pat, ty, .. }) => (pat, make_mid(&**ty)),
FnArg::Typed(PatType { pat, ty, .. }) => (pat, make_mid(ty)),
FnArg::Receiver(_) => unreachable!(),
};
let name_string = name.to_string();
Expand Down Expand Up @@ -264,7 +264,7 @@ pub fn template_impl(input: TemplateFn) -> TokenStream {
// add it to the page state store
let arg = &fn_args[1];
let rx_props_ty = match arg {
FnArg::Typed(PatType { ty, .. }) => make_mid(&**ty),
FnArg::Typed(PatType { ty, .. }) => make_mid(ty),
FnArg::Receiver(_) => unreachable!(),
};
let name_string = name.to_string();
Expand Down
14 changes: 13 additions & 1 deletion packages/perseus-warp/src/initial_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,23 @@ pub async fn initial_load_handler<M: MutableStore, T: TranslationsManager>(
return html_err(err_to_status_code(&err), &fmt_err(&err));
}
};
// Get the translations to interpolate into the page
let translations = translations_manager
.get_translations_str_for_locale(locale)
.await;
let translations = match translations {
Ok(translations) => translations,
// We know for sure that this locale is supported, so there's been an internal
// server error if it can't be found
Err(err) => {
return html_err(500, &fmt_err(&err));
}
};

let final_html = html_shell
.as_ref()
.clone()
.page_data(&page_data, &global_state)
.page_data(&page_data, &global_state, &translations)
.to_string();

let mut http_res = Response::builder().status(200);
Expand Down
Loading

0 comments on commit d407727

Please sign in to comment.