Skip to content

Commit d450a81

Browse files
committed
feat: made custom servers easier and added example
Note that the Actix Web integration is currently completely broken, and live reloading seems to have problems. These will both be solved very soon. BREAKING CHANGE: Made custom servers take `ServerProps` and `(String, u16)` for the host and port.
1 parent 51ad962 commit d450a81

File tree

21 files changed

+271
-64
lines changed

21 files changed

+271
-64
lines changed

bonnie.toml

+1-11
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,7 @@ dev.subcommands.export-serve-deploy-relative.cmd.targets.windows = [
3535
dev.subcommands.export-serve-deploy-relative.args = [ "category", "example" ]
3636
dev.subcommands.export-serve-deploy-relative.desc = "deploys (exported) and serves the given example at a relative local path"
3737

38-
# TODO Make this not set the integration feature unless a certain file in the example calls for it
39-
dev.subcommands.example.cmd.generic = [
40-
"cd packages/perseus-cli",
41-
# Point this live version of the CLI at the given example
42-
"TEST_EXAMPLE=../../examples/%category/%example PERSEUS_CARGO_ARGS=\"--features \"perseus-integration/%EXAMPLE_INTEGRATION\"\" cargo run -- %%"
43-
]
44-
dev.subcommands.example.cmd.targets.windows = [
45-
"cd packages\\perseus-cli",
46-
# Point this live version of the CLI at the given example
47-
"powershell -Command { $env:TEST_EXAMPLE=\"..\\..\\examples\\%category\\%example\"; $end:PERSEUS_CARGO_ARGS=\"--features \"perseus-integration/%EXAMPLE_INTEGRATION\"\"; cargo run -- %% }"
48-
]
38+
dev.subcommands.example.cmd = "rust-script scripts/example.rs %category %example %EXAMPLE_INTEGRATION %%"
4939
dev.subcommands.example.args = [ "category", "example" ]
5040
dev.subcommands.example.env_vars = [ "EXAMPLE_INTEGRATION" ] # This will be set automatically to Warp by `.env` unless overridden
5141
dev.subcommands.example.desc = "runs the given example using a live version of the cli"

examples/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ There's also a `.base/` folder that contains a template that new examples should
77
Each of the examples here are fully self-contained Perseus apps, though they use relative path dependencies to the bleeding edge versions of the Perseus packages in this repository. They're also designed to be used with the local, bleeding-edge version of the CLI, which can be invoked by running `bonnie dev example <category> <example> <cli-command>`, where `<cli-command>` is any series of arguments you'd provide to the usual Perseus CLI.
88

99
If any of these examples don't work, please [open an issue](https://github.com/arctic-hen7/perseus/issues/choose) and let us know!
10+
11+
*Note: by default, all examples are assumed to use the `perseus-integration` helper crate, which allows testing them with all integrations. If this is not the case, add a `.integration_locked` file to the root of the example.*

examples/core/basic/src/templates/index.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ pub async fn get_build_state(
3434
_locale: String,
3535
) -> RenderFnResultWithCause<IndexPageState> {
3636
Ok(IndexPageState {
37-
greeting: "Hello World!".to_string(),
37+
greeting: "Hello Worlds!".to_string(),
3838
})
3939
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/

examples/core/custom_server/.integration_locked

Whitespace-only changes.
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "perseus-example-custom-server"
3+
version = "0.4.0-beta.2"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
perseus = { path = "../../../packages/perseus", features = [ "hydrate" ] }
10+
sycamore = "=0.8.0-beta.7"
11+
serde = { version = "1", features = ["derive"] }
12+
serde_json = "1"
13+
14+
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
15+
fantoccini = "0.17"
16+
17+
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
18+
tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] }
19+
perseus-warp = { path = "../../../packages/perseus-warp", features = [ "dflt-server" ] }
20+
warp = { package = "warp-fix-171", version = "0.3" } # Temporary until Warp #171 is resolved
21+
22+
[target.'cfg(target_arch = "wasm32")'.dependencies]
23+
wasm-bindgen = "0.2"
24+
25+
[lib]
26+
name = "lib"
27+
path = "src/lib.rs"
28+
crate-type = [ "cdylib", "rlib" ]
29+
30+
[[bin]]
31+
name = "perseus-example-custom-server"
32+
path = "src/lib.rs"

