Skip to content

Commit

Permalink
feat: ✨ props now passed around as strings
Browse files Browse the repository at this point in the history
This only adds one extra deserialization at build-time and removes the need for heap allocation
  • Loading branch information
arctic-hen7 committed Jul 31, 2021
1 parent 734f9df commit 7a334cf
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 146 deletions.
8 changes: 4 additions & 4 deletions examples/showcase/app/src/bin/build.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use perseus::{
config_manager::{FsConfigManager, ConfigManager},
build_templates
config_manager::FsConfigManager,
build::build_templates
};
use perseus_showcase_app::pages;
use sycamore::prelude::SsrNode;

fn main() {
let config_manager = FsConfigManager::new();

build_templates!([
build_templates(vec![
pages::index::get_page::<SsrNode>(),
pages::about::get_page::<SsrNode>(),
pages::post::get_page::<SsrNode>()
], &config_manager);
], &config_manager).expect("Static generation failed!");

println!("Static generation successfully completed!");
}
12 changes: 3 additions & 9 deletions examples/showcase/app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,15 @@ pub fn run() -> Result<(), JsValue> {
match route {
AppRoute::Index => app_shell(
"index".to_string(),
Box::new(|props: Option<pages::index::IndexPageProps>| template! {
pages::index::IndexPage(props.unwrap())
})
pages::index::template_fn()
),
AppRoute::About => app_shell(
"about".to_string(),
Box::new(|_: Option<()>| template! {
pages::about::AboutPage()
})
pages::about::template_fn()
),
AppRoute::Post { slug } => app_shell(
format!("post/{}", slug),
Box::new(|props: Option<pages::post::PostPageProps>| template! {
pages::post::PostPage(props.unwrap())
})
pages::post::template_fn()
),
AppRoute::NotFound => template! {
p {"Not Found."}
Expand Down
14 changes: 9 additions & 5 deletions examples/showcase/app/src/pages/about.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ pub fn about_page() -> SycamoreTemplate<G> {
}
}

pub fn get_page<G: GenericNode>() -> Template<(), G> {
pub fn get_page<G: GenericNode>() -> Template<G> {
Template::new("about")
.template(Box::new(|_| template! {
AboutPage()
}
))
.template(template_fn())
}

pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
Box::new(|_| template! {
AboutPage()
}
)
}
24 changes: 16 additions & 8 deletions examples/showcase/app/src/pages/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@ pub fn index_page(props: IndexPageProps) -> SycamoreTemplate<G> {
}
}

pub fn get_page<G: GenericNode>() -> Template<IndexPageProps, G> {
pub fn get_page<G: GenericNode>() -> Template<G> {
Template::new("index")
.build_state_fn(Box::new(get_static_props))
.template(Box::new(|props: Option<IndexPageProps>| template! {
IndexPage(props.unwrap())
}))
.template(template_fn())
}

pub fn get_static_props(_path: String) -> IndexPageProps {
IndexPageProps {
greeting: "Hello World!".to_string()
}
pub fn get_static_props(_path: String) -> String {
serde_json::to_string(
&IndexPageProps {
greeting: "Hello World!".to_string()
}
).unwrap()
}

pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
Box::new(|props: Option<String>| template! {
IndexPage(
serde_json::from_str::<IndexPageProps>(&props.unwrap()).unwrap()
)
})
}
13 changes: 13 additions & 0 deletions examples/showcase/app/src/pages/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
pub mod index;
pub mod about;
pub mod post;

use perseus::{get_templates_map, template::Template};
use sycamore::prelude::GenericNode;
use std::collections::HashMap;

/// Shorthand for the `get_templates_map!` macro from Perseus for our specific app pages.
pub fn get_templates_map<G: GenericNode>() -> HashMap<String, Template<G>> {
get_templates_map! [
index::get_page::<G>(),
about::get_page::<G>(),
post::get_page::<G>()
]
}
26 changes: 17 additions & 9 deletions examples/showcase/app/src/pages/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,39 @@ pub fn post_page(props: PostPageProps) -> SycamoreTemplate<G> {
}
}

pub fn get_page<G: GenericNode>() -> Template<PostPageProps, G> {
pub fn get_page<G: GenericNode>() -> Template<G> {
Template::new("post")
.build_paths_fn(Box::new(get_static_paths))
.build_state_fn(Box::new(get_static_props))
.incremental_path_rendering(true)
.template(Box::new(|props: Option<PostPageProps>| template! {
PostPage(props.unwrap())
}))
.template(template_fn())
}

pub fn get_static_props(path: String) -> PostPageProps {
pub fn get_static_props(path: String) -> String {
let path_vec: Vec<&str> = path.split('/').collect();
let title_slug = path_vec[0];
// This is just an example
let title = urlencoding::decode(title_slug).unwrap();
let content = format!("This is a post entitled '{}'. Its original slug was '{}'.", title, title_slug);

PostPageProps {
title: title.to_string(),
content
}
serde_json::to_string(
&PostPageProps {
title: title.to_string(),
content
}
).unwrap()
}
// TODO
pub fn get_static_paths() -> Vec<String> {
vec![
"test".to_string()
]
}

pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
Box::new(|props: Option<String>| template! {
PostPage(
serde_json::from_str::<PostPageProps>(&props.unwrap()).unwrap()
)
})
}
2 changes: 1 addition & 1 deletion examples/showcase/bonnie.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ build.cmd = [
build.subcommands.--watch = "find ../../ -not -path \"../../target/*\" -not -path \"../../.git/*\" | entr -s \"bonnie build\""
serve = [
"cd server",
"cargo watch -w ../ -x \"run\""
"cargo watch -w ../../../ -x \"run\""
]
1 change: 1 addition & 0 deletions examples/showcase/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ actix-web = "3.3"
actix-files = "0.5"
urlencoding = "2.1"
serde_json = "1"
sycamore = { version = "0.5.1", features = ["ssr"] }
perseus-showcase-app = { path = "../app" }
12 changes: 9 additions & 3 deletions examples/showcase/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use actix_web::{web, App, HttpRequest, HttpServer, Result as ActixResult, error};
use actix_files::{NamedFile};
use sycamore::prelude::SsrNode;

use perseus::{
serve::{get_render_cfg, get_page},
render_cfg::RenderCfg,
config_manager::FsConfigManager
config_manager::FsConfigManager,
template::TemplateMap
};
use perseus_showcase_app::pages;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
Expand All @@ -17,6 +20,9 @@ async fn main() -> std::io::Result<()> {
.data(
FsConfigManager::new()
)
.data(
pages::get_templates_map::<SsrNode>()
)
// TODO chunk JS and WASM bundles
// These allow getting the basic app code (not including the static data)
// This contains everything in the spirit of a pseudo-SPA
Expand All @@ -42,10 +48,10 @@ async fn js_bundle() -> std::io::Result<NamedFile> {
async fn wasm_bundle() -> std::io::Result<NamedFile> {
NamedFile::open("../app/pkg/perseus_showcase_app_bg.wasm")
}
async fn page_data(req: HttpRequest, render_cfg: web::Data<RenderCfg>, config_manager: web::Data<FsConfigManager>) -> ActixResult<String> {
async fn page_data(req: HttpRequest, templates: web::Data<TemplateMap<SsrNode>>, render_cfg: web::Data<RenderCfg>, config_manager: web::Data<FsConfigManager>) -> ActixResult<String> {
let path = req.match_info().query("filename");
// TODO match different types of errors here
let page_data = get_page(path, &render_cfg, config_manager.get_ref()).map_err(error::ErrorNotFound)?;
let page_data = get_page(path, &render_cfg, &templates, config_manager.get_ref()).map_err(error::ErrorNotFound)?;

Ok(
serde_json::to_string(&page_data).unwrap()
Expand Down
104 changes: 47 additions & 57 deletions src/build.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
// This binary builds all the templates with SSG

use serde::{Serialize, de::DeserializeOwned};
use crate::{
template::Template,
config_manager::ConfigManager,
render_cfg::RenderOpt
render_cfg::{RenderOpt, RenderCfg, TemplatesCfg, PagesCfg}
};
use crate::errors::*;
use std::any::Any;
use std::collections::HashMap;
use sycamore::prelude::SsrNode;

/// Builds a template, writing static data as appropriate. This should be used as part of a larger build process. This returns both a list
/// of the extracted render options for this template (needed at request time), a list of pages that it explicitly generated, and a boolean
/// as to whether or not it only generated a single page to occupy the template's root path (`true` unless using using build-time path
/// generation).
pub fn build_template<Props: Serialize + DeserializeOwned + Any>(
template: Template<Props, SsrNode>,
pub fn build_template(
template: Template<SsrNode>,
config_manager: &impl ConfigManager
) -> Result<
(
Expand Down Expand Up @@ -71,20 +70,17 @@ pub fn build_template<Props: Serialize + DeserializeOwned + Any>(
if render_opts.contains(&RenderOpt::StaticProps) {
// We pass in the latter part of the path, without the base specifier (because that would be the same for everything in the template)
let initial_state = template.get_build_state(path.to_string())?;
let initial_state_str = serde_json::to_string(&initial_state).unwrap();
// Write that intial state to a static JSON file
config_manager
.write(&format!("./dist/static/{}.json", full_path), &initial_state_str)
.unwrap();
.write(&format!("./dist/static/{}.json", full_path), &initial_state)?;
// Prerender the template using that state
let prerendered = sycamore::render_to_string(
||
template.render_for_template(Some(initial_state))
);
// Write that prerendered HTML to a static file
config_manager
.write(&format!("./dist/static/{}.html", full_path), &prerendered)
.unwrap();
.write(&format!("./dist/static/{}.html", full_path), &prerendered)?;
}

// Note that SSR has already been handled by checking for `.uses_request_state()` above, we don't need to do any rendering here
Expand All @@ -100,65 +96,59 @@ pub fn build_template<Props: Serialize + DeserializeOwned + Any>(
);
// Write that prerendered HTML to a static file
config_manager
.write(&format!("./dist/static/{}.html", full_path), &prerendered)
.unwrap();
.write(&format!("./dist/static/{}.html", full_path), &prerendered)?;
}
}

Ok((render_opts, paths, single_page))
}

/// Runs the build process of building many different templates. This is done with a macro because typing for a function means we have to do
/// things on the heap.
/// (Any better solutions are welcome in PRs!)
// TODO set up error handling here
#[macro_export]
macro_rules! build_templates {
(
[$($template:expr),+],
$config_manager:expr
) => {
let mut templates_conf: $crate::render_cfg::TemplatesCfg = ::std::collections::HashMap::new();
let mut pages_conf: $crate::render_cfg::PagesCfg = ::std::collections::HashMap::new();
// Create each of the templates
$(
let (render_opts, pages, single_page) = $crate::build::build_template($template, $config_manager)
.unwrap();
let template_root_path = $template.get_path();
templates_conf.insert(
// TODO function to build pages
/// Runs the build process of building many different templates.
pub fn build_templates(templates: Vec<Template<SsrNode>>, config_manager: &impl ConfigManager) -> Result<()> {
let mut templates_conf: TemplatesCfg = HashMap::new();
let mut pages_conf: PagesCfg = HashMap::new();
// Create each of the templates
for template in templates {
let template_root_path = template.get_path();
let is_incremental = template.uses_incremental();
let (render_opts, pages, single_page) = build_template(template, config_manager)?;
templates_conf.insert(
template_root_path.clone(),
render_opts
);
// If the tempalte represents a single page itself, we don't need any concatenation
if single_page {
pages_conf.insert(
template_root_path.clone(),
render_opts
template_root_path.clone()
);
if single_page {
} else {
// Add each page that the template explicitly generated (ignoring ISR for now)
for page in pages {
pages_conf.insert(
format!("{}/{}", &template_root_path, &page),
template_root_path.clone()
);
}
// Now if the page uses ISR, add an explicit `/*` in there after the template root path
// Incremental rendering requires build-time path generation
if is_incremental {
pages_conf.insert(
template_root_path.clone(),
format!("{}/*", &template_root_path),
template_root_path.clone()
);
} else {
// Add each page that the template explicitly generated (ignoring ISR for now)
for page in pages {
pages_conf.insert(
format!("{}/{}", &template_root_path, &page),
template_root_path.clone()
);
}
// Now if the page uses ISR, add an explicit `/*` in there after the template root path
// Incremental rendering requires build-time path generation
if $template.uses_incremental() {
pages_conf.insert(
format!("{}/*", &template_root_path),
template_root_path.clone()
);
}
}
)+
}
}

let render_conf = $crate::render_cfg::RenderCfg {
templates: templates_conf,
pages: pages_conf
};
$config_manager
.write("./dist/render_conf.json", &serde_json::to_string(&render_conf).unwrap())
.unwrap();
let render_conf = RenderCfg {
templates: templates_conf,
pages: pages_conf
};
config_manager
.write("./dist/render_conf.json", &serde_json::to_string(&render_conf)?)?;

Ok(())
}
8 changes: 8 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ error_chain! {
description("a template feature required by a function called was not present")
display("the template '{}' is missing the feature '{}'", name, feature)
}
PageNotFound(path: String) {
description("the requested page was not found")
display("the requested page at path '{}' was not found", path)
}
NoRenderOpts(template_path: String) {
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)
}
}
links {
ConfigManager(crate::config_manager::Error, crate::config_manager::ErrorKind);
Expand Down
Loading

0 comments on commit 7a334cf

Please sign in to comment.