Add option_asset! to match asset! (#4791)

This commit is contained in:
Amar Sood authored and GitHub committed 2025-10-28 11:26:21 -07:00
1 parent e656bace5c
commit 8adea41efa
5 files changed
+77 -16

No files matched your search

+33 -16
View File
@@ -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::<manganis::Asset>::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) }
}
}
@@ -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<Asset> = 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.
@@ -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<Asset> = 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<Asset> = option_asset!("/assets/does_not_exist.txt");
assert!(OPTIONAL.is_none());
}
+8
View File
@@ -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<Asset> = 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
+1
View File
@@ -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;