Skip to content

Commit

Permalink
feat: ✨ added revalidation and refactored a fully modular rendering s…
Browse files Browse the repository at this point in the history
…ystem
  • Loading branch information
arctic-hen7 committed Aug 3, 2021
1 parent 5baf9bf commit c9df616
Show file tree
Hide file tree
Showing 14 changed files with 480 additions and 61 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ error-chain = "0.12"
futures = "0.3"
console_error_panic_hook = "0.1.6"
urlencoding = "2.1"
chrono = "0.4"

[workspace]
members = [
Expand Down
4 changes: 3 additions & 1 deletion examples/showcase/app/src/bin/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ fn main() {
pages::about::get_page::<SsrNode>(),
pages::post::get_page::<SsrNode>(),
pages::new_post::get_page::<SsrNode>(),
pages::ip::get_page::<SsrNode>()
pages::ip::get_page::<SsrNode>(),
pages::time::get_page::<SsrNode>(),
pages::time_root::get_page::<SsrNode>()
], &config_manager).expect("Static generation failed!");

println!("Static generation successfully completed!");
Expand Down
14 changes: 14 additions & 0 deletions examples/showcase/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ enum AppRoute {
},
#[to("/ip")]
Ip,
#[to("/time")]
TimeRoot,
#[to("/timeisr/<slug>")]
Time {
slug: String
},
#[not_found]
NotFound
}
Expand Down Expand Up @@ -62,6 +68,14 @@ pub fn run() -> Result<(), JsValue> {
"ip".to_string(),
pages::ip::template_fn()
),
AppRoute::Time { slug } => app_shell(
format!("timeisr/{}", slug),
pages::time::template_fn()
),
AppRoute::TimeRoot => app_shell(
"time".to_string(),
pages::time_root::template_fn()
),
AppRoute::NotFound => template! {
p {"Not Found."}
}
Expand Down
6 changes: 5 additions & 1 deletion examples/showcase/app/src/pages/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pub mod about;
pub mod post;
pub mod ip;
pub mod new_post;
pub mod time;
pub mod time_root;

use perseus::{get_templates_map, template::Template};
use sycamore::prelude::GenericNode;
Expand All @@ -15,6 +17,8 @@ pub fn get_templates_map<G: GenericNode>() -> HashMap<String, Template<G>> {
about::get_page::<G>(),
post::get_page::<G>(),
new_post::get_page::<G>(),
ip::get_page::<G>()
ip::get_page::<G>(),
time::get_page::<G>(),
time_root::get_page::<G>()
]
}
48 changes: 48 additions & 0 deletions examples/showcase/app/src/pages/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use sycamore::prelude::{template, component, GenericNode, Template as SycamoreTemplate};
use perseus::template::Template;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct TimePageProps {
pub time: String
}

#[component(TimePage<G>)]
pub fn time_page(props: TimePageProps) -> SycamoreTemplate<G> {
template! {
p { (format!("The time when this page was last rendered was '{}'.", props.time)) }
}
}

pub fn get_page<G: GenericNode>() -> Template<G> {
Template::new("timeisr")
.template(template_fn())
// This page will revalidate every five seconds (to illustrate revalidation)
.revalidate_after("5s".to_string())
.incremental_path_rendering(true)
.build_state_fn(Box::new(get_build_state))
.build_paths_fn(Box::new(get_build_paths))
}

pub fn get_build_state(_path: String) -> String {
serde_json::to_string(
&TimePageProps {
time: format!("{:?}", std::time::SystemTime::now())
}
).unwrap()
}

pub fn get_build_paths() -> Vec<String> {
vec![
"test".to_string()
]
}

pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
Box::new(|props: Option<String>| template! {
TimePage(
serde_json::from_str::<TimePageProps>(&props.unwrap()).unwrap()
)
}
)
}
44 changes: 44 additions & 0 deletions examples/showcase/app/src/pages/time_root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use sycamore::prelude::{template, component, GenericNode, Template as SycamoreTemplate};
use perseus::template::Template;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct TimePageProps {
pub time: String
}

#[component(TimePage<G>)]
pub fn time_page(props: TimePageProps) -> SycamoreTemplate<G> {
template! {
p { (format!("The time when this page was last rendered was '{}'.", props.time)) }
}
}

pub fn get_page<G: GenericNode>() -> Template<G> {
Template::new("time")
.template(template_fn())
// This page will revalidate every five seconds (to illustrate revalidation)
// Try changing this to a week, even though the below custom logic says to always revalidate, we'll only do it weekly
.revalidate_after("5s".to_string())
.should_revalidate_fn(Box::new(|| {
true
}))
.build_state_fn(Box::new(get_build_state))
}

pub fn get_build_state(_path: String) -> String {
serde_json::to_string(
&TimePageProps {
time: format!("{:?}", std::time::SystemTime::now())
}
).unwrap()
}

pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
Box::new(|props: Option<String>| template! {
TimePage(
serde_json::from_str::<TimePageProps>(&props.unwrap()).unwrap()
)
}
)
}
6 changes: 5 additions & 1 deletion examples/showcase/bonnie.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
version="0.3.1"

