use crate::{
config::Project,
ext::{eyre::CustomWrapErr, PathBufExt},
internal_prelude::*,
};
use base64ct::{Base64UrlUnpadded, Encoding};
use camino::Utf8PathBuf;
use eyre::{ContextCompat, Result};
use md5::{Digest, Md5};
use std::{collections::HashMap, fs};
pub fn add_hashes_to_site(proj: &Project) -> Result<()> {
let files_to_hashes = compute_front_file_hashes(proj).dot()?;
debug!("Hash computed: {files_to_hashes:?}");
let renamed_files = rename_files(&files_to_hashes).dot()?;
let pkg_dir = proj.site.root_relative_pkg_dir();
replace_in_file(
&renamed_files[&proj.lib.js_file.dest],
&renamed_files,
&pkg_dir,
false,
);
let wasm_split_hash = if proj.split {
let old_wasm_split = proj
.lib
.js_file
.dest
.clone()
.without_last()
.join("__wasm_split.______________________.js");
let new_wasm_split = &renamed_files[&old_wasm_split];
replace_in_file(new_wasm_split, &renamed_files, &pkg_dir, false);
let old_wasm_split_filename = old_wasm_split.file_name().unwrap();
let new_wasm_split_filename = new_wasm_split.file_name().unwrap();
for entry in fs::read_dir(&pkg_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
if filename.ends_with(".wasm") {
replace_in_binary_file(
&Utf8PathBuf::try_from(path).unwrap(),
old_wasm_split_filename,
new_wasm_split_filename,
);
} else if filename.starts_with("__wasm_split_manifest") {
replace_in_file(
&Utf8PathBuf::try_from(path).unwrap(),
&renamed_files,
&pkg_dir,
true,
);
} else if filename.starts_with("__wasm_split") {
replace_in_file(
&Utf8PathBuf::try_from(path).unwrap(),
&renamed_files,
&pkg_dir,
false,
);
}
}
}
}
Some(&files_to_hashes[&old_wasm_split])
} else {
None
};
let manifest_file = files_to_hashes
.iter()
.find_map(|(f, h)| (f.ends_with("__wasm_split_manifest.json")).then_some(h));
let manifest_file = manifest_file
.map(|f| format!("manifest: {f}\n"))
.unwrap_or_default();
let wasm_split_file = wasm_split_hash
.map(|f| format!("split: {f}\n"))
.unwrap_or_default();
fs::create_dir_all(
proj.hash_file
.abs
.parent()
.wrap_err_with(|| format!("no parent dir for {}", proj.hash_file.abs))?,
)
.wrap_err_with(|| format!("Failed to create parent dir for {}", proj.hash_file.abs))?;
fs::write(
&proj.hash_file.abs,
format!(
"{}: {}\n{}: {}\n{}: {}\n{}{}",
proj.lib
.js_file
.dest
.extension()
.ok_or(eyre!("no extension"))?,
files_to_hashes[&proj.lib.js_file.dest],
proj.lib
.wasm_file
.dest
.extension()
.ok_or(eyre!("no extension"))?,
files_to_hashes[&proj.lib.wasm_file.dest],
proj.style
.site_file
.dest
.extension()
.ok_or(eyre!("no extension"))?,
files_to_hashes[&proj.style.site_file.dest],
manifest_file,
wasm_split_file
),
)
.wrap_err_with(|| format!("Failed to write hash file to {}", proj.hash_file.abs))?;
debug!("Hash written to {}", proj.hash_file.abs);
Ok(())
}
fn compute_front_file_hashes(proj: &Project) -> Result<HashMap<Utf8PathBuf, String>> {
let mut files_to_hashes = HashMap::new();
let mut stack = vec![proj.site.root_relative_pkg_dir().into_std_path_buf()];
while let Some(path) = stack.pop() {
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(extension) = path.extension() {
if extension == "css" && path != proj.style.site_file.dest {
continue;
}
}
if let Some(path_str) = path.to_str() {
if path_str.contains("snippets") {
if let Some(file_name) = path.file_name() {
let file_name_str = file_name.to_string_lossy();
if file_name_str.contains("inline") {
if let Some(extension) = path.extension() {
if extension == "js" {
continue;
}
}
}
}
}
}
let hash = Base64UrlUnpadded::encode_string(
&Md5::new().chain_update(fs::read(&path)?).finalize(),
);
if path
.file_stem()
.and_then(|name| name.to_str())
.is_some_and(|name| name == hash)
{
continue;
}
files_to_hashes.insert(
Utf8PathBuf::from_path_buf(path).expect("invalid path"),
hash,
);
} else if path.is_dir() {
stack.push(path);
}
}
}
}
Ok(files_to_hashes)
}
fn rename_files(
files_to_hashes: &HashMap<Utf8PathBuf, String>,
) -> Result<HashMap<Utf8PathBuf, Utf8PathBuf>> {
const HASH_PLACEHOLDER: &str = "______________________";
let mut old_to_new_paths = HashMap::new();
for (path, hash) in files_to_hashes {
let mut new_path = path.clone();
let file_name = new_path.file_name().unwrap_or_default();
let new_file_name = if file_name.contains(HASH_PLACEHOLDER) {
if hash.len() != HASH_PLACEHOLDER.len() {
return Err(anyhow!(
"File hash length did not match placeholder hash length."
));
}
file_name.replace(HASH_PLACEHOLDER, hash)
} else {
format!(
"{}.{}.{}",
path.file_stem().ok_or(eyre!("no file stem"))?,
hash,
path.extension().ok_or(eyre!("no extension"))?,
)
};
new_path.set_file_name(new_file_name);
fs::rename(path, &new_path)
.wrap_err_with(|| format!("Failed to rename {path} to {new_path}"))?;
old_to_new_paths.insert(path.clone(), new_path);
}
Ok(old_to_new_paths)
}
fn replace_in_file(
path: &Utf8PathBuf,
old_to_new_paths: &HashMap<Utf8PathBuf, Utf8PathBuf>,
root_dir: &Utf8PathBuf,
omit_extension: bool,
) {
let mut contents = fs::read_to_string(path)
.unwrap_or_else(|e| panic!("error {e}: could not read file {path}"));
for (old_path, new_path) in old_to_new_paths {
let old_path = old_path
.strip_prefix(root_dir)
.expect("could not strip root path");
let new_path = new_path
.strip_prefix(root_dir)
.expect("could not strip root path");
if omit_extension {
let old_path = old_path.as_str().trim_end_matches(".wasm");
let new_path = new_path.as_str().trim_end_matches(".wasm");
contents = contents.replace(old_path, new_path);
} else {
contents = contents.replace(old_path.as_str(), new_path.as_str());
}
}
fs::write(path, contents).expect("could not write file");
}
fn replace_in_binary_file(path: &Utf8PathBuf, old_wasm_split: &str, new_wasm_split: &str) {
let mut contents =
fs::read(path).unwrap_or_else(|e| panic!("error {e}: could not read file {path}"));
let old_path = old_wasm_split.as_bytes();
let new_path = new_wasm_split.as_bytes();
for i in 0..=contents.len() - old_path.len() {
if contents[i..].starts_with(old_path) {
contents[i..(i + old_path.len())].clone_from_slice(new_path);
}
}
fs::write(path, contents).expect("could not write file");
}