Commit WIP version of common config struct that writes a KDL file for cargo-leptos

This commit is contained in:
Ben Wishovich 2022-12-04 14:50:36 -08:00
parent 7b376b6d3a
commit f63cb02277
13 changed files with 158 additions and 29 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ blob.rs
Cargo.lock Cargo.lock
**/*.rs.bk **/*.rs.bk
.DS_Store .DS_Store
.leptos.kdl

View file

@ -4,6 +4,7 @@ members = [
"leptos", "leptos",
"leptos_dom", "leptos_dom",
"leptos_core", "leptos_core",
"leptos_config",
"leptos_macro", "leptos_macro",
"leptos_reactive", "leptos_reactive",
"leptos_server", "leptos_server",

View file

@ -12,6 +12,7 @@ if #[cfg(feature = "ssr")] {
use http::StatusCode; use http::StatusCode;
use std::net::SocketAddr; use std::net::SocketAddr;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use leptos_axum::RenderOptions;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -36,12 +37,14 @@ if #[cfg(feature = "ssr")] {
) )
} }
let render_options: RenderOptions = leptos_axum::RenderOptionsBuilder::default().client_pkg_path("/pkg/leptos_hackernews_axum").auto_reload(3001).build().expect("Failed to Parse RenderOptions");
// build our application with a route // build our application with a route
let app = Router::new() let app = Router::new()
// `GET /` goes to `root` // `GET /` goes to `root`
.nest_service("/pkg", pkg_service) .nest_service("/pkg", pkg_service)
.nest_service("/static", static_service) .nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream("/pkg/leptos_hackernews_axum", |cx| view! { cx, <App/> })); .fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <App/> }));
// run our app with hyper // run our app with hyper
// `axum::Server` is a re-export of `hyper::Server` // `axum::Server` is a re-export of `hyper::Server`

View file

@ -14,7 +14,7 @@ if #[cfg(feature = "ssr")] {
use todo_app_sqlite_axum::*; use todo_app_sqlite_axum::*;
use http::StatusCode; use http::StatusCode;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use leptos_axum::RenderOptions; use std::env;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -45,8 +45,9 @@ if #[cfg(feature = "ssr")] {
) )
} }
let render_options: RenderOptions = leptos_axum::RenderOptionsBuilder::default().client_pkg_path("/pkg/todo_app_sqlite_axum").auto_reload(3001).build().expect("Failed to Parse RenderOptions");
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite_axum").reload_port(3001).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
// build our application with a route // build our application with a route
let app = Router::new() let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .route("/api/*fn_name", post(leptos_axum::handle_server_fns))

View file

