diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs
index 8039600220..159078746b 100644
--- a/integrations/utils/src/lib.rs
+++ b/integrations/utils/src/lib.rs
@@ -2,7 +2,7 @@ use futures::{Stream, StreamExt};
use leptos::{nonce::use_nonce, use_context, RuntimeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
-use std::borrow::Cow;
+use std::{borrow::Cow, collections::HashMap, env, fs};
extern crate tracing;
@@ -103,6 +103,14 @@ pub fn html_parts_separated(
} else {
"() => mod.hydrate()"
};
+
+ let (js_hash, wasm_hash, css_hash) = get_hashes(&options);
+
+ let head = head.replace(
+ &format!("{output_name}.css"),
+ &format!("{output_name}{css_hash}.css"),
+ );
+
let head = format!(
r#"
@@ -110,8 +118,8 @@ pub fn html_parts_separated(
{head}
-
-
+
+
@@ -134,6 +142,46 @@ pub fn html_parts_separated(
(head, tail)
}
+#[tracing::instrument(level = "trace", fields(error), skip_all)]
+fn get_hashes(options: &LeptosOptions) -> (String, String, String) {
+ let mut ext_to_hash = HashMap::from([
+ ("js".to_string(), "".to_string()),
+ ("wasm".to_string(), "".to_string()),
+ ("css".to_string(), "".to_string()),
+ ]);
+
+ if options.frontend_files_content_hashes {
+ let hash_path = env::current_exe()
+ .map(|path| {
+ path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
+ })
+ .unwrap_or_default()
+ .join(&options.hash_file);
+
+ if hash_path.exists() {
+ let hashes = fs::read_to_string(&hash_path)
+ .expect("failed to read hash file");
+ for line in hashes.lines() {
+ let line = line.trim();
+ if !line.is_empty() {
+ if let Some((k, v)) = line.split_once(':') {
+ ext_to_hash.insert(
+ k.trim().to_string(),
+ format!(".{}", v.trim()),
+ );
+ }
+ }
+ }
+ }
+ }
+
+ (
+ ext_to_hash["js"].clone(),
+ ext_to_hash["wasm"].clone(),
+ ext_to_hash["css"].clone(),
+ )
+}
+
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn build_async_response(
stream: impl Stream- + 'static,
diff --git a/leptos_config/src/errors.rs b/leptos_config/src/errors.rs
index 4c13d2d8c4..17517f8cb3 100644
--- a/leptos_config/src/errors.rs
+++ b/leptos_config/src/errors.rs
@@ -1,4 +1,4 @@
-use std::{net::AddrParseError, num::ParseIntError};
+use std::{net::AddrParseError, num::ParseIntError, str::ParseBoolError};
use thiserror::Error;
#[derive(Debug, Error, Clone)]
@@ -31,3 +31,9 @@ impl From for LeptosConfigError {
Self::ConfigError(e.to_string())
}
}
+
+impl From for LeptosConfigError {
+ fn from(e: ParseBoolError) -> Self {
+ Self::ConfigError(e.to_string())
+ }
+}
diff --git a/leptos_config/src/lib.rs b/leptos_config/src/lib.rs
index ffde76e1a1..96f89af3e5 100644
--- a/leptos_config/src/lib.rs
+++ b/leptos_config/src/lib.rs
@@ -70,6 +70,15 @@ pub struct LeptosOptions {
#[builder(default = default_not_found_path())]
#[serde(default = "default_not_found_path")]
pub not_found_path: String,
+ /// The file name of the hash text file generated by cargo-leptos. Defaults to `hash.txt`.
+ #[builder(default = default_hash_file_name())]
+ #[serde(default = "default_hash_file_name")]
+ pub hash_file: String,
+ /// If true, hashes will be generated for all files in the site_root and added to their file names.
+ /// Defaults to `true`.
+ #[builder(default = default_frontend_files_content_hashes())]
+ #[serde(default = "default_frontend_files_content_hashes")]
+ pub frontend_files_content_hashes: bool,
}
impl LeptosOptions {
@@ -108,6 +117,15 @@ impl LeptosOptions {
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
)?,
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
+ hash_file: env_w_default("LEPTOS_HASH_FILE_NAME", "hash.txt")?,
+ frontend_files_content_hashes: env_w_default(
+ "LEPTOS_FRONTEND_FILES_CONTENT_HASHES",
+ "ON",
+ )?
+ .to_uppercase()
+ .replace("ON", "true")
+ .replace("OFF", "false")
+ .parse()?,
})
}
}
@@ -146,6 +164,14 @@ fn default_not_found_path() -> String {
"/404".to_string()
}
+fn default_hash_file_name() -> String {
+ "hash.txt".to_string()
+}
+
+fn default_frontend_files_content_hashes() -> bool {
+ true
+}
+
fn env_wo_default(key: &str) -> Result