Skip to content

Commit c9df616

Browse files
committed
feat: ✨ added revalidation and refactored a fully modular rendering system
1 parent 5baf9bf commit c9df616

File tree

14 files changed

+480
-61
lines changed

14 files changed

+480
-61
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ error-chain = "0.12"
1818
futures = "0.3"
1919
console_error_panic_hook = "0.1.6"
2020
urlencoding = "2.1"
21+
chrono = "0.4"
2122

2223
[workspace]
2324
members = [

examples/showcase/app/src/bin/build.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ fn main() {
1313
pages::about::get_page::<SsrNode>(),
1414
pages::post::get_page::<SsrNode>(),
1515
pages::new_post::get_page::<SsrNode>(),
16-
pages::ip::get_page::<SsrNode>()
16+
pages::ip::get_page::<SsrNode>(),
17+
pages::time::get_page::<SsrNode>(),
18+
pages::time_root::get_page::<SsrNode>()
1719
], &config_manager).expect("Static generation failed!");
1820

1921
println!("Static generation successfully completed!");

examples/showcase/app/src/lib.rs

+14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ enum AppRoute {
2020
},
2121
#[to("/ip")]
2222
Ip,
23+
#[to("/time")]
24+
TimeRoot,
25+
#[to("/timeisr/<slug>")]
26+
Time {
27+
slug: String
28+
},
2329
#[not_found]
2430
NotFound
2531
}
@@ -62,6 +68,14 @@ pub fn run() -> Result<(), JsValue> {
6268
"ip".to_string(),
6369
pages::ip::template_fn()
6470
),
71+
AppRoute::Time { slug } => app_shell(
72+
format!("timeisr/{}", slug),
73+
pages::time::template_fn()
74+
),
75+
AppRoute::TimeRoot => app_shell(
76+
"time".to_string(),
77+
pages::time_root::template_fn()
78+
),
6579
AppRoute::NotFound => template! {
6680
p {"Not Found."}
6781
}

examples/showcase/app/src/pages/mod.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ pub mod about;
33
pub mod post;
44
pub mod ip;
55
pub mod new_post;
6+
pub mod time;
7+
pub mod time_root;
68