@ -12,9 +12,9 @@ cfg_if! {
} }
pub fn register_server_functions() { pub fn register_server_functions() {
GetTodos::register(); _ = GetTodos::register();
AddTodo::register(); _ = AddTodo::register();
DeleteTodo::register(); _ = DeleteTodo::register();
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@ -34,7 +34,7 @@ cfg_if! {
} }
#[server(GetTodos, "/api")] #[server(GetTodos, "/api")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> { pub async fn get_todos(_cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers // this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this // http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
// let req = use_context::<http::Request<axum::body::BoxBody>>(cx) // let req = use_context::<http::Request<axum::body::BoxBody>>(cx)
@ -70,7 +70,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
.execute(&mut conn) .execute(&mut conn)
.await .await
{ {
Ok(row) => Ok(()), Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())), Err(e) => Err(ServerFnError::ServerError(e.to_string())),
} }
} }
@ -167,7 +167,7 @@ pub fn Todos(cx: Scope) -> Element {
<li> <li>
{todo.title} {todo.title}
<ActionForm action=delete_todo.clone()> <ActionForm action=delete_todo.clone()>
<input type="hidden" name="id" value={todo.id}/> <input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/> <input type="submit" value="X"/>
</ActionForm> </ActionForm>
</li> </li>

View file

@ -9,6 +9,7 @@ cfg_if! {
use actix_files::{Files}; use actix_files::{Files};
use actix_web::*; use actix_web::*;
use crate::todo::*; use crate::todo::*;
use std::env;
#[get("/style.css")] #[get("/style.css")]
async fn css() -> impl Responder { async fn css() -> impl Responder {
@ -26,11 +27,14 @@ cfg_if! {
crate::todo::register_server_functions(); crate::todo::register_server_functions();
HttpServer::new(|| { HttpServer::new(|| {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new() App::new()
.service(Files::new("/pkg", "./pkg")) .service(Files::new("/pkg", "./pkg"))
.service(css) .service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream("/pkg/todo_app_sqlite", |cx| view! { cx, <TodoApp/> })) .route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default()) //.wrap(middleware::Compress::default())
}) })
.bind(("127.0.0.1", 8083))? .bind(("127.0.0.1", 8083))?

View file

@ -138,10 +138,11 @@ pub fn handle_server_fns() -> Route {
/// # } /// # }
/// ``` /// ```
pub fn render_app_to_stream( pub fn render_app_to_stream(
client_pkg_path: &'static str, options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static, app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
) -> Route { ) -> Route {
web::get().to(move |req: HttpRequest| { web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone(); let app_fn = app_fn.clone();
async move { async move {
let path = req.path(); let path = req.path();
@ -165,12 +166,39 @@ pub fn render_app_to_stream(
} }
}; };
let head = format!(r#"<!DOCTYPE html> let pkg_path = &options.pkg_path;
<html>
let leptos_autoreload = match options.reload_port {
Some(port) => match &options.environment {
RustEnv::DEV => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://127.0.0.1:{port}/autoreload');
ws.onmessage = (ev) => {{
console.log(`Reload message: `);
if (ev.data === 'reload') window.location.reload();
}};
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
}})()
</script>
"#
),
RustEnv::PROD => "".to_string(),
},
None => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, {{ hydrate }} from '{client_pkg_path}.js'; init().then(hydrate);</script>"#); <script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>"; let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming( HttpResponse::Ok().content_type("text/html").streaming(

View file

@ -11,6 +11,7 @@ description = "Axum integrations for the Leptos web framework."
axum = "0.6" axum = "0.6"
derive_builder = "0.12.0" derive_builder = "0.12.0"
futures = "0.3" futures = "0.3"
kdl = "4.6.0"
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [ leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
"ssr", "ssr",
] } ] }

View file

@ -4,7 +4,6 @@ use axum::{
http::{HeaderMap, HeaderValue, Request, StatusCode}, http::{HeaderMap, HeaderValue, Request, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use derive_builder::Builder;
use futures::{Future, SinkExt, Stream, StreamExt}; use futures::{Future, SinkExt, Stream, StreamExt};
use leptos::*; use leptos::*;
use leptos_meta::MetaContext; use leptos_meta::MetaContext;
@ -128,14 +127,6 @@ pub async fn handle_server_fns(
pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>; pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
#[derive(Default, Builder, Clone)]
pub struct RenderOptions {
#[builder(setter(into))]
client_pkg_path: String,
#[builder(setter(strip_option), default)]
auto_reload: Option<u32>,
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries /// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application. /// to route it using [leptos_router], serving an HTML stream of your application.
/// ///
@ -201,11 +192,12 @@ pub fn render_app_to_stream(
full_path = "http://leptos".to_string() + &path.to_string() full_path = "http://leptos".to_string() + &path.to_string()
} }
let client_pkg_path = &options.client_pkg_path; let pkg_path = &options.pkg_path;
let leptos_autoreload = match options.auto_reload { let leptos_autoreload = match options.reload_port {
Some(port) => format!( Some(port) => match &options.environment {
r#" RustEnv::DEV => format!(
r#"
<script crossorigin="">(function () {{ <script crossorigin="">(function () {{
var ws = new WebSocket('ws://127.0.0.1:{port}/autoreload'); var ws = new WebSocket('ws://127.0.0.1:{port}/autoreload');
ws.onmessage = (ev) => {{ ws.onmessage = (ev) => {{
@ -216,7 +208,9 @@ pub fn render_app_to_stream(
}})() }})()
</script> </script>
"# "#
), ),
RustEnv::PROD => "".to_string(),
},
None => "".to_string(), None => "".to_string(),
}; };
@ -226,7 +220,7 @@ pub fn render_app_to_stream(
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, {{ hydrate }} from '{client_pkg_path}.js'; init().then(hydrate);</script> <script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
{leptos_autoreload} {leptos_autoreload}
"# "#
); );

View file

@ -10,6 +10,7 @@ readme = "../README.md"
[dependencies] [dependencies]
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.19" } leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.19" }
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.1" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" } leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.19" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.19" } leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.19" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" } leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.19" }

View file

@ -141,6 +141,7 @@
//! # } //! # }
//! ``` //! ```
pub use leptos_config::*;
pub use leptos_core::*; pub use leptos_core::*;
pub use leptos_dom; pub use leptos_dom;
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt}; pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};

11
leptos_config/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "leptos_config"
version = "0.0.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Configuraiton for Leptos"
[dependencies]
typed-builder = "0.11.0"

83
leptos_config/src/lib.rs Normal file
View file

@ -0,0 +1,83 @@
use std::{env::VarError, str::FromStr};
use typed_builder::TypedBuilder;
#[derive(Default, TypedBuilder, Clone)]
pub struct RenderOptions {
#[builder(setter(into))]
pub pkg_path: String,
#[builder(setter(into))]
pub environment: RustEnv,
#[builder(setter(strip_option), default)]
pub reload_port: Option<u32>,
}
impl RenderOptions {
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings
pub fn write_to_file(&self) {
use std::fs;
let options = format!(
r#"render_options: {{
pkg_path {}
environment {:?}
reload_port {:?}
}}
"#,
self.pkg_path, self.environment, self.reload_port
);
fs::write("./.leptos.kdl", options).expect("Unable to write file");
}
}
#[derive(Default, Debug, Clone)]
pub enum RustEnv {
#[default]
PROD,
DEV,
}
impl FromStr for RustEnv {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
_ => Ok(Self::PROD),
}
}
}
impl From<&str> for RustEnv {
fn from(str: &str) -> Self {
let sanitized = str.to_lowercase();
match sanitized.as_str() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
}
impl From<&Result<String, VarError>> for RustEnv {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => {
let sanitized = str.to_lowercase();
match sanitized.as_ref() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
Err(_) => Self::PROD,
}
}
}