Optimize wasm bindgen asset with manganis (#3531)

This commit is contained in:
Evan Almloff authored and GitHub committed 2025-01-21 15:14:45 -08:00
1 parent d8850c7666
commit 13dcd80fbf
29 files changed
+933 -1089

No files matched your search

@@ -59,6 +59,21 @@ impl BundledAsset {
Self::new(absolute_source_path, bundled_path, options)
}
#[doc(hidden)]
/// This should only be called from the macro
/// Create a new asset from const paths
pub const fn new_from_const(
absolute_source_path: ConstStr,
bundled_path: ConstStr,
options: AssetOptions,
) -> Self {
Self {
absolute_source_path,
bundled_path,
options,
}
}
/// Get the bundled name of the asset. This identifier cannot be used to read the asset directly
pub fn bundled_path(&self) -> &str {
self.bundled_path.as_str()
@@ -0,0 +1,94 @@
//! Utilities for creating hashed paths to assets in Manganis. This module defines [`AssetHash`] which is used to create a hashed path to an asset in both the CLI and the macro.
use std::{
error::Error,
hash::{Hash, Hasher},
io::Read,
path::{Path, PathBuf},
};
/// An error that can occur when hashing an asset
#[derive(Debug)]
#[non_exhaustive]
pub enum AssetHashError {
/// An io error occurred
IoError { err: std::io::Error, path: PathBuf },
}
impl std::fmt::Display for AssetHashError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetHashError::IoError { path, err } => {
write!(f, "Failed to read file: {}; {}", path.display(), err)
}
}
}
}
impl Error for AssetHashError {}
/// The opaque hash type manganis uses to identify assets. Each time an asset or asset options change, this hash will
/// change. This hash is included in the URL of the bundled asset for cache busting.
pub struct AssetHash {
/// We use a wrapper type here to hide the exact size of the hash so we can switch to a sha hash in a minor version bump
hash: [u8; 8],
}
impl AssetHash {
/// Create a new asset hash
const fn new(hash: u64) -> Self {
Self {
hash: hash.to_le_bytes(),
}
}
/// Get the hash bytes
pub const fn bytes(&self) -> &[u8] {
&self.hash
}
/// Create a new asset hash for a file. The input file to this function should be fully resolved
pub fn hash_file_contents(file_path: &Path) -> Result<AssetHash, AssetHashError> {
// Create a hasher
let mut hash = std::collections::hash_map::DefaultHasher::new();
// If this is a folder, hash the folder contents
if file_path.is_dir() {
let files = std::fs::read_dir(file_path).map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
for file in files.flatten() {
let path = file.path();
Self::hash_file_contents(&path)?.bytes().hash(&mut hash);
}
let hash = hash.finish();
return Ok(AssetHash::new(hash));
}
// Otherwise, open the file to get its contents
let mut file = std::fs::File::open(file_path).map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
// We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
// The hash includes the file contents, the options, and the version of manganis. From the macro, we just
// know the file contents, so we only include that hash
let mut buffer = [0; 8192];
loop {
let read = file
.read(&mut buffer)
.map_err(|err| AssetHashError::IoError {
err,
path: file_path.to_path_buf(),
})?;
if read == 0 {
break;
}
hash.write(&buffer[..read]);
}
Ok(AssetHash::new(hash.finish()))
}
}
@@ -17,3 +17,5 @@ mod asset;
pub use asset::*;
pub mod linker;
pub mod hash;
+32 -85
View File
@@ -1,15 +1,35 @@
use manganis_core::hash::AssetHash;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use std::{
hash::Hasher,
io::Read,
path::{Path, PathBuf},
};
use std::path::PathBuf;
use syn::{
parse::{Parse, ParseStream},
LitStr, Token,
};
#[derive(Debug)]
pub(crate) enum AssetParseError {
AssetDoesntExist { path: PathBuf },
InvalidPath { path: PathBuf },
}
impl std::fmt::Display for AssetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetParseError::AssetDoesntExist { path } => {
write!(f, "Asset at {} doesn't exist", path.display())
}
AssetParseError::InvalidPath { path } => {
write!(
f,
"Asset path {} is invalid. Make sure the asset exists within this crate.",
path.display()
)
}
}
}
}
fn resolve_path(raw: &str) -> Result<PathBuf, AssetParseError> {
// Get the location of the root of the crate which is where all assets are relative to
//
@@ -53,75 +73,6 @@ fn resolve_path(raw: &str) -> Result<PathBuf, AssetParseError> {
Ok(path)
}
fn hash_file_contents(file_path: &Path) -> Result<u64, AssetParseError> {
// Create a hasher
let mut hash = std::collections::hash_map::DefaultHasher::new();
// If this is a folder, hash the folder contents
if file_path.is_dir() {
let files = std::fs::read_dir(file_path).map_err(|err| AssetParseError::IoError {
err,
path: file_path.to_path_buf(),
})?;
for file in files.flatten() {
let path = file.path();
hash_file_contents(&path)?;
}
return Ok(hash.finish());
}
// Otherwise, open the file to get its contents
let mut file = std::fs::File::open(file_path).map_err(|err| AssetParseError::IoError {
err,
path: file_path.to_path_buf(),
})?;
// We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
// The hash includes the file contents, the options, and the version of manganis. From the macro, we just
// know the file contents, so we only include that hash
let mut buffer = [0; 8192];
loop {
let read = file
.read(&mut buffer)
.map_err(AssetParseError::FailedToReadAsset)?;
if read == 0 {
break;
}
hash.write(&buffer[..read]);
}
Ok(hash.finish())
}
#[derive(Debug)]
pub(crate) enum AssetParseError {
AssetDoesntExist { path: PathBuf },
IoError { err: std::io::Error, path: PathBuf },
InvalidPath { path: PathBuf },
FailedToReadAsset(std::io::Error),
}
impl std::fmt::Display for AssetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetParseError::AssetDoesntExist { path } => {
write!(f, "Asset at {} doesn't exist", path.display())
}
AssetParseError::IoError { path, err } => {
write!(f, "Failed to read file: {}; {}", path.display(), err)
}
AssetParseError::InvalidPath { path } => {
write!(
f,
"Asset path {} is invalid. Make sure the asset exists within this crate.",
path.display()
)
}
AssetParseError::FailedToReadAsset(err) => write!(f, "Failed to read asset: {}", err),
}
}
}
pub struct AssetParser {
/// The span of the source string
path_span: proc_macro2::Span,
@@ -189,7 +140,7 @@ impl ToTokens for AssetParser {
let mut asset_str = proc_macro2::Literal::string(&asset_str);
asset_str.set_span(self.path_span);
let hash = match hash_file_contents(asset) {
let hash = match AssetHash::hash_file_contents(asset) {
Ok(hash) => hash,
Err(err) => {
let err = err.to_string();
@@ -198,15 +149,17 @@ impl ToTokens for AssetParser {
}
};
let hash = hash.bytes();
// Generate the link section for the asset
// The link section includes the source path and the output path of the asset
let link_section = crate::generate_link_section(quote!(__ASSET));
// generate the asset::new method to deprecate the `./assets/blah.css` syntax
let constructor = if asset.is_relative() {
quote::quote! { new_relative }
quote::quote! { create_bundled_asset_relative }
} else {
quote::quote! { new }
quote::quote! { create_bundled_asset }
};
let options = if self.options.is_empty() {
@@ -218,22 +171,16 @@ impl ToTokens for AssetParser {
tokens.extend(quote! {
{
// We keep a hash of the contents of the asset for cache busting
const __ASSET_HASH: u64 = #hash;
const __ASSET_HASH: &[u8] = &[#(#hash),*];
// The source is used by the CLI to copy the asset
const __ASSET_SOURCE_PATH: &'static str = #asset_str;
// The options give the CLI info about how to process the asset
// Note: into_asset_options is not a trait, so we cannot accept the options directly
// in the constructor. Stable rust doesn't have support for constant functions in traits
const __ASSET_OPTIONS: manganis::AssetOptions = #options.into_asset_options();
// We calculate the bundled path from the hash and any transformations done by the options
// This is the final path that the asset will be written to
const __ASSET_BUNDLED_PATH: manganis::macro_helpers::const_serialize::ConstStr = manganis::macro_helpers::generate_unique_path(__ASSET_SOURCE_PATH, __ASSET_HASH, &__ASSET_OPTIONS);
// Get the reference to the string that was generated. We cannot return &'static str from
// generate_unique_path because it would return a reference to data generated in the function
const __ASSET_BUNDLED_PATH_STR: &'static str = __ASSET_BUNDLED_PATH.as_str();
// Create the asset that the crate will use. This is used both in the return value and
// added to the linker for the bundler to copy later
const __ASSET: manganis::BundledAsset = manganis::BundledAsset::#constructor(__ASSET_SOURCE_PATH, __ASSET_BUNDLED_PATH_STR, __ASSET_OPTIONS);
const __ASSET: manganis::BundledAsset = manganis::macro_helpers::#constructor(__ASSET_SOURCE_PATH, __ASSET_HASH, __ASSET_OPTIONS);
#link_section
@@ -4,11 +4,45 @@ use manganis_core::{AssetOptions, BundledAsset};
use crate::hash::ConstHasher;
/// Create a bundled asset from the input path, the content hash, and the asset options
pub const fn create_bundled_asset(
input_path: &str,
content_hash: &[u8],
asset_config: AssetOptions,
) -> BundledAsset {
let hashed_path = generate_unique_path_with_byte_hash(input_path, content_hash, &asset_config);
BundledAsset::new_from_const(ConstStr::new(input_path), hashed_path, asset_config)
}
/// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning
///
/// This method is deprecated and will be removed in a future release.
#[deprecated(
note = "Relative asset!() paths are not supported. Use a path like `/assets/myfile.png` instead of `./assets/myfile.png`"
)]
pub const fn create_bundled_asset_relative(
input_path: &str,
content_hash: &[u8],
asset_config: AssetOptions,
) -> BundledAsset {
create_bundled_asset(input_path, content_hash, asset_config)
}
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
pub const fn generate_unique_path(
input_path: &str,
content_hash: u64,
asset_config: &AssetOptions,
) -> ConstStr {
let byte_hash = content_hash.to_le_bytes();
generate_unique_path_with_byte_hash(input_path, &byte_hash, asset_config)
}
/// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
const fn generate_unique_path_with_byte_hash(
input_path: &str,
content_hash: &[u8],
asset_config: &AssetOptions,
) -> ConstStr {
// Format the unique path with the format `{input_path}-{hash}.{extension}`
// Start with the input path
@@ -43,7 +77,7 @@ pub const fn generate_unique_path(
// Hash the contents along with the asset config to create a unique hash for the asset
// When this hash changes, the client needs to re-fetch the asset
let mut hasher = ConstHasher::new();
hasher = hasher.write(&content_hash.to_le_bytes());
hasher = hasher.write(content_hash);
hasher = hasher.hash_by_bytes(asset_config);
let hash = hasher.finish();