1#![forbid(unsafe_code)]
2
3pub mod errors;
4
5use crate::errors::LeptosConfigError;
6use config::{Case, Config, File, FileFormat};
7use regex::Regex;
8use std::{
9 env::VarError, fs, net::SocketAddr, path::Path, str::FromStr, sync::Arc,
10};
11use typed_builder::TypedBuilder;
12
13#[derive(Clone, Debug, serde::Deserialize)]
16#[serde(rename_all = "kebab-case")]
17#[non_exhaustive]
18pub struct ConfFile {
19 pub leptos_options: LeptosOptions,
20}
21
22#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
27#[serde(rename_all = "kebab-case")]
28#[non_exhaustive]
29pub struct LeptosOptions {
30 #[builder(setter(into))]
36 pub output_name: Arc<str>,
37 #[builder(setter(into), default=default_site_root())]
40 #[serde(default = "default_site_root")]
41 pub site_root: Arc<str>,
42 #[builder(setter(into), default=default_site_pkg_dir())]
45 #[serde(default = "default_site_pkg_dir")]
46 pub site_pkg_dir: Arc<str>,
47 #[builder(setter(into), default=default_env())]
51 #[serde(default = "default_env")]
52 pub env: Env,
53 #[builder(setter(into), default=default_site_addr())]
57 #[serde(default = "default_site_addr")]
58 pub site_addr: SocketAddr,
59 #[builder(default = default_reload_port())]
62 #[serde(default = "default_reload_port")]
63 pub reload_port: u32,
64 #[builder(default)]
67 #[serde(default)]
68 pub reload_external_port: Option<u32>,
69 #[builder(default)]
72 #[serde(default)]
73 pub reload_ws_protocol: ReloadWSProtocol,
74 #[builder(default = default_not_found_path())]
76 #[serde(default = "default_not_found_path")]
77 pub not_found_path: Arc<str>,
78 #[builder(default = default_hash_file_name())]
80 #[serde(default = "default_hash_file_name")]
81 pub hash_file: Arc<str>,
82 #[builder(default = default_hash_files())]
85 #[serde(default = "default_hash_files")]
86 pub hash_files: bool,
87 #[builder(default, setter(strip_option))]
93 #[serde(default)]
94 pub server_fn_prefix: Option<String>,
95 #[builder(default)]
110 #[serde(default)]
111 pub disable_server_fn_hash: bool,
112 #[builder(default)]
119 #[serde(default)]
120 pub server_fn_mod_path: bool,
121}
122
123impl LeptosOptions {
124 fn try_from_env() -> Result<Self, LeptosConfigError> {
125 let output_name = env_w_default(
126 "LEPTOS_OUTPUT_NAME",
127 std::option_env!("LEPTOS_OUTPUT_NAME",).unwrap_or_default(),
128 )?;
129 if output_name.is_empty() {
130 eprintln!(
131 "It looks like you're trying to compile Leptos without the \
132 LEPTOS_OUTPUT_NAME environment variable being set. There are \
133 two options\n 1. cargo-leptos is not being used, but \
134 get_configuration() is being passed None. This needs to be \
135 changed to Some(\"Cargo.toml\")\n 2. You are compiling \
136 Leptos without LEPTOS_OUTPUT_NAME being set with \
137 cargo-leptos. This shouldn't be possible!"
138 );
139 }
140 Ok(LeptosOptions {
141 output_name: output_name.into(),
142 site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?.into(),
143 site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?.into(),
144 env: env_from_str(env_w_default("LEPTOS_ENV", "DEV")?.as_str())?,
145 site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
146 .parse()?,
147 reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
148 .parse()?,
149 reload_external_port: match env_wo_default(
150 "LEPTOS_RELOAD_EXTERNAL_PORT",
151 )? {
152 Some(val) => Some(val.parse()?),
153 None => None,
154 },
155 reload_ws_protocol: ws_from_str(
156 env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
157 )?,
158 not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?
159 .into(),
160 hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?
161 .into(),
162 hash_files: env_w_default("LEPTOS_HASH_FILES", "false")?.parse()?,
163 server_fn_prefix: env_wo_default("SERVER_FN_PREFIX")?,
164 disable_server_fn_hash: env_wo_default("DISABLE_SERVER_FN_HASH")?
165 .is_some(),
166 server_fn_mod_path: env_wo_default("SERVER_FN_MOD_PATH")?.is_some(),
167 })
168 }
169}
170
171fn default_site_root() -> Arc<str> {
172 ".".into()
173}
174
175fn default_site_pkg_dir() -> Arc<str> {
176 "pkg".into()
177}
178
179fn default_env() -> Env {
180 Env::DEV
181}
182
183fn default_site_addr() -> SocketAddr {
184 SocketAddr::from(([127, 0, 0, 1], 3000))
185}
186
187fn default_reload_port() -> u32 {
188 3001
189}
190
191fn default_not_found_path() -> Arc<str> {
192 "/404".into()
193}
194
195fn default_hash_file_name() -> Arc<str> {
196 "hash.txt".into()
197}
198
199fn default_hash_files() -> bool {
200 false
201}
202
203fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
204 match std::env::var(key) {
205 Ok(val) => Ok(Some(val)),
206 Err(VarError::NotPresent) => Ok(None),
207 Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
208 }
209}
210fn env_w_default(
211 key: &str,
212 default: &str,
213) -> Result<String, LeptosConfigError> {
214 match std::env::var(key) {
215 Ok(val) => Ok(val),
216 Err(VarError::NotPresent) => Ok(default.to_string()),
217 Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
218 }
219}
220
221#[derive(
225 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
226)]
227pub enum Env {
228 PROD,
229 #[default]
230 DEV,
231}
232
233fn env_from_str(input: &str) -> Result<Env, LeptosConfigError> {
234 let sanitized = input.to_lowercase();
235 match sanitized.as_ref() {
236 "dev" | "development" => Ok(Env::DEV),
237 "prod" | "production" => Ok(Env::PROD),
238 _ => Err(LeptosConfigError::EnvVarError(format!(
239 "{input} is not a supported environment. Use either `dev` or \
240 `production`.",
241 ))),
242 }
243}
244
245impl FromStr for Env {
246 type Err = ();
247 fn from_str(input: &str) -> Result<Self, Self::Err> {
248 env_from_str(input).or_else(|_| Ok(Self::default()))
249 }
250}
251
252impl From<&str> for Env {
253 fn from(str: &str) -> Self {
254 env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
255 }
256}
257
258impl From<&Result<String, VarError>> for Env {
259 fn from(input: &Result<String, VarError>) -> Self {
260 match input {
261 Ok(str) => {
262 env_from_str(str).unwrap_or_else(|err| panic!("{}", err))
263 }
264 Err(_) => Self::default(),
265 }
266 }
267}
268
269impl TryFrom<String> for Env {
270 type Error = LeptosConfigError;
271
272 fn try_from(s: String) -> Result<Self, Self::Error> {
273 env_from_str(s.as_str())
274 }
275}
276
277#[derive(
280 Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default,
281)]
282pub enum ReloadWSProtocol {
283 #[default]
284 WS,
285 WSS,
286}
287
288fn ws_from_str(input: &str) -> Result<ReloadWSProtocol, LeptosConfigError> {
289 let sanitized = input.to_lowercase();
290 match sanitized.as_ref() {
291 "ws" | "WS" => Ok(ReloadWSProtocol::WS),
292 "wss" | "WSS" => Ok(ReloadWSProtocol::WSS),
293 _ => Err(LeptosConfigError::EnvVarError(format!(
294 "{input} is not a supported websocket protocol. Use only `ws` or \
295 `wss`.",
296 ))),
297 }
298}
299
300impl FromStr for ReloadWSProtocol {
301 type Err = ();
302 fn from_str(input: &str) -> Result<Self, Self::Err> {
303 ws_from_str(input).or_else(|_| Ok(Self::default()))
304 }
305}
306
307impl From<&str> for ReloadWSProtocol {
308 fn from(str: &str) -> Self {
309 ws_from_str(str).unwrap_or_else(|err| panic!("{}", err))
310 }
311}
312
313impl From<&Result<String, VarError>> for ReloadWSProtocol {
314 fn from(input: &Result<String, VarError>) -> Self {
315 match input {
316 Ok(str) => ws_from_str(str).unwrap_or_else(|err| panic!("{}", err)),
317 Err(_) => Self::default(),
318 }
319 }
320}
321
322impl TryFrom<String> for ReloadWSProtocol {
323 type Error = LeptosConfigError;
324
325 fn try_from(s: String) -> Result<Self, Self::Error> {
326 ws_from_str(s.as_str())
327 }
328}
329
330pub fn get_config_from_str(
333 text: &str,
334) -> Result<LeptosOptions, LeptosConfigError> {
335 let re: Regex = Regex::new(r"(?m)^\[package.metadata.leptos\]").unwrap();
336 let re_workspace: Regex =
337 Regex::new(r"(?m)^\[\[workspace.metadata.leptos\]\]").unwrap();
338
339 let metadata_name;
340 let start;
341 match re.find(text) {
342 Some(found) => {
343 metadata_name = "[package.metadata.leptos]";
344 start = found.start();
345 }
346 None => match re_workspace.find(text) {
347 Some(found) => {
348 metadata_name = "[[workspace.metadata.leptos]]";
349 start = found.start();
350 }
351 None => return Err(LeptosConfigError::ConfigSectionNotFound),
352 },
353 };
354
355 let newlines = text[..start].matches('\n').count();
357 let input = "\n".repeat(newlines) + &text[start..];
358 let toml = input.replace(metadata_name, "");
360 let settings = Config::builder()
361 .add_source(File::from_str(&toml, FileFormat::Toml))
363 .add_source(
367 config::Environment::with_prefix("LEPTOS")
368 .convert_case(Case::Kebab),
369 )
370 .build()?;
371
372 settings
373 .try_deserialize()
374 .map_err(|e| LeptosConfigError::ConfigError(e.to_string()))
375}
376
377pub fn get_configuration(
383 path: Option<&str>,
384) -> Result<ConfFile, LeptosConfigError> {
385 if let Some(path) = path {
386 get_config_from_file(path)
387 } else {
388 get_config_from_env()
389 }
390}
391
392pub fn get_config_from_file<P: AsRef<Path>>(
395 path: P,
396) -> Result<ConfFile, LeptosConfigError> {
397 let text = fs::read_to_string(path)
398 .map_err(|_| LeptosConfigError::ConfigNotFound)?;
399 let leptos_options = get_config_from_str(&text)?;
400 Ok(ConfFile { leptos_options })
401}
402
403pub fn get_config_from_env() -> Result<ConfFile, LeptosConfigError> {
405 Ok(ConfFile {
406 leptos_options: LeptosOptions::try_from_env()?,
407 })
408}
409
410#[path = "tests.rs"]
411#[cfg(test)]
412mod tests;