[scripts]
clean = "rm -f ./app/dist/static/*"
build.cmd = [
"cd app",
"cargo run --bin ssg",
"wasm-pack build --target web",
"rollup ./main.js --format iife --file ./pkg/bundle.js"
]
build.subcommands.--watch = "find ../../ -not -path \"../../target/*\" -not -path \"../../.git/*\" -not -path \"../../examples/showcase/app/dist/*\" | entr -s \"bonnie build\""
build.subcommands.--watch = [
"bonnie clean",
"find ../../ -not -path \"../../target/*\" -not -path \"../../.git/*\" -not -path \"../../examples/showcase/app/dist/*\" | entr -s \"bonnie build\""
]
serve = [
"cd server",
"cargo watch -w ../../../ -x \"run\""
Expand Down
11 changes: 11 additions & 0 deletions src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::{
template::Template,
config_manager::ConfigManager,
decode_time_str::decode_time_str
};
use crate::errors::*;
use std::collections::HashMap;
Expand Down Expand Up @@ -62,6 +63,16 @@ pub fn build_template(
.write(&format!("./dist/static/{}.html", full_path), &prerendered)?;
}

// Handle revalidation, we need to parse any given time strings into datetimes
// We don't need to worry about revalidation that operates by logic, that's request-time only
if template.revalidates_with_time() {
let datetime_to_revalidate = decode_time_str(&template.get_revalidate_interval().unwrap())?;
// Write that to a static file, we'll update it every time we revalidate
// Note that this runs for every path generated, so it's fully usable with ISR
config_manager
.write(&format!("./dist/static/{}.revld.txt", full_path), &datetime_to_revalidate.to_string())?;
}

// Note that SSR has already been handled by checking for `.uses_request_state()` above, we don't need to do any rendering here
// If a template only uses SSR, it won't get prerendered at build time whatsoever

Expand Down
2 changes: 1 addition & 1 deletion src/config_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ error_chain! {
pub trait ConfigManager {
/// Reads data from the named asset.
fn read(&self, name: &str) -> Result<String>;
/// Writes data to the named asset. This will create a new asset if on edoesn't exist already.
/// Writes data to the named asset. This will create a new asset if one doesn't exist already.
fn write(&self, name: &str, content: &str) -> Result<()>;
}

Expand Down
51 changes: 51 additions & 0 deletions src/decode_time_str.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::errors::*;
use chrono::{Duration, Utc};

// Decodes time strings like '1w' into actual datetimes from the present moment. If you've ever used NodeJS's [`jsonwebtoken`](https://www.npmjs.com/package/jsonwebtoken) module, this is
/// very similar (based on Vercel's [`ms`](https://github.com/vercel/ms) module for JavaScript).
/// Accepts strings of the form 'xXyYzZ...', where the lower-case letters are numbers meaning a number of the intervals X/Y/Z (e.g. 1m4d -- one month four days).
/// The available intervals are:
///
/// - s: second,
/// - m: minute,
/// - h: hour,
/// - d: day,
/// - w: week,
/// - M: month (30 days used here, 12M ≠ 1y!),
/// - y: year (365 days always, leap years ignored, if you want them add them as days)
pub fn decode_time_str(time_str: &str) -> Result<String> {
let mut duration_after_current = Duration::zero();
// Get the current datetime since Unix epoch, we'll add to that
let current = Utc::now();
// A working variable to store the '123' part of an interval until we reach the idnicator and can do the full conversion
let mut curr_duration_length = String::new();
// Iterate through the time string's characters to get each interval
for c in time_str.chars() {
// If we have a number, append it to the working cache
// If we have an indicator character, we'll match it to a duration
if c.is_numeric() {
curr_duration_length.push(c);
} else {
// Parse the working variable into an actual number
let interval_length = curr_duration_length.parse::<i64>().unwrap(); // It's just a string of numbers, we know more than the compiler
let duration = match c {
's' => Duration::seconds(interval_length),
'm' => Duration::minutes(interval_length),
'h' => Duration::hours(interval_length),
'd' => Duration::days(interval_length),
'w' => Duration::weeks(interval_length),
'M' => Duration::days(interval_length * 30), // Multiplying the number of months by 30 days (assumed length of a month)
'y' => Duration::days(interval_length * 365), // Multiplying the number of years by 365 days (assumed length of a year)
c => bail!(ErrorKind::InvalidDatetimeIntervalIndicator(c.to_string())),
};
duration_after_current = duration_after_current + duration;
// Reset that working variable
curr_duration_length = String::new();
}
}
// Form the final duration by reducing the durations vector into one
let datetime = current + duration_after_current;

// We return an easily parsible format (RFC 3339)
Ok(datetime.to_rfc3339())
}
9 changes: 9 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ error_chain! {
description("a template had no rendering options for use at request-time")
display("the template '{}' had no rendering options for use at request-time", template_path)
}
InvalidDatetimeIntervalIndicator(indicator: String) {
description("invalid indicator in timestring")
display("invalid indicator '{}' in timestring, must be one of: s, m, h, d, w, M, y", indicator)
}
BothStatesDefined {
description("both build and request states were defined for a template when only one or fewer were expected")
display("both build and request states were defined for a template when only one or fewer were expected")
}
}
links {
ConfigManager(crate::config_manager::Error, crate::config_manager::ErrorKind);
Expand All @@ -33,5 +41,6 @@ error_chain! {
foreign_links {
Io(::std::io::Error);
Json(::serde_json::Error);
ChronoParse(::chrono::ParseError);
}
}
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pub mod shell;
pub mod serve;
pub mod config_manager;
pub mod template;
pub mod build;
pub mod build;
pub mod decode_time_str;
Loading

0 comments on commit c9df616

Please sign in to comment.