79
use perseus::{get_templates_map, template::Template};
810
use sycamore::prelude::GenericNode;
@@ -15,6 +17,8 @@ pub fn get_templates_map<G: GenericNode>() -> HashMap<String, Template<G>> {
1517
about::get_page::<G>(),
1618
post::get_page::<G>(),
1719
new_post::get_page::<G>(),
18-
ip::get_page::<G>()
20+
ip::get_page::<G>(),
21+
time::get_page::<G>(),
22+
time_root::get_page::<G>()
1923
]
2024
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use sycamore::prelude::{template, component, GenericNode, Template as SycamoreTemplate};
2+
use perseus::template::Template;
3+
use serde::{Serialize, Deserialize};
4+
5+
#[derive(Serialize, Deserialize, Debug)]
6+
pub struct TimePageProps {
7+
pub time: String
8+
}
9+
10+
#[component(TimePage<G>)]
11+
pub fn time_page(props: TimePageProps) -> SycamoreTemplate<G> {
12+
template! {
13+
p { (format!("The time when this page was last rendered was '{}'.", props.time)) }
14+
}
15+
}
16+
17+
pub fn get_page<G: GenericNode>() -> Template<G> {
18+
Template::new("timeisr")
19+
.template(template_fn())
20+
// This page will revalidate every five seconds (to illustrate revalidation)
21+
.revalidate_after("5s".to_string())
22+
.incremental_path_rendering(true)
23+
.build_state_fn(Box::new(get_build_state))
24+
.build_paths_fn(Box::new(get_build_paths))
25+
}
26+
27+
pub fn get_build_state(_path: String) -> String {
28+
serde_json::to_string(
29+
&TimePageProps {
30+
time: format!("{:?}", std::time::SystemTime::now())
31+
}
32+
).unwrap()
33+
}
34+
35+
pub fn get_build_paths() -> Vec<String> {
36+
vec![
37+
"test".to_string()
38+
]
39+
}
40+
41+
pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
42+
Box::new(|props: Option<String>| template! {
43+
TimePage(
44+
serde_json::from_str::<TimePageProps>(&props.unwrap()).unwrap()
45+
)
46+
}
47+
)
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use sycamore::prelude::{template, component, GenericNode, Template as SycamoreTemplate};
2+
use perseus::template::Template;
3+
use serde::{Serialize, Deserialize};
4+
5+
#[derive(Serialize, Deserialize, Debug)]
6+
pub struct TimePageProps {
7+
pub time: String
8+
}
9+
10+
#[component(TimePage<G>)]
11+
pub fn time_page(props: TimePageProps) -> SycamoreTemplate<G> {
12+
template! {
13+
p { (format!("The time when this page was last rendered was '{}'.", props.time)) }
14+
}
15+
}
16+
17+
pub fn get_page<G: GenericNode>() -> Template<G> {
18+
Template::new("time")
19+
.template(template_fn())
20+
// This page will revalidate every five seconds (to illustrate revalidation)
21+
// Try changing this to a week, even though the below custom logic says to always revalidate, we'll only do it weekly
22+
.revalidate_after("5s".to_string())
23+
.should_revalidate_fn(Box::new(|| {
24+
true
25+
}))
26+
.build_state_fn(Box::new(get_build_state))
27+
}
28+
29+
pub fn get_build_state(_path: String) -> String {
30+
serde_json::to_string(
31+
&TimePageProps {
32+
time: format!("{:?}", std::time::SystemTime::now())
33+
}
34+
).unwrap()
35+
}
36+
37+
pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
38+
Box::new(|props: Option<String>| template! {
39+
TimePage(
40+
serde_json::from_str::<TimePageProps>(&props.unwrap()).unwrap()
41+
)
42+
}
43+
)
44+
}

examples/showcase/bonnie.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
version="0.3.1"
22