examples/core/custom_server/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Custom Server Example
2+
3+
This is an example of setting up a custom server with Perseus, with its own API routes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use perseus::{ErrorPages, Html};
2+
use sycamore::view;
3+
4+
pub fn get_error_pages<G: Html>() -> ErrorPages<G> {
5+
let mut error_pages = ErrorPages::new(|cx, url, status, err, _| {
6+
view! { cx,
7+
p { (format!("An error with HTTP code {} occurred at '{}': '{}'.", status, url, err)) }
8+
}
9+
});
10+
error_pages.add_page(404, |cx, _, _, _, _| {
11+
view! { cx,
12+
p { "Page not found." }
13+
}
14+
});
15+
16+
error_pages
17+
}
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
mod error_pages;
2+
mod templates;
3+
4+
use perseus::{Html, PerseusApp};
5+
6+
// pub fn get_app<G: Html>() -> PerseusApp<G> {
7+
// PerseusApp::new()
8+
// .template(crate::templates::index::get_template)
9+
// .template(crate::templates::about::get_template)
10+
// .error_pages(crate::error_pages::get_error_pages)
11+
// }
12+
13+
// #[perseus::engine_main]
14+
// async fn main() {
15+
// use perseus::builder::{get_op, run_dflt_engine};
16+
17+
// let op = get_op().unwrap();
18+
// let exit_code = run_dflt_engine(op, get_app, perseus_warp::dflt_server).await;
19+
// std::process::exit(exit_code);
20+
// }
21+
22+
// #[perseus::browser_main]
23+
// pub fn main() -> perseus::ClientReturn {
24+
// use perseus::run_client;
25+
26+
// run_client(get_app)
27+
// }
28+
29+
// Note: we use fully-qualified paths in the types to this function so we don't have to target-gate some more imports
30+
#[cfg(not(target_arch = "wasm32"))] // We only have access to `warp` etc. on the engine-side, so this function should only exist there
31+
pub async fn dflt_server<
32+
M: perseus::stores::MutableStore + 'static,
33+
T: perseus::internal::i18n::TranslationsManager + 'static,
34+
>(
35+
props: perseus::internal::serve::ServerProps<M, T>,
36+
(host, port): (String, u16),
37+
) {
38+
use perseus_warp::perseus_routes;
39+
use std::net::SocketAddr;
40+
use warp::Filter;
41+
42+
// The Warp integration takes a `SocketAddr`, so we convert the given host and port into that format
43+
let addr: SocketAddr = format!("{}:{}", host, port)
44+
.parse()
45+
.expect("Invalid address provided to bind to.");
46+
// Now, we generate the routes from the properties we were given
47+
// All integrations provide some function for setting them up that just takes those universal properties
48+
// Usually, you shouldn't ever have to worry about the value of the properties, which are set from your `PerseusApp` config
49+
let perseus_routes = perseus_routes(props).await;
50+
// And now set up our own routes
51+
// You could set up as many of these as you like in a production app
52+
// Note that they mustn't define anything under `/.perseus` or anything conflicting with any of your static aliases
53+
// This one will just echo whatever is sent to it
54+
let api_route = warp::path!("api" / "echo" / String).map(|msg| {
55+
// You can do absolutely anything in here that you can do with Warp as usual
56+
msg
57+
});
58+
// We could add as many routes as we wanted here, but the Perseus routes, no matter what integration you're using, MUST
59+
// always come last! This is because they define a wildcard handler for pages, which has to be defined last, or none of your routes
60+
// will do anything.
61+
let routes = api_route.or(perseus_routes);
62+
63+
warp::serve(routes).run(addr).await;
64+
65+
// If you try interacting with the app as usual, everything will work fine
66+
// If you try going to `/api/echo/test`, you'll get `test` printed back to you! Try replacing `test` with anything else
67+
// and it'll print whatever you put in back to you!
68+
}
69+
70+
#[perseus::main(dflt_server)]
71+
pub fn main<G: Html>() -> PerseusApp<G> {
72+
PerseusApp::new()
73+
.template(crate::templates::index::get_template)
74+
.template(crate::templates::about::get_template)
75+
.error_pages(crate::error_pages::get_error_pages)
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use perseus::Template;
2+
use sycamore::prelude::{view, Html, Scope, View};
3+
4+
#[perseus::template_rx]
5+
pub fn about_page<G: Html>(cx: Scope) -> View<G> {
6+
view! { cx,
7+
p { "About." }
8+
}
9+
}
10+
11+
pub fn get_template<G: Html>() -> Template<G> {
12+
Template::new("about").template(about_page)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use perseus::{Html, Template};
2+
use sycamore::prelude::{view, Scope, View};
3+
4+
#[perseus::template_rx]
5+
pub fn index_page<'a, G: Html>(cx: Scope<'a>) -> View<G> {
6+
view! { cx,
7+
p { "Hello World!" }
8+
a(href = "about", id = "about-link") { "About!" }
9+
}
10+
}
11+
12+
pub fn get_template<G: Html>() -> Template<G> {
13+
Template::new("index").template(index_page)
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod about;
2+
pub mod index;
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use fantoccini::{Client, Locator};
2+
use perseus::wait_for_checkpoint;
3+
4+
#[perseus::test]
5+
async fn main(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
6+
c.goto("http://localhost:8080").await?;
7+
wait_for_checkpoint!("begin", 0, c);
8+
let url = c.current_url().await?;
9+
assert!(url.as_ref().starts_with("http://localhost:8080"));
10+
11+
// The greeting was passed through using build state
12+
wait_for_checkpoint!("initial_state_present", 0, c);
13+
wait_for_checkpoint!("page_visible", 0, c);
14+
let greeting = c.find(Locator::Css("p")).await?.text().await?;
15+
assert_eq!(greeting, "Hello World!");
16+
// For some reason, retrieving the inner HTML or text of a `<title>` doens't work
17+
let title = c.find(Locator::Css("title")).await?.html(false).await?;
18+
assert!(title.contains("Index Page"));
19+
20+
// Go to `/about`
21+
c.find(Locator::Id("about-link")).await?.click().await?;
22+
let url = c.current_url().await?;
23+
assert!(url.as_ref().starts_with("http://localhost:8080/about"));
24+
wait_for_checkpoint!("initial_state_not_present", 0, c);
25+
wait_for_checkpoint!("page_visible", 1, c);
26+
// Make sure the hardcoded text there exists
27+
let text = c.find(Locator::Css("p")).await?.text().await?;
28+
assert_eq!(text, "About.");
29+
let title = c.find(Locator::Css("title")).await?.html(false).await?;
30+
assert!(title.contains("About Page"));
31+
// Make sure we get initial state if we refresh
32+
c.refresh().await?;
33+
wait_for_checkpoint!("initial_state_present", 0, c);
34+
35+
Ok(())
36+
}

packages/perseus-actix-web/src/dflt_server.rs

+5-10
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,23 @@ use crate::configurer;
22
use actix_web::{App, HttpServer};
33
use futures::executor::block_on;
44
use perseus::{
5-
builder::{get_host_and_port, get_props, get_standalone_and_act},
6-
internal::i18n::TranslationsManager,
7-
stores::MutableStore,
5+
internal::i18n::TranslationsManager, internal::serve::ServerProps, stores::MutableStore,
86
PerseusAppBase, SsrNode,
97
};
108

119
/// Creates and starts the default Perseus server using Actix Web. This should be run in a `main()` function annotated with `#[tokio::main]` (which requires the `macros` and
1210
/// `rt-multi-thread` features on the `tokio` dependency).
1311
pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'static>(
14-
app: impl Fn() -> PerseusAppBase<SsrNode, M, T> + 'static + Send + Sync + Clone,
12+
props: ServerProps<M, T>,
13+
(host, port): (String, u16),
1514
) {
16-
get_standalone_and_act();
17-
let (host, port) = get_host_and_port();
18-
15+
// TODO Fix issues here
1916
HttpServer::new(move ||
2017
App::new()
2118
.configure(
2219
block_on(
2320
configurer(
24-
get_props(
25-
app()
26-
)
21+
props
2722
)
2823
)
2924
)

packages/perseus-axum/src/dflt_server.rs

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
use crate::get_router;
2-
use futures::executor::block_on;
32
use perseus::{
4-
builder::{get_host_and_port, get_props, get_standalone_and_act},
5-
internal::i18n::TranslationsManager,
6-
stores::MutableStore,
3+
internal::i18n::TranslationsManager, internal::serve::ServerProps, stores::MutableStore,
74
PerseusAppBase, SsrNode,
85
};
96
use std::net::SocketAddr;
107

118
/// Creates and starts the default Perseus server with Axum. This should be run in a `main` function annotated with `#[tokio::main]` (which requires the `macros` and
129
/// `rt-multi-thread` features on the `tokio` dependency).
1310
pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'static>(
14-
app: impl Fn() -> PerseusAppBase<SsrNode, M, T> + 'static + Send + Sync + Clone,
11+
props: ServerProps<M, T>,
12+
(host, port): (String, u16),
1513
) {
16-
get_standalone_and_act();
17-
let props = get_props(app());
18-
let (host, port) = get_host_and_port();
1914
let addr: SocketAddr = format!("{}:{}", host, port)
2015
.parse()
2116
.expect("Invalid address provided to bind to.");
22-
let app = block_on(get_router(props));
17+
let app = get_router(props).await;
2318
axum::Server::bind(&addr)
2419
.serve(app.into_make_service())
2520
.await

packages/perseus-cli/src/bin/main.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ async fn core(dir: PathBuf) -> Result<i32, Error> {
134134
for entry in std::fs::read_dir(".")
135135
.map_err(|err| WatchError::ReadCurrentDirFailed { source: err })?
136136
{
137-
// We want to exclude `target/` and `.perseus/`, otherwise we should watch everything
137+
// We want to exclude `target/` and `dist`, otherwise we should watch everything
138138
let entry = entry.map_err(|err| WatchError::ReadDirEntryFailed { source: err })?;
139139
let name = entry.file_name();
140-
if name != "target" && name != ".perseus" && name != ".git" {
140+
if name != "target" && name != "dist" && name != ".git" {
141141
watcher
142142
.watch(&entry.path(), RecursiveMode::Recursive)
143143
.map_err(|err| WatchError::WatchFileFailed {
+4-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
use crate::perseus_routes;
2-
use futures::executor::block_on;
32
use perseus::{
4-
builder::{get_host_and_port, get_props, get_standalone_and_act},
5-
internal::i18n::TranslationsManager,
3+
internal::{i18n::TranslationsManager, serve::ServerProps},
64
stores::MutableStore,
7-
PerseusAppBase, SsrNode,
85
};
96
use std::net::SocketAddr;
107

118
/// Creates and starts the default Perseus server with Warp. This should be run in a `main` function annotated with `#[tokio::main]` (which requires the `macros` and
129
/// `rt-multi-thread` features on the `tokio` dependency).
1310
pub async fn dflt_server<M: MutableStore + 'static, T: TranslationsManager + 'static>(
14-
app: impl Fn() -> PerseusAppBase<SsrNode, M, T> + 'static + Send + Sync + Clone,
11+
props: ServerProps<M, T>,
12+
(host, port): (String, u16),
1513
) {
16-
get_standalone_and_act();
17-
let props = get_props(app());
18-
let (host, port) = get_host_and_port();
1914
let addr: SocketAddr = format!("{}:{}", host, port)
2015
.parse()
2116
.expect("Invalid address provided to bind to.");
22-
let routes = block_on(perseus_routes(props));
17+
let routes = perseus_routes(props).await;
2318
warp::serve(routes).run(addr).await;
2419
}

packages/perseus/src/engine/dflt_engine.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// This file contains functions exclusive to the default engine systems
22

3-
use super::EngineOperation;
4-
use crate::{i18n::TranslationsManager, stores::MutableStore, PerseusAppBase, SsrNode};
3+
use super::{get_host_and_port, get_props, EngineOperation};
4+
use crate::{
5+
i18n::TranslationsManager, server::ServerProps, stores::MutableStore, PerseusAppBase, SsrNode,
6+
};
57
use fmterr::fmt_err;
68
use futures::Future;
79
use std::env;
@@ -14,7 +16,7 @@ where
1416
T: TranslationsManager,
1517
A: Fn() -> PerseusAppBase<SsrNode, M, T> + 'static + Send + Sync + Clone,
1618
{
17-
let serve_fn = |_app: A| async {
19+
let serve_fn = |_, _| async {
1820
panic!("`run_dflt_engine_export_only` cannot run a server; you should use `run_dflt_engine` instead and import a server integration (e.g. `perseus-warp`)")
1921
};
2022
run_dflt_engine(op, app, serve_fn).await
@@ -33,7 +35,7 @@ where
3335
pub async fn run_dflt_engine<M, T, F, A>(
3436
op: EngineOperation,
3537
app: A,
36-
serve_fn: impl Fn(A) -> F,
38+
serve_fn: impl Fn(ServerProps<M, T>, (String, u16)) -> F,
3739
) -> i32
3840
where
3941
M: MutableStore,
@@ -92,7 +94,11 @@ where
9294
}
9395
}
9496
EngineOperation::Serve => {
95-
serve_fn(app).await;
97+
// To reduce friction for default servers and user-made servers, we automatically do the boilerplate that all servers would have to do
98+
let props = get_props(app());
99+
// This returns a `(String, u16)` of the host and port for maximum compatibility
100+
let addr = get_host_and_port();
101+
serve_fn(props, addr).await;
96102
0
97103
}
98104
EngineOperation::Tinker => {

packages/perseus/src/engine/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ mod get_op;
1616
pub use get_op::{get_op, EngineOperation};
1717

1818
mod serve;
19-
pub use serve::{get_host_and_port, get_props, get_standalone_and_act};
19+
pub use serve::{get_host_and_port, get_props};

0 commit comments

Comments
 (0)