WIP Implementation of expanded LeptosOptions, which interop with the cargo-leptos beta and allow configuration of Leptos. Adds id option to <Stylesheet/>

This commit is contained in:
Ben Wishovich 2022-12-20 12:14:05 -08:00
parent 80287f7a61
commit d5fbeb9474
2 changed files with 99 additions and 46 deletions

View file

@ -1,67 +1,61 @@
pub mod errors;
use crate::errors::LeptosConfigError;
use config::{Config, File, FileFormat};
use regex::Regex;
use std::convert::{TryFrom, TryInto};
use std::fs;
use std::{env::VarError, net::SocketAddr, str::FromStr};
use typed_builder::TypedBuilder;
/// This struct serves as a convenient place to store details used for rendering.
/// It's serialized into a file in the root called `.leptos.kdl` for cargo-leptos
/// to watch. It's also used in our actix and axum integrations to generate the
/// correct path for WASM, JS, and Websockets. Its goal is to be the single source
/// of truth for render options
#[derive(TypedBuilder, Clone)]
pub struct RenderOptions {
/// The path and name of the WASM and JS files generated by wasm-bindgen
/// For example, `/pkg/app` might be a valid input if your crate name was `app`.
/// This struct serves as a convenient place to store details used for configuring Leptos.
/// It's used in our actix and axum integrations to generate the
/// correct path for WASM, JS, and Websockets, as well as other configuration tasks.
/// It shares keys with cargo-leptos, to allow for easy interoperability
#[derive(TypedBuilder, Clone, serde::Deserialize)]
pub struct LeptosOptions {
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
/// By default, wasm-bindgen puts them in `/pkg`.
#[builder(setter(into))]
pub pkg_path: String,
/// Used to control whether the Websocket code for code watching is included.
/// I recommend passing in the result of `env::var("RUST_ENV")`
/// The name of the WASM and JS files generated by wasm-bindgen.
#[builder(setter(into))]
pub pkg_name: String,
/// Used to configure the running environment of Leptos. Can be used to load dev constants and keys v prod, or change
/// things based on the deployment environment
/// I recommend passing in the result of `env::var("LEPTOS__ENV")`
#[builder(setter(into), default)]
pub environment: RustEnv,
pub env: Env,
/// Provides a way to control the address leptos is served from.
/// Using an env variable here would allow you to run the same code in dev and prod
/// Defaults to `127.0.0.1:3000`
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
pub socket_address: SocketAddr,
pub site_address: SocketAddr,
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
/// Defaults to `3001`
#[builder(default = 3001)]
pub reload_port: u32,
/// This controls whether the Leptos Websocket Autoreload JS is included for each page
/// Defaults to false
#[builder(default = false)]
pub leptos_watch: bool,
}
impl RenderOptions {
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings. We do not read from this file
/// only write to it, you'll want to change the settings in your main function when you create RenderOptions
pub fn write_to_file(&self) {
use std::fs;
let options = format!(
r#"// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
RenderOptions {{
pkg-path "{}"
environment "{:?}"
socket-address "{:?}"
reload-port {:?}
}}
"#,
self.pkg_path, self.environment, self.socket_address, self.reload_port
);
fs::write("./.leptos.kdl", options).expect("Unable to write file");
}
}
/// An enum that can be used to define the environment Leptos is running in. Can be passed to RenderOptions.
/// Setting this to the PROD variant will not include the websockets code for cargo-leptos' watch.
/// Defaults to PROD
#[derive(Debug, Clone)]
pub enum RustEnv {
/// Defaults to DEV
#[derive(Debug, Clone, serde::Deserialize)]
pub enum Env {
PROD,
DEV,
}
impl Default for RustEnv {
impl Default for Env {
fn default() -> Self {
Self::PROD
Self::DEV
}
}
impl FromStr for RustEnv {
impl FromStr for Env {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
let sanitized = input.to_lowercase();
@ -70,12 +64,12 @@ impl FromStr for RustEnv {
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
_ => Ok(Self::PROD),
_ => Ok(Self::DEV),
}
}
}
impl From<&str> for RustEnv {
impl From<&str> for Env {
fn from(str: &str) -> Self {
let sanitized = str.to_lowercase();
match sanitized.as_str() {
@ -84,12 +78,12 @@ impl From<&str> for RustEnv {
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
}
}
}
}
impl From<&Result<String, VarError>> for RustEnv {
impl From<&Result<String, VarError>> for Env {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => {
@ -100,11 +94,64 @@ impl From<&Result<String, VarError>> for RustEnv {
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
}
}
}
Err(_) => Self::PROD,
Err(_) => Self::DEV,
}
}
}
impl TryFrom<String> for Env {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
other => Err(format!(
"{} is not a supported environment. Use either `dev` or `production`.",
other
)),
}
}
}
pub async fn get_configuration() -> Result<LeptosOptions, LeptosConfigError> {
let text = fs::read_to_string("Cargo.toml").map_err(|_| LeptosConfigError::ConfigNotFound)?;
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
let start = match re.find(&text) {
Some(found) => found.start(),
None => return Err(LeptosConfigError::ConfigSectionNotFound),
};
println!("Config file content:\n{text}");
// so that serde error messages have right line number
let newlines = text[..start].matches('\n').count();
let toml = "\n".repeat(newlines) + &text[start..];
// Detect the running environment.
// Default to `local` if unspecified.
let environment: Env = std::env::var("LEPTOS_ENV")
.unwrap_or_else(|_| "dev".into())
.try_into()
.map_err(|_| LeptosConfigError::EnvError)?; //shouldn't happen
println!("Loading configuration for environment: {:?}", environment);
let settings = Config::builder()
// Read the "default" configuration file
.add_source(File::from_str(&toml, FileFormat::Toml))
// Layer on the environment-specific values.
// Add in settings from environment variables (with a prefix of APP and '__' as separator)
// E.g. `APP_APPLICATION__PORT=5001 would set `Settings.application.port`
.add_source(config::Environment::with_prefix("LEPTOS").separator("__"))
.build()?;
settings
.try_deserialize()
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
}

View file

@ -27,6 +27,9 @@ pub struct StylesheetProps {
/// The URL at which the stylesheet can be located.
#[builder(setter(into))]
pub href: String,
/// The URL at which the stylesheet can be located.
#[builder(setter(into, strip_option))]
pub id: Option<String>,
}
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
@ -49,7 +52,7 @@ pub struct StylesheetProps {
/// ```
#[allow(non_snake_case)]
pub fn Stylesheet(cx: Scope, props: StylesheetProps) {
let StylesheetProps { href } = props;
let StylesheetProps { href, id } = props;
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
use leptos::document;
@ -66,6 +69,9 @@ pub fn Stylesheet(cx: Scope, props: StylesheetProps) {
} else {
let el = document().create_element("link").unwrap_throw();
el.set_attribute("rel", "stylesheet").unwrap_throw();
if let Some(id) = id{
el.set_attribute("id", &id).unwrap_throw();
}
el.set_attribute("href", &href).unwrap_throw();
document()
.query_selector("head")