From 8adea41efab192059e334528391d77be58e20475 Mon Sep 17 00:00:00 2001 From: Amar Sood Date: Tue, 28 Oct 2025 14:26:21 -0400 Subject: [PATCH] Add option_asset! to match asset! (#4791) --- packages/manganis/manganis-macro/src/asset.rs | 49 +++++++++++++------ packages/manganis/manganis-macro/src/lib.rs | 18 +++++++ .../manganis-macro/tests/option_asset.rs | 17 +++++++ packages/manganis/manganis/README.md | 8 +++ packages/manganis/manganis/src/lib.rs | 1 + 5 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 packages/manganis/manganis-macro/tests/option_asset.rs diff --git a/packages/manganis/manganis-macro/src/asset.rs b/packages/manganis/manganis-macro/src/asset.rs index cfb07548d..0367f10f7 100644 --- a/packages/manganis/manganis-macro/src/asset.rs +++ b/packages/manganis/manganis-macro/src/asset.rs @@ -1,10 +1,10 @@ use crate::{resolve_path, AssetParseError}; use macro_string::MacroString; use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; +use quote::{quote, ToTokens}; use std::{ hash::{DefaultHasher, Hash, Hasher}, - path::PathBuf, + path::{Path, PathBuf}, }; use syn::{ parse::{Parse, ParseStream}, @@ -66,14 +66,15 @@ impl ToTokens for AssetParser { // - The macro needs to output the absolute path to the asset for the bundler to find later // - It also needs to serialize the bundled asset along with the asset options for the bundler to use later fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let asset = match self.asset.as_ref() { - Ok(asset) => asset, - Err(err) => { - let err = err.to_string(); - tokens.append_all(quote! { compile_error!(#err) }); - return; - } - }; + match self.asset.as_ref() { + Ok(asset) => tokens.extend(self.expand_asset_tokens(asset)), + Err(err) => tokens.extend(self.error_tokens(err)), + } + } +} + +impl AssetParser { + pub(crate) fn expand_asset_tokens(&self, asset: &Path) -> proc_macro2::TokenStream { let asset_string = asset.to_string_lossy(); let mut asset_str = proc_macro2::Literal::string(&asset_string); asset_str.set_span(self.path_expr.span()); @@ -91,9 +92,9 @@ impl ToTokens for AssetParser { // generate the asset::new method to deprecate the `./assets/blah.css` syntax let constructor = if asset.is_relative() { - quote::quote! { create_bundled_asset_relative } + quote! { create_bundled_asset_relative } } else { - quote::quote! { create_bundled_asset } + quote! { create_bundled_asset } }; let options = if self.options.is_empty() { @@ -102,7 +103,7 @@ impl ToTokens for AssetParser { self.options.clone() }; - tokens.extend(quote! { + quote! { { // The source is used by the CLI to copy the asset const __ASSET_SOURCE_PATH: &'static str = #asset_str; @@ -120,10 +121,26 @@ impl ToTokens for AssetParser { static __REFERENCE_TO_LINK_SECTION: &'static [u8] = &__LINK_SECTION; - - manganis::Asset::new(|| unsafe { std::ptr::read_volatile(&__REFERENCE_TO_LINK_SECTION) }) } - }) + } + } + + pub(crate) fn expand_option_tokens(&self) -> proc_macro2::TokenStream { + match self.asset.as_ref() { + Ok(asset) => { + let asset_tokens = self.expand_asset_tokens(asset); + quote! { ::core::option::Option::Some(#asset_tokens) } + } + Err(AssetParseError::AssetDoesntExist { .. }) => { + quote! { ::core::option::Option::::None } + } + Err(err) => self.error_tokens(err), + } + } + + fn error_tokens(&self, err: &AssetParseError) -> proc_macro2::TokenStream { + let err = err.to_string(); + quote! { compile_error!(#err) } } } diff --git a/packages/manganis/manganis-macro/src/lib.rs b/packages/manganis/manganis-macro/src/lib.rs index 47a18a92e..467ee3e9e 100644 --- a/packages/manganis/manganis-macro/src/lib.rs +++ b/packages/manganis/manganis-macro/src/lib.rs @@ -66,6 +66,24 @@ pub fn asset(input: TokenStream) -> TokenStream { quote! { #asset }.into_token_stream().into() } +/// Resolve an asset at compile time, returning `None` if the asset does not exist. +/// +/// This behaves like the `asset!` macro when the asset can be resolved, but mirrors +/// [`option_env!`](core::option_env) by returning an `Option` instead of emitting a compile error +/// when the asset is missing. +/// +/// ```rust +/// # use manganis::{asset, option_asset, Asset}; +/// const REQUIRED: Asset = asset!("/assets/style.css"); +/// const OPTIONAL: Option = option_asset!("/assets/maybe.css"); +/// ``` +#[proc_macro] +pub fn option_asset(input: TokenStream) -> TokenStream { + let asset = parse_macro_input!(input as asset::AssetParser); + + asset.expand_option_tokens().into() +} + /// Generate type-safe and globally-unique CSS identifiers from a CSS module. /// /// CSS modules allow you to have unique, scoped and type-safe CSS identifiers. A CSS module is a CSS file with the `.module.css` file extension. diff --git a/packages/manganis/manganis-macro/tests/option_asset.rs b/packages/manganis/manganis-macro/tests/option_asset.rs new file mode 100644 index 000000000..c357adc6c --- /dev/null +++ b/packages/manganis/manganis-macro/tests/option_asset.rs @@ -0,0 +1,17 @@ +use manganis::{asset, option_asset, Asset}; + +#[test] +fn resolves_existing_asset() { + const REQUIRED: Asset = asset!("/assets/asset.txt"); + const OPTIONAL: Option = option_asset!("/assets/asset.txt"); + + let optional = OPTIONAL.expect("option_asset! should return Some for existing assets"); + assert_eq!(optional.to_string(), REQUIRED.to_string()); +} + +#[test] +fn missing_asset_returns_none() { + const OPTIONAL: Option = option_asset!("/assets/does_not_exist.txt"); + + assert!(OPTIONAL.is_none()); +} diff --git a/packages/manganis/manganis/README.md b/packages/manganis/manganis/README.md index 81a5486dd..01deab8ae 100644 --- a/packages/manganis/manganis/README.md +++ b/packages/manganis/manganis/README.md @@ -11,6 +11,14 @@ const AVIF_ASSET: Asset = manganis::asset!("/assets/image.png"); AVIF_ASSET will be set to a new file name that will be served by some CLI. That file can be collected by any package that depends on the component library. +If you have assets that may not always be bundled, you can fall back gracefully with `option_asset!`: + +```rust +use manganis::{Asset, asset, option_asset}; +const REQUIRED: Asset = asset!("/assets/style.css"); +const OPTIONAL: Option = option_asset!("/assets/missing.css"); +``` + ```rust use manganis::{ImageFormat, Asset, asset, ImageSize, AssetOptions}; // You can collect arbitrary files. Absolute paths are resolved relative to the package root diff --git a/packages/manganis/manganis/src/lib.rs b/packages/manganis/manganis/src/lib.rs index fdb8876f8..c7b867665 100644 --- a/packages/manganis/manganis/src/lib.rs +++ b/packages/manganis/manganis/src/lib.rs @@ -4,6 +4,7 @@ #[doc(hidden)] pub mod macro_helpers; pub use manganis_macro::asset; +pub use manganis_macro::option_asset; #[doc(hidden)] pub use manganis_macro::css_module;