#![forbid(unsafe_code)]
pub mod errors;
use crate::errors::LeptosConfigError;
use config::{Case, Config, File, FileFormat};
use regex::Regex;
use std::{
env::VarError,
fs,
net::SocketAddr,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use typed_builder::TypedBuilder;
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct LeptosOptions {
#[builder(setter(into))]
pub output_name: Arc<str>,
#[builder(setter(into), default=default_site_root())]
#[serde(default = "default_site_root")]
pub site_root: Arc<str>,
#[builder(setter(into), default=default_site_pkg_dir())]
#[serde(default = "default_site_pkg_dir")]
pub site_pkg_dir: Arc<str>,
#[builder(setter(into), default=default_env())]
#[serde(default = "default_env")]
pub env: Env,
#[builder(setter(into), default=default_site_addr())]
#[serde(default = "default_site_addr")]
pub site_addr: SocketAddr,
#[builder(default = default_reload_port())]
#[serde(default = "default_reload_port")]
pub reload_port: u32,
#[builder(default)]
#[serde(default)]
pub reload_external_port: Option<u32>,
#[builder(default)]
#[serde(default)]
pub reload_ws_protocol: ReloadWSProtocol,
#[builder(default = default_not_found_path())]
#[serde(default = "default_not_found_path")]
pub not_found_path: Arc<str>,
#[builder(default = default_hash_file_name())]
#[serde(default = "default_hash_file_name")]
pub hash_file: Arc<str>,
#[builder(default = default_hash_files())]
#[serde(default = "default_hash_files")]
pub hash_files: bool,
#[builder(default, setter(strip_option))]
#[serde(default)]
pub server_fn_prefix: Option<String>,
#[builder(default)]
#[serde(default)]
pub disable_server_fn_hash: bool,
#[builder(default)]
#[serde(default)]
pub server_fn_mod_path: bool,
}
impl LeptosOptions {
pub fn css_file_path(&self) -> PathBuf {
Path::new(&*self.site_root)
.join(&*self.site_pkg_dir)
.join(format!("{}.css", self.output_name))
}
pub fn css_path(&self) -> String {
let mut path = self.site_pkg_dir_route_base();
path.push_str(&self.output_name);
path.push_str(".css");
path
}
pub fn site_pkg_dir_route_base(&self) -> String {
let mut path = String::new();
if !self.site_pkg_dir.starts_with('/') {
path.push('/');
}
path.push_str(&self.site_pkg_dir);
if !path.ends_with('/') {
path.push('/');
}
path
}
fn try_from_env() -> Result<Self, LeptosConfigError> {
let output_name = env_w_default(
"LEPTOS_OUTPUT_NAME",
std::option_env!("LEPTOS_OUTPUT_NAME",).unwrap_or_default(),
)?;
if output_name.is_empty() {
eprintln!(
"It looks like you're trying to compile Leptos without the \
LEPTOS_OUTPUT_NAME environment variable being set. There are \
two options\n 1. cargo-leptos is not being used, but \
get_configuration() is being passed None. This needs to be \
changed to Some(\"Cargo.toml\")\n 2. You are compiling \
Leptos without LEPTOS_OUTPUT_NAME being set with \
cargo-leptos. This shouldn't be possible!"
);
}
Ok(LeptosOptions {
output_name: output_name.into(),
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?.into(),
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?.into(),
env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
.parse()?,
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
.parse()?,
reload_external_port: match env_wo_default(
"LEPTOS_RELOAD_EXTERNAL_PORT",
)? {
Some(val) => Some(val.parse()?),
None => None,
},
reload_ws_protocol: ws_from_str(
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
)?,
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?
.into(),
hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
.into(),
hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
.is_some(),
server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
})
}
}
fn default_site_root() -> Arc<str> {
".".into()
}
fn default_site_pkg_dir() -> Arc<str> {
"pkg".into()
}
fn default_env() -> Env {
Env::DEV
}
fn default_site_addr() -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], 3000))
}
fn default_reload_port() -> u32 {
3001
}
fn default_not_found_path() -> Arc<str> {
"/404".into()
}
fn default_hash_file_name() -> Arc<str> {
"hash.txt".into()
}
fn default_hash_files() -> bool {
false
}
fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(Some(val)),
Err(VarError::NotPresent) => Ok(None),
Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
}
}
fn env_w_default(
key: &str,
default: &str,
) -> Result<String, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(val),
Err(VarError::NotPresent) => Ok(default.to_string()),
Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
}
}
pub const ENV_DEV_KEY_SHORT: &str = "dev";
pub const ENV_DEV_KEY_LONG: &str = "development";
pub const ENV_PROD_KEY_SHORT: &str = "prod";
pub const ENV_PROD_KEY_LONG: &str = "production";
#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq, Default)]
pub enum Env {
PROD,
#[default]
DEV,
}
fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
ENV_DEV_KEY_SHORT | ENV_DEV_KEY_LONG => Ok(Env::DEV),
ENV_PROD_KEY_SHORT | ENV_PROD_KEY_LONG => Ok(Env::PROD),
_ => Err(LeptosConfigError::EnvVarError(format!(
"{input} is not a supported environment. Use either \
`{ENV_DEV_KEY_SHORT}`, `{ENV_DEV_KEY_LONG}`, \
`{ENV_PROD_KEY_SHORT}`, or `{ENV_PROD_KEY_LONG}`.",
))),
}
}
impl FromStr for Env {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
env_from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for Env {
fn from(str: &str) -> Self {
env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
}
impl From<&Result<String, VarError>> for Env {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => {
env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
Err(_) => Self::default(),
}
}
}
impl TryFrom<String> for Env {
type Error = LeptosConfigError;
fn try_from(s: String) -> Result<Self, Self::Error> {
env_from_str(s.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
struct EnvVisitor;
impl<'de> serde::de::Visitor<'de> for EnvVisitor {
type Value = Env;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(
formatter,
"a case-insensitive string of either `{ENV_DEV_KEY_SHORT}`, \
`{ENV_DEV_KEY_LONG}`, `{ENV_PROD_KEY_SHORT}`, or \
`{ENV_PROD_KEY_LONG}`"
)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
env_from_str(v).map_err(|err| E::custom(err.to_string()))
}
}
impl<'de> serde::Deserialize<'de> for Env {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(EnvVisitor)
}
}
#[derive(
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
)]
pub enum ReloadWSProtocol {
#[default]
WS,
WSS,
}
fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, LeptosConfigError> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"ws" | "WS" => Ok(ReloadWSProtocol::WS),
"wss" | "WSS" => Ok(ReloadWSProtocol::WSS),
_ => Err(LeptosConfigError::EnvVarError(format!(
"{input} is not a supported websocket protocol. Use only `ws` or \
`wss`.",
))),
}
}
impl FromStr for ReloadWSProtocol {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
ws_from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for ReloadWSProtocol {
fn from(str: &str) -> Self {
ws_from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
}
impl From<&Result<String, VarError>> for ReloadWSProtocol {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => ws_from_str(str).unwrap_or_else(|err| panic!("{}", err)),
Err(_) => Self::default(),
}
}
}
impl TryFrom<String> for ReloadWSProtocol {
type Error = LeptosConfigError;
fn try_from(s: String) -> Result<Self, Self::Error> {
ws_from_str(s.as_str())
}
}
pub fn get_config_from_str(
text: &str,
) -> Result<LeptosOptions, LeptosConfigError> {
let re: Regex = Regex::new(r"(?m)^\[package.metadata.leptos\]").unwrap();
let re_workspace: Regex =
Regex::new(r"(?m)^\[\[workspace.metadata.leptos\]\]").unwrap();
let metadata_name;
let start;
match re.find(text) {
Some(found) => {
metadata_name = "[package.metadata.leptos]";
start = found.start();
}
None => match re_workspace.find(text) {
Some(found) => {
metadata_name = "[[workspace.metadata.leptos]]";
start = found.start();
}
None => return Err(LeptosConfigError::ConfigSectionNotFound),
},
};
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input.replace(metadata_name, "");
let settings = Config::builder()
.add_source(File::from_str(&toml, FileFormat::Toml))
.add_source(
config::Environment::with_prefix("LEPTOS")
.convert_case(Case::Kebab),
)
.build()?;
settings
.try_deserialize()
.map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
}
pub fn get_configuration(
path: Option<&str>,
) -> Result<ConfFile, LeptosConfigError> {
if let Some(path) = path {
get_config_from_file(path)
} else {
get_config_from_env()
}
}
pub fn get_config_from_file<P: AsRef<Path>>(
path: P,
) -> Result<ConfFile, LeptosConfigError> {
let text = fs::read_to_string(path)
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
let leptos_options = get_config_from_str(&text)?;
Ok(ConfFile { leptos_options })
}
pub fn get_config_from_env() -> Result<ConfFile, LeptosConfigError> {
Ok(ConfFile {
leptos_options: LeptosOptions::try_from_env()?,
})
}
#[path = "tests.rs"]
#[cfg(test)]
mod tests;