Hashless assets (#4312)

* add support for external assets

* fix playwright tests

* add more documentation to with_hash_suffix

* restore external asset hashing tests

* Update gtk workflow dependencies

* fix doc test

* multiple versions of libgtk

* clear dx in playwright tests

* fix fullstack caching test

* try without apt cache

* cache everything except libxdo-dev

* revert ci changes

* fix wasm asset options

* fix merge conflict

* recommend the used attribute instead

* Move options to AssetOptionsBuilder

* Don't deprecate non-canonical new asset options functions

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2025-06-30 19:21:49 -07:00
1 parent 0bd376b873
commit e74c838441
22 files changed
+569 -316

No files matched your search

@@ -46,12 +46,12 @@ parse_quote! {
} }
p { p {
img { img {
src: asset!("/example-book/assets1/logo.png", ImageAssetOptions::new().with_avif()), src: asset!("/example-book/assets1/logo.png", AssetOptions::image().with_avif()),
alt: "some_local1", alt: "some_local1",
title: "", title: "",
} }
img { img {
src: asset!("/example-book/assets2/logo.png", ImageAssetOptions::new().with_avif()), src: asset!("/example-book/assets2/logo.png", AssetOptions::image().with_avif()),
alt: "some_local2", alt: "some_local2",
title: "", title: "",
} }
+29 -19
View File
@@ -1,6 +1,6 @@
use anyhow::Context; use anyhow::Context;
use manganis::{CssModuleAssetOptions, FolderAssetOptions}; use manganis::{AssetOptions, CssModuleAssetOptions, FolderAssetOptions};
use manganis_core::{AssetOptions, CssAssetOptions, ImageAssetOptions, JsAssetOptions}; use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
use std::path::Path; use std::path::Path;
use crate::css::{process_css_module, process_scss}; use crate::css::{process_css_module, process_scss};
@@ -26,10 +26,10 @@ pub(crate) fn process_file_to_with_options(
output_path: &Path, output_path: &Path,
in_folder: bool, in_folder: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// If the file already exists, then we must have a file with the same hash // If the file already exists and this is a hashed asset, then we must have a file
// already. The hash has the file contents and options, so if we find a file // with the same hash already. The hash has the file contents and options, so if we
// with the same hash, we probably already created this file in the past // find a file with the same hash, we probably already created this file in the past
if output_path.exists() { if output_path.exists() && options.hash_suffix() {
return Ok(()); return Ok(());
} }
if let Some(parent) = output_path.parent() { if let Some(parent) = output_path.parent() {
@@ -48,7 +48,7 @@ pub(crate) fn process_file_to_with_options(
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy()
)); ));
let resolved_options = resolve_asset_options(source, options); let resolved_options = resolve_asset_options(source, options.variant());
match &resolved_options { match &resolved_options {
ResolvedAssetType::Css(options) => { ResolvedAssetType::Css(options) => {
@@ -86,6 +86,16 @@ pub(crate) fn process_file_to_with_options(
} }
} }
// Remove the existing output file if it exists
if output_path.exists() {
if output_path.is_file() {
std::fs::remove_file(output_path).context("Failed to remove previous output file")?;
} else if output_path.is_dir() {
std::fs::remove_dir_all(output_path)
.context("Failed to remove previous output file")?;
}
}
// If everything was successful, rename the temp file to the final output path // If everything was successful, rename the temp file to the final output path
std::fs::rename(temp_path, output_path).context("Failed to rename output file")?; std::fs::rename(temp_path, output_path).context("Failed to rename output file")?;
@@ -111,14 +121,14 @@ pub(crate) enum ResolvedAssetType {
File, File,
} }
pub(crate) fn resolve_asset_options(source: &Path, options: &AssetOptions) -> ResolvedAssetType { pub(crate) fn resolve_asset_options(source: &Path, options: &AssetVariant) -> ResolvedAssetType {
match options { match options {
AssetOptions::Image(image) => ResolvedAssetType::Image(*image), AssetVariant::Image(image) => ResolvedAssetType::Image(*image),
AssetOptions::Css(css) => ResolvedAssetType::Css(*css), AssetVariant::Css(css) => ResolvedAssetType::Css(*css),
AssetOptions::CssModule(css) => ResolvedAssetType::CssModule(*css), AssetVariant::CssModule(css) => ResolvedAssetType::CssModule(*css),
AssetOptions::Js(js) => ResolvedAssetType::Js(*js), AssetVariant::Js(js) => ResolvedAssetType::Js(*js),
AssetOptions::Folder(folder) => ResolvedAssetType::Folder(*folder), AssetVariant::Folder(folder) => ResolvedAssetType::Folder(*folder),
AssetOptions::Unknown => resolve_unknown_asset_options(source), AssetVariant::Unknown => resolve_unknown_asset_options(source),
_ => { _ => {
tracing::warn!("Unknown asset options... you may need to update the Dioxus CLI. Defaulting to a generic file: {:?}", options); tracing::warn!("Unknown asset options... you may need to update the Dioxus CLI. Defaulting to a generic file: {:?}", options);
resolve_unknown_asset_options(source) resolve_unknown_asset_options(source)
@@ -128,14 +138,14 @@ pub(crate) fn resolve_asset_options(source: &Path, options: &AssetOptions) -> Re
fn resolve_unknown_asset_options(source: &Path) -> ResolvedAssetType { fn resolve_unknown_asset_options(source: &Path) -> ResolvedAssetType {
match source.extension().map(|e| e.to_string_lossy()).as_deref() { match source.extension().map(|e| e.to_string_lossy()).as_deref() {
Some("scss" | "sass") => ResolvedAssetType::Scss(CssAssetOptions::new()), Some("scss" | "sass") => ResolvedAssetType::Scss(CssAssetOptions::default()),
Some("css") => ResolvedAssetType::Css(CssAssetOptions::new()), Some("css") => ResolvedAssetType::Css(CssAssetOptions::default()),
Some("js") => ResolvedAssetType::Js(JsAssetOptions::new()), Some("js") => ResolvedAssetType::Js(JsAssetOptions::default()),
Some("json") => ResolvedAssetType::Json, Some("json") => ResolvedAssetType::Json,
Some("jpg" | "jpeg" | "png" | "webp" | "avif") => { Some("jpg" | "jpeg" | "png" | "webp" | "avif") => {
ResolvedAssetType::Image(ImageAssetOptions::new()) ResolvedAssetType::Image(ImageAssetOptions::default())
} }
_ if source.is_dir() => ResolvedAssetType::Folder(FolderAssetOptions::new()), _ if source.is_dir() => ResolvedAssetType::Folder(FolderAssetOptions::default()),
_ => ResolvedAssetType::File, _ => ResolvedAssetType::File,
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
/// Optimize a file without changing any of its contents significantly (e.g. by changing the extension) /// Optimize a file without changing any of its contents significantly (e.g. by changing the extension)
fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> { fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> {
process_file_to_with_options( process_file_to_with_options(
&manganis_core::AssetOptions::Unknown, &manganis_core::AssetOptions::builder().into_asset_options(),
input_path, input_path,
output_path, output_path,
true, true,
+6 -3
View File
@@ -62,7 +62,7 @@ pub(crate) fn hash_file_with_options(
hasher: &mut impl Hasher, hasher: &mut impl Hasher,
in_folder: bool, in_folder: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let resolved_options = resolve_asset_options(source, options); let resolved_options = resolve_asset_options(source, options.variant());
match &resolved_options { match &resolved_options {
// Scss and JS can import files during the bundling process. We need to hash // Scss and JS can import files during the bundling process. We need to hash
@@ -145,8 +145,11 @@ pub fn add_hash_to_asset(asset: &mut BundledAsset) {
.map(|byte| format!("{byte:x}")) .map(|byte| format!("{byte:x}"))
.collect::<String>(); .collect::<String>();
let file_stem = source_path.file_stem().unwrap_or(file_name); let file_stem = source_path.file_stem().unwrap_or(file_name);
let mut bundled_path = let mut bundled_path = if asset.options().hash_suffix() {
PathBuf::from(format!("{}-dxh{hash}", file_stem.to_string_lossy())); PathBuf::from(format!("{}-dxh{hash}", file_stem.to_string_lossy()))
} else {
PathBuf::from(file_stem)
};
if let Some(ext) = ext { if let Some(ext) = ext {
bundled_path.set_extension(ext); bundled_path.set_extension(ext);
+17 -9
View File
@@ -327,7 +327,8 @@ use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
use dioxus_cli_opt::{process_file_to, AssetManifest}; use dioxus_cli_opt::{process_file_to, AssetManifest};
use itertools::Itertools; use itertools::Itertools;
use krates::{cm::TargetKind, NodeId}; use krates::{cm::TargetKind, NodeId};
use manganis::{AssetOptions, JsAssetOptions}; use manganis::AssetOptions;
use manganis_core::AssetVariant;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
@@ -3310,7 +3311,7 @@ impl BuildRequest {
writeln!( writeln!(
glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/assets/{url}\", [], fusedImports);", glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/assets/{url}\", [], fusedImports);",
url = assets url = assets
.register_asset(&path, AssetOptions::Unknown)?.bundled_path(), .register_asset(&path, AssetOptions::builder().into_asset_options())?.bundled_path(),
)?; )?;
} }
@@ -3338,7 +3339,8 @@ impl BuildRequest {
// Again, register this wasm with the asset system // Again, register this wasm with the asset system
url = assets url = assets
.register_asset(&path, AssetOptions::Unknown)?.bundled_path(), .register_asset(&path, AssetOptions::builder().into_asset_options())?
.bundled_path(),
// This time, make sure to write the dependencies of this chunk // This time, make sure to write the dependencies of this chunk
// The names here are again, hardcoded in wasm-split - fix this eventually. // The names here are again, hardcoded in wasm-split - fix this eventually.
@@ -3386,7 +3388,10 @@ impl BuildRequest {
if self.should_bundle_to_asset() { if self.should_bundle_to_asset() {
// Make sure to register the main wasm file with the asset system // Make sure to register the main wasm file with the asset system
assets.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?; assets.register_asset(
&post_bindgen_wasm,
AssetOptions::builder().into_asset_options(),
)?;
} }
// Now that the wasm is registered as an asset, we can write the js glue shim // Now that the wasm is registered as an asset, we can write the js glue shim
@@ -3396,7 +3401,10 @@ impl BuildRequest {
// Register the main.js with the asset system so it bundles in the snippets and optimizes // Register the main.js with the asset system so it bundles in the snippets and optimizes
assets.register_asset( assets.register_asset(
&self.wasm_bindgen_js_output_file(), &self.wasm_bindgen_js_output_file(),
AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)), AssetOptions::js()
.with_minify(true)
.with_preload(true)
.into_asset_options(),
)?; )?;
} }
@@ -4034,22 +4042,22 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
// Inject any resources from manganis into the head // Inject any resources from manganis into the head
for asset in assets.assets() { for asset in assets.assets() {
let asset_path = asset.bundled_path(); let asset_path = asset.bundled_path();
match asset.options() { match asset.options().variant() {
AssetOptions::Css(css_options) => { AssetVariant::Css(css_options) => {
if css_options.preloaded() { if css_options.preloaded() {
head_resources.push_str(&format!( head_resources.push_str(&format!(
"<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>" "<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
)) ))
} }
} }
AssetOptions::Image(image_options) => { AssetVariant::Image(image_options) => {
if image_options.preloaded() { if image_options.preloaded() {
head_resources.push_str(&format!( head_resources.push_str(&format!(
"<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>" "<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
)) ))
} }
} }
AssetOptions::Js(js_options) => { AssetVariant::Js(js_options) => {
if js_options.preloaded() { if js_options.preloaded() {
head_resources.push_str(&format!( head_resources.push_str(&format!(
"<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>" "<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
@@ -66,37 +66,6 @@ impl BundledAsset {
} }
} }
#[doc(hidden)]
/// This should only be called from the macro
/// Create a new asset but with a relative path
///
/// 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 new_relative(
absolute_source_path: &'static str,
bundled_path: &'static str,
options: AssetOptions,
) -> Self {
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 /// Get the bundled name of the asset. This identifier cannot be used to read the asset directly
pub fn bundled_path(&self) -> &str { pub fn bundled_path(&self) -> &str {
self.bundled_path.as_str() self.bundled_path.as_str()
+44 -22
View File
@@ -1,4 +1,4 @@
use crate::AssetOptions; use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
use const_serialize::SerializeConst; use const_serialize::SerializeConst;
/// Options for a css asset /// Options for a css asset
@@ -21,35 +21,59 @@ pub struct CssAssetOptions {
impl Default for CssAssetOptions { impl Default for CssAssetOptions {
fn default() -> Self { fn default() -> Self {
Self::new() Self::default()
} }
} }
impl CssAssetOptions { impl CssAssetOptions {
/// Create a new css asset using the builder /// Create a new css asset using the builder
pub const fn new() -> Self { pub const fn new() -> AssetOptionsBuilder<CssAssetOptions> {
AssetOptions::css()
}
/// Create a default css asset options
pub const fn default() -> Self {
Self { Self {
preload: false, preload: false,
minify: true, minify: true,
} }
} }
/// Check if the asset is preloaded
pub const fn preloaded(&self) -> bool {
self.preload
}
/// Check if the asset is minified
pub const fn minified(&self) -> bool {
self.minify
}
}
impl AssetOptions {
/// Create a new css asset builder
///
/// ```rust
/// # use manganis::{asset, Asset, CssAssetOptions};
/// const _: Asset = asset!("/assets/style.css", AssetOptions::css());
/// ```
pub const fn css() -> AssetOptionsBuilder<CssAssetOptions> {
AssetOptionsBuilder::variant(CssAssetOptions::default())
}
}
impl AssetOptionsBuilder<CssAssetOptions> {
/// Sets whether the css should be minified (default: true) /// Sets whether the css should be minified (default: true)
/// ///
/// Minifying the css can make your site load faster by loading less data /// Minifying the css can make your site load faster by loading less data
/// ///
/// ```rust /// ```rust
/// # use manganis::{asset, Asset, CssAssetOptions}; /// # use manganis::{asset, Asset, CssAssetOptions};
/// const _: Asset = asset!("/assets/style.css", CssAssetOptions::new().with_minify(false)); /// const _: Asset = asset!("/assets/style.css", AssetOptions::css().with_minify(false));
/// ``` /// ```
#[allow(unused)] pub const fn with_minify(mut self, minify: bool) -> Self {
pub const fn with_minify(self, minify: bool) -> Self { self.variant.minify = minify;
Self { minify, ..self } self
}
/// Check if the asset is minified
pub const fn minified(&self) -> bool {
self.minify
} }
/// Make the asset preloaded /// Make the asset preloaded
@@ -58,20 +82,18 @@ impl CssAssetOptions {
/// ///
/// ```rust /// ```rust
/// # use manganis::{asset, Asset, CssAssetOptions}; /// # use manganis::{asset, Asset, CssAssetOptions};
/// const _: Asset = asset!("/assets/style.css", CssAssetOptions::new().with_preload(true)); /// const _: Asset = asset!("/assets/style.css", AssetOptions::css().with_preload(true));
/// ``` /// ```
#[allow(unused)] pub const fn with_preload(mut self, preload: bool) -> Self {
pub const fn with_preload(self, preload: bool) -> Self { self.variant.preload = preload;
Self { preload, ..self } self
}
/// Check if the asset is preloaded
pub const fn preloaded(&self) -> bool {
self.preload
} }
/// Convert the options into options for a generic asset /// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions { pub const fn into_asset_options(self) -> AssetOptions {
AssetOptions::Css(self) AssetOptions {
add_hash: true,
variant: AssetVariant::Css(self.variant),
}
} }
} }
@@ -1,4 +1,4 @@
use crate::AssetOptions; use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
use const_serialize::SerializeConst; use const_serialize::SerializeConst;
use std::collections::HashSet; use std::collections::HashSet;
@@ -24,48 +24,70 @@ pub struct CssModuleAssetOptions {
impl Default for CssModuleAssetOptions { impl Default for CssModuleAssetOptions {
fn default() -> Self { fn default() -> Self {
Self::new() Self::default()
} }
} }
impl CssModuleAssetOptions { impl CssModuleAssetOptions {
/// Create a new css asset using the builder /// Create a new css asset using the builder
pub const fn new() -> Self { pub const fn new() -> AssetOptionsBuilder<CssModuleAssetOptions> {
AssetOptions::css_module()
}
/// Create a default css module asset options
pub const fn default() -> Self {
Self { Self {
preload: false, preload: false,
minify: true, minify: true,
} }
} }
/// Sets whether the css should be minified (default: true)
///
/// Minifying the css can make your site load faster by loading less data
#[allow(unused)]
pub const fn with_minify(self, minify: bool) -> Self {
Self { minify, ..self }
}
/// Check if the asset is minified /// Check if the asset is minified
pub const fn minified(&self) -> bool { pub const fn minified(&self) -> bool {
self.minify self.minify
} }
/// Make the asset preloaded
///
/// Preloading css will make the image start to load as soon as possible. This is useful for css that is used soon after the page loads or css that may not be used immediately, but should start loading sooner
#[allow(unused)]
pub const fn with_preload(self, preload: bool) -> Self {
Self { preload, ..self }
}
/// Check if the asset is preloaded /// Check if the asset is preloaded
pub const fn preloaded(&self) -> bool { pub const fn preloaded(&self) -> bool {
self.preload self.preload
} }
}
impl AssetOptions {
/// Create a new css module asset builder
///
/// ```rust
/// # use manganis::{asset, Asset, CssModuleAssetOptions};
/// const _: Asset = asset!("/assets/style.css", AssetOptions::css_module());
/// ```
pub const fn css_module() -> AssetOptionsBuilder<CssModuleAssetOptions> {
AssetOptionsBuilder::variant(CssModuleAssetOptions::default())
}
}
impl AssetOptionsBuilder<CssModuleAssetOptions> {
/// Sets whether the css should be minified (default: true)
///
/// Minifying the css can make your site load faster by loading less data
pub const fn with_minify(mut self, minify: bool) -> Self {
self.variant.minify = minify;
self
}
/// Make the asset preloaded
///
/// Preloading css will make the image start to load as soon as possible. This is useful for css that is used soon after the page loads or css that may not be used immediately, but should start loading sooner
pub const fn with_preload(mut self, preload: bool) -> Self {
self.variant.preload = preload;
self
}
/// Convert the options into options for a generic asset /// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions { pub const fn into_asset_options(self) -> AssetOptions {
AssetOptions::CssModule(self) AssetOptions {
add_hash: self.add_hash,
variant: AssetVariant::CssModule(self.variant),
}
} }
} }
+28 -6
View File
@@ -1,6 +1,6 @@
use const_serialize::SerializeConst; use const_serialize::SerializeConst;
use crate::AssetOptions; use crate::{AssetOptions, AssetOptionsBuilder};
/// The builder for a folder asset. /// The builder for a folder asset.
#[derive( #[derive(
@@ -19,18 +19,40 @@ pub struct FolderAssetOptions {}
impl Default for FolderAssetOptions { impl Default for FolderAssetOptions {
fn default() -> Self { fn default() -> Self {
Self::new() Self::default()
} }
} }
impl FolderAssetOptions { impl FolderAssetOptions {
/// Create a new folder asset using the builder /// Create a new folder asset builder
pub const fn new() -> Self { pub const fn new() -> AssetOptionsBuilder<FolderAssetOptions> {
Self {} AssetOptions::folder()
} }
/// Create a default folder asset options
pub const fn default() -> Self {
Self {}
}
}
impl AssetOptions {
/// Create a new folder asset builder
///
/// ```rust
/// # use manganis::{asset, Asset, AssetOptions};
/// const _: Asset = asset!("/assets", AssetOptions::folder());
/// ```
pub const fn folder() -> AssetOptionsBuilder<FolderAssetOptions> {
AssetOptionsBuilder::variant(FolderAssetOptions::default())
}
}
impl AssetOptionsBuilder<FolderAssetOptions> {
/// Convert the options into options for a generic asset /// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions { pub const fn into_asset_options(self) -> AssetOptions {
AssetOptions::Folder(self) AssetOptions {
add_hash: false,
variant: crate::AssetVariant::Folder(self.variant),
}
} }
} }
+120 -112
View File
@@ -1,6 +1,6 @@
use const_serialize::SerializeConst; use const_serialize::SerializeConst;
use crate::AssetOptions; use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
/// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types) /// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types)
#[derive( #[derive(
@@ -77,13 +77,18 @@ pub struct ImageAssetOptions {
impl Default for ImageAssetOptions { impl Default for ImageAssetOptions {
fn default() -> Self { fn default() -> Self {
Self::new() Self::default()
} }
} }
impl ImageAssetOptions { impl ImageAssetOptions {
/// Create a new image asset options /// Create a new builder for image asset options
pub const fn new() -> Self { pub const fn new() -> AssetOptionsBuilder<ImageAssetOptions> {
AssetOptions::image()
}
/// Create a default image asset options
pub const fn default() -> Self {
Self { Self {
ty: ImageFormat::Unknown, ty: ImageFormat::Unknown,
low_quality_preview: false, low_quality_preview: false,
@@ -92,129 +97,21 @@ impl ImageAssetOptions {
} }
} }
/// Make the asset preloaded
///
/// Preloading an image will make the image start to load as soon as possible. This is useful for images that will be displayed soon after the page loads or images that may not be visible immediately, but should start loading sooner
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_preload(true));
/// ```
pub const fn with_preload(self, preload: bool) -> Self {
Self { preload, ..self }
}
/// Check if the asset is preloaded /// Check if the asset is preloaded
pub const fn preloaded(&self) -> bool { pub const fn preloaded(&self) -> bool {
self.preload self.preload
} }
/// Sets the format of the image
///
/// Choosing the right format can make your site load much faster. Webp and avif images tend to be a good default for most images
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_format(ImageFormat::Webp));
/// ```
pub const fn with_format(self, format: ImageFormat) -> Self {
Self { ty: format, ..self }
}
/// Sets the format of the image to [`ImageFormat::Avif`]
///
/// Avif images tend to be a good default for most images rendered in browser because
/// they compress images well
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_avif());
/// ```
pub const fn with_avif(self) -> Self {
self.with_format(ImageFormat::Avif)
}
/// Sets the format of the image to [`ImageFormat::Webp`]
///
/// Webp images tend to be a good default for most images rendered in browser because
/// they compress images well
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_webp());
/// ```
pub const fn with_webp(self) -> Self {
self.with_format(ImageFormat::Webp)
}
/// Sets the format of the image to [`ImageFormat::Jpg`]
///
/// Jpeg images compress much better than [`ImageFormat::Png`], but worse than [`ImageFormat::Webp`] or [`ImageFormat::Avif`]
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_jpg());
/// ```
pub const fn with_jpg(self) -> Self {
self.with_format(ImageFormat::Jpg)
}
/// Sets the format of the image to [`ImageFormat::Png`]
///
/// Png images don't compress very well, so they are not recommended for large images
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_png());
/// ```
pub const fn with_png(self) -> Self {
self.with_format(ImageFormat::Png)
}
/// Get the format of the image /// Get the format of the image
pub const fn format(&self) -> ImageFormat { pub const fn format(&self) -> ImageFormat {
self.ty self.ty
} }
/// Sets the size of the image
///
/// If you only use the image in one place, you can set the size of the image to the size it will be displayed at. This will make the image load faster
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageSize};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_size(ImageSize::Manual { width: 512, height: 512 }));
/// ```
pub const fn with_size(self, size: ImageSize) -> Self {
Self { size, ..self }
}
/// Get the size of the image /// Get the size of the image
pub const fn size(&self) -> ImageSize { pub const fn size(&self) -> ImageSize {
self.size self.size
} }
// LQIP is currently disabled until we have the CLI set up to inject the low quality image preview after the crate is built through the linker
// /// Make the image use a low quality preview
// ///
// /// A low quality preview is a small version of the image that will load faster. This is useful for large images on mobile devices that may take longer to load
// ///
// /// ```rust
// /// # use manganis::{asset, Asset, ImageAssetOptions};
// /// const _: Asset = manganis::asset!("/assets/image.png", ImageAssetOptions::new().with_low_quality_image_preview());
// /// ```
//
// pub const fn with_low_quality_image_preview(self, low_quality_preview: bool) -> Self {
// Self {
// low_quality_preview,
// ..self
// }
// }
/// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions {
AssetOptions::Image(self)
}
pub(crate) const fn extension(&self) -> Option<&'static str> { pub(crate) const fn extension(&self) -> Option<&'static str> {
match self.ty { match self.ty {
ImageFormat::Png => Some("png"), ImageFormat::Png => Some("png"),
@@ -225,3 +122,114 @@ impl ImageAssetOptions {
} }
} }
} }
impl AssetOptions {
/// Create a new image asset builder
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image());
/// ```
pub const fn image() -> AssetOptionsBuilder<ImageAssetOptions> {
AssetOptionsBuilder::variant(ImageAssetOptions::default())
}
}
impl AssetOptionsBuilder<ImageAssetOptions> {
/// Make the asset preloaded
///
/// Preloading an image will make the image start to load as soon as possible. This is useful for images that will be displayed soon after the page loads or images that may not be visible immediately, but should start loading sooner
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_preload(true));
/// ```
pub const fn with_preload(mut self, preload: bool) -> Self {
self.variant.preload = preload;
self
}
/// Sets the format of the image
///
/// Choosing the right format can make your site load much faster. Webp and avif images tend to be a good default for most images
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Webp));
/// ```
pub const fn with_format(mut self, format: ImageFormat) -> Self {
self.variant.ty = format;
self
}
/// Sets the format of the image to [`ImageFormat::Avif`]
///
/// Avif images tend to be a good default for most images rendered in browser because
/// they compress images well
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_avif());
/// ```
pub const fn with_avif(self) -> Self {
self.with_format(ImageFormat::Avif)
}
/// Sets the format of the image to [`ImageFormat::Webp`]
///
/// Webp images tend to be a good default for most images rendered in browser because
/// they compress images well
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_webp());
/// ```
pub const fn with_webp(self) -> Self {
self.with_format(ImageFormat::Webp)
}
/// Sets the format of the image to [`ImageFormat::Jpg`]
///
/// Jpeg images compress much better than [`ImageFormat::Png`], but worse than [`ImageFormat::Webp`] or [`ImageFormat::Avif`]
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_jpg());
/// ```
pub const fn with_jpg(self) -> Self {
self.with_format(ImageFormat::Jpg)
}
/// Sets the format of the image to [`ImageFormat::Png`]
///
/// Png images don't compress very well, so they are not recommended for large images
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_png());
/// ```
pub const fn with_png(self) -> Self {
self.with_format(ImageFormat::Png)
}
/// Sets the size of the image
///
/// If you only use the image in one place, you can set the size of the image to the size it will be displayed at. This will make the image load faster
///
/// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageSize};
/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_size(ImageSize::Manual { width: 512, height: 512 }));
/// ```
pub const fn with_size(mut self, size: ImageSize) -> Self {
self.variant.size = size;
self
}
/// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions {
AssetOptions {
add_hash: self.add_hash,
variant: AssetVariant::Image(self.variant),
}
}
}
+47 -23
View File
@@ -1,6 +1,6 @@
use const_serialize::SerializeConst; use const_serialize::SerializeConst;
use crate::AssetOptions; use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
/// Options for a javascript asset /// Options for a javascript asset
#[derive( #[derive(
@@ -22,35 +22,60 @@ pub struct JsAssetOptions {
impl Default for JsAssetOptions { impl Default for JsAssetOptions {
fn default() -> Self { fn default() -> Self {
Self::new() Self::default()
} }
} }
impl JsAssetOptions { impl JsAssetOptions {
/// Create a new js asset builder /// Create a new js asset options builder
pub const fn new() -> Self { pub const fn new() -> AssetOptionsBuilder<JsAssetOptions> {
AssetOptions::js()
}
/// Create a default js asset options
pub const fn default() -> Self {
Self { Self {
minify: true,
preload: false, preload: false,
minify: true,
} }
} }
/// Check if the asset is preloaded
pub const fn preloaded(&self) -> bool {
self.preload
}
/// Check if the asset is minified
pub const fn minified(&self) -> bool {
self.minify
}
}
impl AssetOptions {
/// Create a new js asset builder
///
/// ```rust
/// # use manganis::{asset, Asset, JsAssetOptions};
/// const _: Asset = asset!("/assets/script.js", AssetOptions::js());
/// ```
pub const fn js() -> AssetOptionsBuilder<JsAssetOptions> {
AssetOptionsBuilder::variant(JsAssetOptions::default())
}
}
impl AssetOptionsBuilder<JsAssetOptions> {
/// Sets whether the js should be minified (default: true) /// Sets whether the js should be minified (default: true)
/// ///
/// Minifying the js can make your site load faster by loading less data /// Minifying the js can make your site load faster by loading less data
/// ///
/// ```rust /// ```rust
/// # use manganis::{asset, Asset, JsAssetOptions}; /// # use manganis::{asset, Asset, JsAssetOptions};
/// const _: Asset = asset!("/assets/script.js", JsAssetOptions::new().with_minify(false)); /// const _: Asset = asset!("/assets/script.js", AssetOptions::js().with_minify(false));
/// ``` /// ```
#[allow(unused)] #[allow(unused)]
pub const fn with_minify(self, minify: bool) -> Self { pub const fn with_minify(mut self, minify: bool) -> Self {
Self { minify, ..self } self.variant.minify = minify;
} self
/// Check if the asset is minified
pub const fn minified(&self) -> bool {
self.minify
} }
/// Make the asset preloaded /// Make the asset preloaded
@@ -59,20 +84,19 @@ impl JsAssetOptions {
/// ///
/// ```rust /// ```rust
/// # use manganis::{asset, Asset, JsAssetOptions}; /// # use manganis::{asset, Asset, JsAssetOptions};
/// const _: Asset = asset!("/assets/script.js", JsAssetOptions::new().with_preload(true)); /// const _: Asset = asset!("/assets/script.js", AssetOptions::js().with_preload(true));
/// ``` /// ```
#[allow(unused)] #[allow(unused)]
pub const fn with_preload(self, preload: bool) -> Self { pub const fn with_preload(mut self, preload: bool) -> Self {
Self { preload, ..self } self.variant.preload = preload;
self
} }
/// Check if the asset is preloaded /// Convert the builder into asset options with the given variant
pub const fn preloaded(&self) -> bool {
self.preload
}
/// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions { pub const fn into_asset_options(self) -> AssetOptions {
AssetOptions::Js(self) AssetOptions {
add_hash: self.add_hash,
variant: AssetVariant::Js(self.variant),
}
} }
} }
+140 -20
View File
@@ -17,9 +17,148 @@ use crate::{
serde::Serialize, serde::Serialize,
serde::Deserialize, serde::Deserialize,
)] )]
#[non_exhaustive]
pub struct AssetOptions {
/// If a hash should be added to the asset path
pub(crate) add_hash: bool,
/// The variant of the asset
pub(crate) variant: AssetVariant,
}
impl AssetOptions {
/// Create a new asset options builder
pub const fn builder() -> AssetOptionsBuilder<()> {
AssetOptionsBuilder::new()
}
/// Get the variant of the asset
pub const fn variant(&self) -> &AssetVariant {
&self.variant
}
/// Check if a hash should be added to the asset path
pub const fn hash_suffix(&self) -> bool {
self.add_hash
}
/// Try to get the extension for the asset. If the asset options don't define an extension, this will return None
pub const fn extension(&self) -> Option<&'static str> {
match self.variant {
AssetVariant::Image(image) => image.extension(),
AssetVariant::Css(_) => Some("css"),
AssetVariant::CssModule(_) => Some("css"),
AssetVariant::Js(_) => Some("js"),
AssetVariant::Folder(_) => None,
AssetVariant::Unknown => None,
}
}
/// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions {
self
}
}
/// A builder for [`AssetOptions`]
///
/// ```rust
/// # use manganis::AssetOptionsBuilder;
/// static ASSET: Asset = asset!(
/// "image.png",
/// AssetOptionsBuilder::new()
/// .with_hash_suffix(false)
/// );
/// ```
pub struct AssetOptionsBuilder<T> {
/// If a hash should be added to the asset path
pub(crate) add_hash: bool,
/// The variant of the asset
pub(crate) variant: T,
}
impl Default for AssetOptionsBuilder<()> {
fn default() -> Self {
Self::default()
}
}
impl AssetOptionsBuilder<()> {
/// Create a new asset options builder with an unknown variant
pub const fn new() -> Self {
Self {
add_hash: true,
variant: (),
}
}
/// Create a default asset options builder
pub const fn default() -> Self {
Self::new()
}
/// Convert the builder into asset options with the given variant
pub const fn into_asset_options(self) -> AssetOptions {
AssetOptions {
add_hash: self.add_hash,
variant: AssetVariant::Unknown,
}
}
}
impl<T> AssetOptionsBuilder<T> {
/// Create a new asset options builder with the given variant
pub(crate) const fn variant(variant: T) -> Self {
Self {
add_hash: true,
variant,
}
}
/// Set whether a hash should be added to the asset path. Manganis adds hashes to asset paths by default
/// for [cache busting](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching#cache_busting).
/// With hashed assets, you can serve the asset with a long expiration time, and when the asset changes,
/// the hash in the path will change, causing the browser to fetch the new version.
///
/// This method will only effect if the hash is added to the bundled asset path. If you are using the asset
/// macro, the asset struct still needs to be used in your rust code to ensure the asset is included in the binary.
///
/// <div class="warning">
///
/// If you are using an asset outside of rust code where you know what the asset hash will be, you must use the
/// `#[used]` attribute to ensure the asset is included in the binary even if it is not referenced in the code.
///
/// ```rust
/// #[used]
/// static ASSET: manganis::Asset = manganis::asset!(
/// "path/to/asset.png",
/// AssetVariant::Unknown.into_asset_options()
/// .with_hash_suffix(false)
/// );
/// ```
///
/// </div>
pub const fn with_hash_suffix(mut self, add_hash: bool) -> Self {
self.add_hash = add_hash;
self
}
}
/// Settings for a specific type of asset
#[derive(
Debug,
Eq,
PartialEq,
PartialOrd,
Clone,
Copy,
Hash,
SerializeConst,
serde::Serialize,
serde::Deserialize,
)]
#[repr(C, u8)] #[repr(C, u8)]
#[non_exhaustive] #[non_exhaustive]
pub enum AssetOptions { pub enum AssetVariant {
/// An image asset /// An image asset
Image(ImageAssetOptions), Image(ImageAssetOptions),
/// A folder asset /// A folder asset
@@ -33,22 +172,3 @@ pub enum AssetOptions {
/// An unknown asset /// An unknown asset
Unknown, Unknown,
} }
impl AssetOptions {
/// Try to get the extension for the asset. If the asset options don't define an extension, this will return None
pub const fn extension(&self) -> Option<&'static str> {
match self {
AssetOptions::Image(image) => image.extension(),
AssetOptions::Css(_) => Some("css"),
AssetOptions::CssModule(_) => Some("css"),
AssetOptions::Js(_) => Some("js"),
AssetOptions::Folder(_) => None,
AssetOptions::Unknown => None,
}
}
/// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> Self {
self
}
}
@@ -35,7 +35,7 @@ impl Parse for AssetParser {
// ``` // ```
// asset!( // asset!(
// "/assets/myfile.png", // "/assets/myfile.png",
// ImageAssetOptions::new() // AssetOptions::image()
// .format(ImageFormat::Jpg) // .format(ImageFormat::Jpg)
// .size(512, 512) // .size(512, 512)
// ) // )
@@ -84,8 +84,9 @@ impl ToTokens for AssetParser {
asset_string.hash(&mut hash); asset_string.hash(&mut hash);
let asset_hash = format!("{:016x}", hash.finish()); let asset_hash = format!("{:016x}", hash.finish());
// Generate the link section for the asset // Generate the link section for the asset. The link section includes the source path and the
// The link section includes the source path and the output path of the asset // output path of the asset. We force the asset to be included in the binary even if it is unused
// if the asset is unhashed
let link_section = crate::generate_link_section(quote!(__ASSET), &asset_hash); let link_section = crate::generate_link_section(quote!(__ASSET), &asset_hash);
// generate the asset::new method to deprecate the `./assets/blah.css` syntax // generate the asset::new method to deprecate the `./assets/blah.css` syntax
@@ -96,7 +97,7 @@ impl ToTokens for AssetParser {
}; };
let options = if self.options.is_empty() { let options = if self.options.is_empty() {
quote! { manganis::AssetOptions::Unknown } quote! { manganis::AssetOptions::builder() }
} else { } else {
self.options.clone() self.options.clone()
}; };
@@ -34,7 +34,7 @@ impl Parse for CssModuleParser {
// Optional options // Optional options
let mut options = input.parse::<TokenStream>()?; let mut options = input.parse::<TokenStream>()?;
if options.is_empty() { if options.is_empty() {
options = quote! { manganis::CssModuleAssetOptions::new() } options = quote! { manganis::AssetOptions::css_module() }
} }
let asset_parser = AssetParser { let asset_parser = AssetParser {
+5 -5
View File
@@ -46,17 +46,17 @@ use linker::generate_link_section;
/// Resize the image at compile time to make the assets file size smaller: /// Resize the image at compile time to make the assets file size smaller:
/// ```rust /// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageSize}; /// # use manganis::{asset, Asset, ImageAssetOptions, ImageSize};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_size(ImageSize::Manual { width: 52, height: 52 })); /// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_size(ImageSize::Manual { width: 52, height: 52 }));
/// ``` /// ```
/// Or convert the image at compile time to a web friendly format: /// Or convert the image at compile time to a web friendly format:
/// ```rust /// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions, ImageSize, ImageFormat}; /// # use manganis::{asset, Asset, ImageAssetOptions, ImageSize, ImageFormat};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_format(ImageFormat::Avif)); /// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Avif));
/// ``` /// ```
/// You can mark images as preloaded to make them load faster in your app /// You can mark images as preloaded to make them load faster in your app
/// ```rust /// ```rust
/// # use manganis::{asset, Asset, ImageAssetOptions}; /// # use manganis::{asset, Asset, ImageAssetOptions};
/// const _: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_preload(true)); /// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_preload(true));
/// ``` /// ```
#[proc_macro] #[proc_macro]
pub fn asset(input: TokenStream) -> TokenStream { pub fn asset(input: TokenStream) -> TokenStream {
@@ -78,7 +78,7 @@ pub fn asset(input: TokenStream) -> TokenStream {
/// - An optional `CssModuleAssetOptions` struct to configure the processing of your CSS module. /// - An optional `CssModuleAssetOptions` struct to configure the processing of your CSS module.
/// ///
/// ```rust /// ```rust
/// css_module!(StylesIdent = "/my.module.css", CssModuleAssetOptions::new()); /// css_module!(StylesIdent = "/my.module.css", AssetOptions::css_module());
/// ``` /// ```
/// ///
/// The styles struct can be made public by appending `pub` before the identifier. /// The styles struct can be made public by appending `pub` before the identifier.
@@ -131,7 +131,7 @@ pub fn asset(input: TokenStream) -> TokenStream {
/// use manganis::CssModuleAssetOptions; /// use manganis::CssModuleAssetOptions;
/// ///
/// css_module!(Styles = "/mycss.module.css", /// css_module!(Styles = "/mycss.module.css",
/// CssModuleAssetOptions::new() /// AssetOptions::css_module()
/// .with_minify(true) /// .with_minify(true)
/// .with_preload(false), /// .with_preload(false),
/// ); /// );
+2 -2
View File
@@ -21,9 +21,9 @@ pub const PNG_ASSET: Asset =
asset!("/assets/image.png"); asset!("/assets/image.png");
// Resize the image at compile time to make the assets smaller // Resize the image at compile time to make the assets smaller
pub const RESIZED_PNG_ASSET: Asset = pub const RESIZED_PNG_ASSET: Asset =
asset!("/assets/image.png", ImageAssetOptions::new().with_size(ImageSize::Manual { width: 52, height: 52 })); asset!("/assets/image.png", AssetOptions::image().with_size(ImageSize::Manual { width: 52, height: 52 }));
// Or convert the image at compile time to a web friendly format // Or convert the image at compile time to a web friendly format
pub const AVIF_ASSET: Asset = asset!("/assets/image.png", ImageAssetOptions::new().with_format(ImageFormat::Avif)); pub const AVIF_ASSET: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Avif));
``` ```
## Adding Support to Your CLI ## Adding Support to Your CLI
+2 -2
View File
@@ -9,6 +9,6 @@ pub use manganis_macro::asset;
pub use manganis_macro::css_module; pub use manganis_macro::css_module;
pub use manganis_core::{ pub use manganis_core::{
Asset, AssetOptions, BundledAsset, CssAssetOptions, CssModuleAssetOptions, FolderAssetOptions, Asset, AssetOptions, AssetVariant, BundledAsset, CssAssetOptions, CssModuleAssetOptions,
ImageAssetOptions, ImageFormat, ImageSize, JsAssetOptions, FolderAssetOptions, ImageAssetOptions, ImageFormat, ImageSize, JsAssetOptions,
}; };
@@ -1,13 +1,12 @@
pub use const_serialize; pub use const_serialize;
use const_serialize::{serialize_const, ConstStr, ConstVec, SerializeConst}; use const_serialize::{serialize_const, ConstVec, SerializeConst};
use manganis_core::{AssetOptions, BundledAsset}; use manganis_core::{AssetOptions, BundledAsset};
const PLACEHOLDER_HASH: ConstStr = const PLACEHOLDER_HASH: &str = "this is a placeholder path which will be replaced by the linker";
ConstStr::new("this is a placeholder path which will be replaced by the linker");
/// Create a bundled asset from the input path, the content hash, and the asset options /// Create a bundled asset from the input path, the content hash, and the asset options
pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset { pub const fn create_bundled_asset(input_path: &str, asset_config: AssetOptions) -> BundledAsset {
BundledAsset::new_from_const(ConstStr::new(input_path), PLACEHOLDER_HASH, asset_config) BundledAsset::new(input_path, PLACEHOLDER_HASH, asset_config)
} }
/// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning /// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning
@@ -23,4 +23,25 @@ test("optimized scripts run", async ({ page }) => {
// Expect the urls to be different // Expect the urls to be different
expect(src).not.toEqual(src2); expect(src).not.toEqual(src2);
// Expect the page to contain an image with the id "some_image_without_hash"
const image3 = page.locator("#some_image_without_hash");
await expect(image3).toBeVisible();
// Get the image src
const src3 = await image3.getAttribute("src");
// Expect the src to be without a hash
expect(src3).toEqual("/assets/toasts.avif");
});
test("unused external assets are bundled", async ({ page }) => {
await page.goto("http://localhost:8989");
// Assert http://localhost:8989/assets/toasts.png is found even though it is not used in the page
const response = await page.request.get(
"http://localhost:8989/assets/toasts.png"
);
// Expect the response to be ok
expect(response.status()).toBe(200);
// make sure the response is an image
expect(response.headers()["content-type"]).toBe("image/png");
}); });
@@ -3,9 +3,20 @@
use dioxus::prelude::*; use dioxus::prelude::*;
const MONACO_FOLDER: Asset = asset!("/monaco-editor/package/min/vs"); const MONACO_FOLDER: Asset = asset!("/monaco-editor/package/min/vs");
const SOME_IMAGE: Asset = asset!("/images/toasts.png", ImageAssetOptions::new().with_avif()); const SOME_IMAGE: Asset = asset!("/images/toasts.png", AssetOptions::image().with_avif());
const SOME_IMAGE_WITH_THE_SAME_URL: Asset = const SOME_IMAGE_WITH_THE_SAME_URL: Asset =
asset!("/images/toasts.png", ImageAssetOptions::new().with_jpg()); asset!("/images/toasts.png", AssetOptions::image().with_jpg());
#[used]
static SOME_IMAGE_WITHOUT_HASH: Asset = asset!(
"/images/toasts.png",
AssetOptions::image().with_avif().with_hash_suffix(false)
);
// This asset is unused, but it should still be bundled because it is an external asset
#[used]
static _ASSET: Asset = asset!(
"/images/toasts.png",
AssetOptions::builder().with_hash_suffix(false)
);
fn main() { fn main() {
dioxus::launch(App); dioxus::launch(App);
@@ -41,5 +52,9 @@ fn App() -> Element {
id: "some_image_with_the_same_url", id: "some_image_with_the_same_url",
src: "{SOME_IMAGE_WITH_THE_SAME_URL}" src: "{SOME_IMAGE_WITH_THE_SAME_URL}"
} }
img {
id: "some_image_without_hash",
src: "{SOME_IMAGE_WITHOUT_HASH}"
}
} }
} }
+27 -20
View File
@@ -75,6 +75,13 @@ test("assets cache correctly", async ({ page }) => {
console.log("Response URL:", resp.url()); console.log("Response URL:", resp.url());
return resp.url().includes("/assets/image-") && resp.status() === 200; return resp.url().includes("/assets/image-") && resp.status() === 200;
}); });
const assetImageFuture = page.waitForResponse(
(resp) => resp.url().includes("/assets/image.png") && resp.status() === 200
);
const nestedAssetImageFuture = page.waitForResponse(
(resp) =>
resp.url().includes("/assets/nested/image.png") && resp.status() === 200
);
// Navigate to the page that includes the image. // Navigate to the page that includes the image.
await page.goto("http://localhost:3333"); await page.goto("http://localhost:3333");
@@ -86,27 +93,27 @@ test("assets cache correctly", async ({ page }) => {
console.log("Cache-Control header:", cacheControl); console.log("Cache-Control header:", cacheControl);
expect(cacheControl).toContain("immutable"); expect(cacheControl).toContain("immutable");
// TODO: raw assets support was removed and needs to be restored // Wait for the asset image to be loaded
// https://github.com/DioxusLabs/dioxus/issues/4115 const assetImageResponse = await assetImageFuture;
// // Wait for the asset image to be loaded console.log("Asset Image Response:", assetImageResponse);
// const assetImageResponse = await page.waitForResponse( // Make sure the asset image cache control header does not contain immutable
// (resp) => resp.url().includes("/assets/image.png") && resp.status() === 200 const assetCacheControl = assetImageResponse.headers()["cache-control"];
// ); console.log("Cache-Control header:", assetCacheControl);
// // Make sure the asset image cache control header does not contain immutable // Expect there to be no cache control header
// const assetCacheControl = assetImageResponse.headers()["cache-control"]; expect(assetCacheControl).toBeFalsy();
// console.log("Cache-Control header:", assetCacheControl);
// expect(assetCacheControl).not.toContain("immutable");
// // Wait for the nested asset image to be loaded // Wait for the nested asset image to be loaded
// const nestedAssetImageResponse = await page.waitForResponse( const nestedAssetImageResponse = await nestedAssetImageFuture;
// (resp) => console.log(
// resp.url().includes("/assets/nested/image.png") && resp.status() === 200 "Nested Asset Image Response:",
// ); nestedAssetImageResponse
// // Make sure the nested asset image cache control header does not contain immutable );
// const nestedAssetCacheControl = // Make sure the nested asset image cache control header does not contain immutable
// nestedAssetImageResponse.headers()["cache-control"]; const nestedAssetCacheControl =
// console.log("Cache-Control header:", nestedAssetCacheControl); nestedAssetImageResponse.headers()["cache-control"];
// expect(nestedAssetCacheControl).not.toContain("immutable"); console.log("Cache-Control header:", nestedAssetCacheControl);
// Expect there to be no cache control header
expect(nestedAssetCacheControl).toBeFalsy();
}); });
test("websockets", async ({ page }) => { test("websockets", async ({ page }) => {
@@ -164,18 +164,20 @@ fn DocumentElements() -> Element {
/// Make sure assets in the assets folder are served correctly and hashed assets are cached forever /// Make sure assets in the assets folder are served correctly and hashed assets are cached forever
#[component] #[component]
fn Assets() -> Element { fn Assets() -> Element {
#[used]
static _ASSET: Asset = asset!("/assets/image.png");
#[used]
static _OTHER_ASSET: Asset = asset!("/assets/nested");
rsx! { rsx! {
img { img {
src: asset!("/assets/image.png"), src: asset!("/assets/image.png"),
} }
// TODO: raw assets support was removed and needs to be restored img {
// https://github.com/DioxusLabs/dioxus/issues/4115 src: "/assets/image.png",
// img { }
// src: "/assets/image.png", img {
// } src: "/assets/nested/image.png",
// img { }
// src: "/assets/nested/image.png",
// }
} }
} }