Optimize wasm bindgen asset with manganis (#3531)
This commit is contained in:
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;
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user