33
[scripts]
4+
clean = "rm -f ./app/dist/static/*"
45
build.cmd = [
56
"cd app",
67
"cargo run --bin ssg",
78
"wasm-pack build --target web",
89
"rollup ./main.js --format iife --file ./pkg/bundle.js"
910
]
10-
build.subcommands.--watch = "find ../../ -not -path \"../../target/*\" -not -path \"../../.git/*\" -not -path \"../../examples/showcase/app/dist/*\" | entr -s \"bonnie build\""
11+
build.subcommands.--watch = [
12+
"bonnie clean",
13+
"find ../../ -not -path \"../../target/*\" -not -path \"../../.git/*\" -not -path \"../../examples/showcase/app/dist/*\" | entr -s \"bonnie build\""
14+
]
1115
serve = [
1216
"cd server",
1317
"cargo watch -w ../../../ -x \"run\""

src/build.rs

+11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::{
44
template::Template,
55
config_manager::ConfigManager,
6+
decode_time_str::decode_time_str
67
};
78
use crate::errors::*;
89
use std::collections::HashMap;
@@ -62,6 +63,16 @@ pub fn build_template(
6263
.write(&format!("./dist/static/{}.html", full_path), &prerendered)?;
6364
}
6465

66+
// Handle revalidation, we need to parse any given time strings into datetimes
67+
// We don't need to worry about revalidation that operates by logic, that's request-time only
68+
if template.revalidates_with_time() {
69+
let datetime_to_revalidate = decode_time_str(&template.get_revalidate_interval().unwrap())?;
70+
// Write that to a static file, we'll update it every time we revalidate
71+
// Note that this runs for every path generated, so it's fully usable with ISR
72+
config_manager
73+
.write(&format!("./dist/static/{}.revld.txt", full_path), &datetime_to_revalidate.to_string())?;
74+
}
75+
6576
// Note that SSR has already been handled by checking for `.uses_request_state()` above, we don't need to do any rendering here
6677
// If a template only uses SSR, it won't get prerendered at build time whatsoever
6778

src/config_manager.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ error_chain! {
3131
pub trait ConfigManager {
3232
/// Reads data from the named asset.
3333
fn read(&self, name: &str) -> Result<String>;
34-
/// Writes data to the named asset. This will create a new asset if on edoesn't exist already.
34+
/// Writes data to the named asset. This will create a new asset if one doesn't exist already.
3535
fn write(&self, name: &str, content: &str) -> Result<()>;
3636
}
3737

src/decode_time_str.rs

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::errors::*;
2+
use chrono::{Duration, Utc};
3+
4+
// 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
5+
/// very similar (based on Vercel's [`ms`](https://github.com/vercel/ms) module for JavaScript).
6+
/// 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).
7+
/// The available intervals are:
8+
///
9+
/// - s: second,
10+
/// - m: minute,
11+
/// - h: hour,
12+
/// - d: day,
13+
/// - w: week,
14+
/// - M: month (30 days used here, 12M ≠ 1y!),
15+
/// - y: year (365 days always, leap years ignored, if you want them add them as days)
16+
pub fn decode_time_str(time_str: &str) -> Result<String> {
17+
let mut duration_after_current = Duration::zero();
18+
// Get the current datetime since Unix epoch, we'll add to that
19+
let current = Utc::now();
20+
// A working variable to store the '123' part of an interval until we reach the idnicator and can do the full conversion
21+
let mut curr_duration_length = String::new();
22+
// Iterate through the time string's characters to get each interval
23+
for c in time_str.chars() {
24+
// If we have a number, append it to the working cache
25+
// If we have an indicator character, we'll match it to a duration
26+
if c.is_numeric() {
27+
curr_duration_length.push(c);
28+
} else {
29+
// Parse the working variable into an actual number
30+
let interval_length = curr_duration_length.parse::<i64>().unwrap(); // It's just a string of numbers, we know more than the compiler
31+
let duration = match c {
32+
's' => Duration::seconds(interval_length),
33+
'm' => Duration::minutes(interval_length),
34+
'h' => Duration::hours(interval_length),
35+
'd' => Duration::days(interval_length),
36+
'w' => Duration::weeks(interval_length),
37+
'M' => Duration::days(interval_length * 30), // Multiplying the number of months by 30 days (assumed length of a month)
38+
'y' => Duration::days(interval_length * 365), // Multiplying the number of years by 365 days (assumed length of a year)
39+
c => bail!(ErrorKind::InvalidDatetimeIntervalIndicator(c.to_string())),
40+
};
41+
duration_after_current = duration_after_current + duration;
42+
// Reset that working variable
43+
curr_duration_length = String::new();
44+
}
45+
}
46+
// Form the final duration by reducing the durations vector into one
47+
let datetime = current + duration_after_current;
48+
49+
// We return an easily parsible format (RFC 3339)
50+
Ok(datetime.to_rfc3339())
51+
}

src/errors.rs

+9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ error_chain! {
2525
description("a template had no rendering options for use at request-time")
2626
display("the template '{}' had no rendering options for use at request-time", template_path)
2727
}
28+
InvalidDatetimeIntervalIndicator(indicator: String) {
29+
description("invalid indicator in timestring")
30+
display("invalid indicator '{}' in timestring, must be one of: s, m, h, d, w, M, y", indicator)
31+
}
32+
BothStatesDefined {
33+
description("both build and request states were defined for a template when only one or fewer were expected")
34+
display("both build and request states were defined for a template when only one or fewer were expected")
35+
}
2836
}
2937
links {
3038
ConfigManager(crate::config_manager::Error, crate::config_manager::ErrorKind);
@@ -33,5 +41,6 @@ error_chain! {
3341
foreign_links {
3442
Io(::std::io::Error);
3543
Json(::serde_json::Error);
44+
ChronoParse(::chrono::ParseError);
3645
}
3746
}

src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ pub mod shell;
33
pub mod serve;
44
pub mod config_manager;
55
pub mod template;
6-
pub mod build;
6+
pub mod build;
7+
pub mod decode_time_str;

0 commit comments

Comments
 (0)