-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ added basic sycamore ssg systems
Added from private pre-dev repo
- Loading branch information
1 parent
1d424b5
commit c8530cf
Showing
26 changed files
with
884 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[package] | ||
name = "perseus" | ||
version = "0.1.0" | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
version="0.3.1" | ||
|
||
[scripts] | ||
start = "echo \"No start script yet!\"" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Examples | ||
|
||
This folder contains examples for Perseus, which are used to test the project and are excellent learning resources! If any of these don't work, please [open an issue](https://github.com/arctic-hen7/perseus/issues/choose) to let us know! | ||
|
||
These examples are all fully self-contained, and do not serve as examples in the traditional Cargo way, they are each indepedent crates to enable the use of build tools such as `wasm-pack`. | ||
|
||
- Showcase -- an app that demonstrates all the different features of Perseus, including SSR, SSG, and ISR (this example is actively used for testing) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
target/ | ||
Cargo.lock | ||
dist/ | ||
pkg/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[workspace] | ||
members = [ | ||
"app", | ||
"server" | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
[package] | ||
name = "perseus-showcase-app" | ||
version = "0.1.0" | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
sycamore = { version = "0.5.1", features = ["ssr"] } | ||
sycamore-router = "0.5.1" | ||
web-sys = { version = "0.3", features = ["Headers", "Request", "RequestInit", "RequestMode", "Response", "ReadableStream", "Window"] } | ||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } | ||
wasm-bindgen-futures = "0.4" | ||
serde = { version = "1", features = ["derive"] } | ||
serde_json = "1" | ||
typetag = "0.1" | ||
error-chain = "0.12" | ||
futures = "0.3" | ||
console_error_panic_hook = "0.1.6" | ||
urlencoding = "2.1" | ||
|
||
# This section is needed for WASM Pack (which we use instead of Trunk for flexibility) | ||
[lib] | ||
crate-type = ["cdylib", "rlib"] | ||
|
||
[[bin]] | ||
name = "ssg" | ||
path = "src/bin/build.rs" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Perseus Starter App</title> | ||
<script src="/.perseus/bundle.js" defer></script> | ||
</head> | ||
<body> | ||
<div id="_perseus_root"></div> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import init, { run } from "./pkg/perseus_showcase_app.js"; | ||
async function main() { | ||
await init("/.perseus/bundle.wasm"); | ||
run(); | ||
} | ||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use perseus_showcase_app::{ | ||
pages, | ||
config_manager::{FsConfigManager, ConfigManager}, | ||
build_pages | ||
}; | ||
|
||
fn main() { | ||
let config_manager = FsConfigManager::new(); | ||
|
||
build_pages!([ | ||
pages::index::get_page(), | ||
pages::about::get_page(), | ||
pages::post::get_page() | ||
], &config_manager); | ||
|
||
println!("Static generation successfully completed!"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// This binary builds all the pages with SSG | ||
|
||
use serde::{Serialize, de::DeserializeOwned}; | ||
use crate::{ | ||
page::Page, | ||
config_manager::ConfigManager, | ||
render_cfg::RenderOpt | ||
}; | ||
use crate::errors::*; | ||
use std::any::Any; | ||
|
||
/// Builds a page, writing static data as appropriate. This should be used as part of a larger build process. | ||
pub fn build_page<Props: Serialize + DeserializeOwned + Any>(page: Page<Props>, config_manager: &impl ConfigManager) -> Result<Vec<RenderOpt>> { | ||
let mut render_opts: Vec<RenderOpt> = Vec::new(); | ||
let page_path = page.get_path(); | ||
|
||
// Handle the boolean properties | ||
if page.revalidates() { | ||
render_opts.push(RenderOpt::Revalidated); | ||
} | ||
if page.uses_incremental() { | ||
render_opts.push(RenderOpt::Incremental); | ||
} | ||
|
||
// Handle static path generation | ||
// Because we iterate over the paths, we need a base path if we're not generating custom ones (that'll be overriden if needed) | ||
let paths = match page.uses_build_paths() { | ||
true => { | ||
render_opts.push(RenderOpt::StaticPaths); | ||
page.get_build_paths()? | ||
}, | ||
false => vec![page_path.clone()] | ||
}; | ||
|
||
// Iterate through the paths to generate initial states if needed | ||
for path in paths.iter() { | ||
// If needed, we'll contruct a full path that's URL encoded so we can easily save it as a file | ||
// BUG: insanely nested paths won't work whatsoever if the filename is too long, maybe hash instead? | ||
let full_path = match render_opts.contains(&RenderOpt::StaticPaths) { | ||
true => urlencoding::encode(&format!("{}/{}", &page_path, path)).to_string(), | ||
// We don't want to concatenate the name twice if we don't have to | ||
false => page_path.clone() | ||
}; | ||
|
||
// Handle static initial state generation | ||
// We'll only write a static state if one is explicitly generated | ||
if page.uses_build_state() { | ||
render_opts.push(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 = page.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(); | ||
// Prerender the page using that state | ||
let prerendered = sycamore::render_to_string( | ||
|| | ||
page.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(); | ||
} | ||
|
||
// Handle server-side rendering | ||
// By definition, everything here is done at request-time, so there's not really much to do | ||
// Note also that if a page only uses SSR, it won't get prerendered at build time whatsoever | ||
if page.uses_request_state() { | ||
render_opts.push(RenderOpt::Server); | ||
} | ||
|
||
// If the page is very basic, prerender without any state | ||
if page.is_basic() { | ||
render_opts.push(RenderOpt::StaticProps); | ||
let prerendered = sycamore::render_to_string( | ||
|| | ||
page.render_for_template(None) | ||
); | ||
// Write that prerendered HTML to a static file | ||
config_manager | ||
.write(&format!("./dist/static/{}.html", full_path), &prerendered) | ||
.unwrap(); | ||
} | ||
} | ||
|
||
Ok(render_opts) | ||
} | ||
|
||
/// Runs the build process of building many different pages. 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!) | ||
#[macro_export] | ||
macro_rules! build_pages { | ||
( | ||
[$($page:expr),+], | ||
$config_manager:expr | ||
) => { | ||
let mut render_conf: $crate::render_cfg::RenderCfg = ::std::collections::HashMap::new(); | ||
$( | ||
render_conf.insert( | ||
$page.get_path(), | ||
$crate::build::build_page($page, $config_manager) | ||
.unwrap() | ||
); | ||
)+ | ||
$config_manager | ||
.write("./dist/render_conf.json", &serde_json::to_string(&render_conf).unwrap()) | ||
.unwrap(); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// This file contains the logic for a universal interface to read and write to static files | ||
// At simplest, this is just a filesystem interface, but it's more likely to be a CMS in production | ||
// This has its own error management logic because the user may implement it separately | ||
|
||
use std::fs; | ||
use error_chain::{error_chain, bail}; | ||
|
||
// This has no foreign links because everything to do with config management should be isolated and generic | ||
error_chain! { | ||
errors { | ||
/// For when data wasn't found. | ||
NotFound(name: String) { | ||
description("data not found") | ||
display("data with name '{}' not found", name) | ||
} | ||
/// For when data couldn't be read for some generic reason. | ||
ReadFailed(name: String, err: String) { | ||
description("data couldn't be read") | ||
display("data with name '{}' couldn't be read, error was '{}'", name, err) | ||
} | ||
/// For when data couldn't be written for some generic reason. | ||
WriteFailed(name: String, err: String) { | ||
description("data couldn't be written") | ||
display("data with name '{}' couldn't be written, error was '{}'", name, err) | ||
} | ||
} | ||
} | ||
|
||
/// A trait for systems that manage where to put configuration files. At simplest, we'll just write them to static files, but they're | ||
/// more likely to be stored on a CMS. | ||
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. | ||
fn write(&self, name: &str, content: &str) -> Result<()>; | ||
} | ||
|
||
#[derive(Default)] | ||
pub struct FsConfigManager {} | ||
impl FsConfigManager { | ||
/// Creates a new filesystem configuration manager. This function only exists to preserve the API surface of the trait. | ||
pub fn new() -> Self { | ||
Self::default() | ||
} | ||
} | ||
impl ConfigManager for FsConfigManager { | ||
fn read(&self, name: &str) -> Result<String> { | ||
match fs::metadata(name) { | ||
Ok(_) => fs::read_to_string(name).map_err( | ||
|err| | ||
ErrorKind::ReadFailed(name.to_string(), err.to_string()).into() | ||
), | ||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => bail!(ErrorKind::NotFound(name.to_string())), | ||
Err(err) => bail!(ErrorKind::ReadFailed(name.to_string(), err.to_string())) | ||
} | ||
} | ||
fn write(&self, name: &str, content: &str) -> Result<()> { | ||
fs::write(name, content).map_err( | ||
|err| | ||
ErrorKind::WriteFailed(name.to_string(), err.to_string()).into() | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
#![allow(missing_docs)] | ||
|
||
pub use error_chain::bail; | ||
use error_chain::error_chain; | ||
|
||
// The `error_chain` setup for the whole crate | ||
error_chain! { | ||
// The custom errors for this crate (very broad) | ||
errors { | ||
/// For indistinct JavaScript errors. | ||
JsErr(err: String) { | ||
description("an error occurred while interfacing with javascript") | ||
display("the following error occurred while interfacing with javascript: {:?}", err) | ||
} | ||
|
||
PageFeatureNotEnabled(name: String, feature: String) { | ||
description("a page feature required by a function called was not present") | ||
display("the page '{}' is missing the feature '{}'", name, feature) | ||
} | ||
} | ||
links { | ||
ConfigManager(crate::config_manager::Error, crate::config_manager::ErrorKind); | ||
} | ||
// We work with many external libraries, all of which have their own errors | ||
foreign_links { | ||
Io(::std::io::Error); | ||
Json(::serde_json::Error); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
pub mod errors; | ||
pub mod pages; | ||
mod shell; | ||
pub mod serve; | ||
pub mod render_cfg; | ||
pub mod config_manager; | ||
pub mod page; | ||
pub mod build; | ||
|
||
use sycamore::prelude::*; | ||
use sycamore_router::{Route, BrowserRouter}; | ||
use wasm_bindgen::prelude::*; | ||
|
||
// Define our routes | ||
#[derive(Route)] | ||
enum AppRoute { | ||
#[to("/")] | ||
Index, | ||
#[to("/about")] | ||
About, | ||
#[to("/post/<slug>")] | ||
Post { | ||
slug: String | ||
}, | ||
#[not_found] | ||
NotFound | ||
} | ||
|
||
// This is deliberately purely client-side rendered | ||
#[wasm_bindgen] | ||
pub fn run() -> Result<(), JsValue> { | ||
std::panic::set_hook(Box::new(console_error_panic_hook::hook)); | ||
// Get the root (for the router) we'll be injecting page content into | ||
let root = web_sys::window() | ||
.unwrap() | ||
.document() | ||
.unwrap() | ||
.query_selector("#_perseus_root") | ||
.unwrap() | ||
.unwrap(); | ||
|
||
sycamore::render_to( | ||
|| | ||
template! { | ||
BrowserRouter(|route: AppRoute| { | ||
match route { | ||
AppRoute::Index => app_shell!({ | ||
name => "index", | ||
props => pages::index::IndexPageProps, | ||
template => |props: Option<pages::index::IndexPageProps>| template! { | ||
pages::index::IndexPage(props.unwrap()) | ||
}, | ||
}), | ||
AppRoute::About => app_shell!({ | ||
name => "about", | ||
template => |_: Option<()>| template! { | ||
pages::about::AboutPage() | ||
}, | ||
}), | ||
AppRoute::Post { slug } => app_shell!({ | ||
name => &format!("post/{}", slug), | ||
props => pages::post::PostPageProps, | ||
template => |props: Option<pages::post::PostPageProps>| template! { | ||
pages::post::PostPage(props.unwrap()) | ||
}, | ||
}), | ||
AppRoute::NotFound => template! { | ||
p {"Not Found."} | ||
} | ||
} | ||
}) | ||
}, | ||
&root | ||
); | ||
|
||
Ok(()) | ||
} |
Oops, something went wrong.