use std::{
hash::{Hash, Hasher},
io::Read,
path::{Path, PathBuf},
};
use crate::opt::{
css::hash_scss,
file::{resolve_asset_options, ResolvedAssetType},
js::hash_js,
};
use manganis::{AssetOptions, BundledAsset};
struct AssetHash {
hash: [u8; 8],
}
impl AssetHash {
const fn new(hash: u64) -> Self {
Self {
hash: hash.to_le_bytes(),
}
}
pub const fn bytes(&self) -> &[u8] {
&self.hash
}
pub fn hash_file_contents(
options: &AssetOptions,
file_path: impl AsRef<Path>,
) -> anyhow::Result<AssetHash> {
hash_file(options, file_path.as_ref())
}
}
fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash> {
let mut hash = std::collections::hash_map::DefaultHasher::new();
options.hash(&mut hash);
hash.write(crate::dx_build_info::PKG_VERSION.as_bytes());
hash_file_with_options(options, source, &mut hash, false)?;
let hash = hash.finish();
Ok(AssetHash::new(hash))
}
pub(crate) fn hash_file_with_options(
options: &AssetOptions,
source: &Path,
hasher: &mut impl Hasher,
in_folder: bool,
) -> anyhow::Result<()> {
let resolved_options = resolve_asset_options(source, options.variant());
match &resolved_options {
ResolvedAssetType::Scss(options) => {
hash_scss(options, source, hasher)?;
}
ResolvedAssetType::Js(options) => {
hash_js(options, source, hasher, !in_folder)?;
}
ResolvedAssetType::CssModule(_)
| ResolvedAssetType::Css(_)
| ResolvedAssetType::Image(_)
| ResolvedAssetType::Json
| ResolvedAssetType::File => {
hash_file_contents(source, hasher)?;
}
ResolvedAssetType::Folder(_) => {
for file in std::fs::read_dir(source)?.flatten() {
let path = file.path();
hash_file_with_options(
&AssetOptions::builder()
.with_hash_suffix(false)
.into_asset_options(),
&path,
hasher,
true,
)?;
}
}
}
Ok(())
}
pub(crate) fn hash_file_contents(source: &Path, hasher: &mut impl Hasher) -> anyhow::Result<()> {
let mut file = std::fs::File::open(source)?;
let mut buffer = [0; 8192];
loop {
let read = file.read(&mut buffer)?;
if read == 0 {
break;
}
hasher.write(&buffer[..read]);
}
Ok(())
}
pub(crate) fn add_hash_to_asset(asset: &mut BundledAsset) {
let source = asset.absolute_source_path();
match AssetHash::hash_file_contents(asset.options(), source) {
Ok(hash) => {
let options = *asset.options();
let source_path = PathBuf::from(source);
let Some(file_name) = source_path.file_name() else {
tracing::error!("Failed to get file name from path: {source}");
return;
};
let mut ext = asset.options().extension().map(Into::into).or_else(|| {
source_path
.extension()
.map(|ext| ext.to_string_lossy().to_string())
});
if let Some("scss" | "sass") = ext.as_deref() {
ext = Some("css".to_string());
}
let hash = hash.bytes();
let hash = hash
.iter()
.map(|byte| format!("{byte:x}"))
.collect::<String>();
let file_stem = source_path.file_stem().unwrap_or(file_name);
let mut bundled_path = if asset.options().hash_suffix() {
PathBuf::from(format!("{}-dxh{hash}", file_stem.to_string_lossy()))
} else {
PathBuf::from(file_stem)
};
if let Some(ext) = ext {
bundled_path.as_mut_os_string().push(format!(".{ext}"));
}
let bundled_path = bundled_path.to_string_lossy().to_string();
*asset = BundledAsset::new(source, &bundled_path, options);
}
Err(err) => {
tracing::error!("Failed to hash asset {source}: {err}");
}
}
}