manganis_macro/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4use std::path::PathBuf;
5
6use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::{quote, ToTokens};
9use syn::{
10    parse::{Parse, ParseStream},
11    parse_macro_input, ItemStruct,
12};
13
14pub(crate) mod asset;
15pub(crate) mod css_module;
16pub(crate) mod linker;
17
18use linker::generate_link_section;
19
20use crate::css_module::{expand_css_module_struct, CssModuleAttribute};
21
22/// The asset macro collects assets that will be included in the final binary
23///
24/// # Files
25///
26/// The file builder collects an arbitrary file. Relative paths are resolved relative to the package root
27/// ```rust
28/// # use manganis::{asset, Asset};
29/// const _: Asset = asset!("/assets/asset.txt");
30/// ```
31/// Macros like `concat!` and `env!` are supported in the asset path.
32/// ```rust
33/// # use manganis::{asset, Asset};
34/// const _: Asset = asset!(concat!("/assets/", env!("CARGO_CRATE_NAME"), ".dat"));
35/// ```
36///
37/// # Images
38///
39/// You can collect images which will be automatically optimized with the image builder:
40/// ```rust
41/// # use manganis::{asset, Asset};
42/// const _: Asset = asset!("/assets/image.png");
43/// ```
44/// Resize the image at compile time to make the assets file size smaller:
45/// ```rust
46/// # use manganis::{asset, Asset, AssetOptions, ImageSize};
47/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_size(ImageSize::Manual { width: 52, height: 52 }));
48/// ```
49/// Or convert the image at compile time to a web friendly format:
50/// ```rust
51/// # use manganis::{asset, Asset, AssetOptions, ImageSize, ImageFormat};
52/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Avif));
53/// ```
54/// You can mark images as preloaded to make them load faster in your app
55/// ```rust
56/// # use manganis::{asset, Asset, AssetOptions};
57/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_preload(true));
58/// ```
59#[proc_macro]
60pub fn asset(input: TokenStream) -> TokenStream {
61    let asset = parse_macro_input!(input as asset::AssetParser);
62
63    quote! { #asset }.into_token_stream().into()
64}
65
66/// Resolve an asset at compile time, returning `None` if the asset does not exist.
67///
68/// This behaves like the `asset!` macro when the asset can be resolved, but mirrors
69/// [`option_env!`](core::option_env) by returning an `Option` instead of emitting a compile error
70/// when the asset is missing.
71///
72/// ```rust
73/// # use manganis::{asset, option_asset, Asset};
74/// const REQUIRED: Asset = asset!("/assets/style.css");
75/// const OPTIONAL: Option<Asset> = option_asset!("/assets/maybe.css");
76/// ```
77#[proc_macro]
78pub fn option_asset(input: TokenStream) -> TokenStream {
79    let asset = parse_macro_input!(input as asset::AssetParser);
80
81    asset.expand_option_tokens().into()
82}
83
84/// Generate type-safe styles with scoped CSS class names.
85///
86/// The `css_module` attribute macro creates scoped CSS modules that prevent class name collisions
87/// by making each class globally unique. It expands the annotated struct to provide type-safe
88/// identifiers for your CSS classes, allowing you to reference styles in your Rust code with
89/// compile-time guarantees.
90///
91/// # Syntax
92///
93/// The `css_module` attribute takes:
94/// - The asset string path - the absolute path (from the crate root) to your CSS file.
95/// - Optional `AssetOptions` to configure the processing of your CSS module.
96///
97/// It must be applied to a unit struct:
98/// ```rust, ignore
99/// #[css_module("/assets/my-styles.css")]
100/// struct Styles;
101///
102/// #[css_module("/assets/my-styles.css", AssetOptions::css_module().with_minify(true))]
103/// struct Styles;
104/// ```
105///
106/// # Generation
107///
108/// The `css_module` attribute macro does two things:
109/// - It generates an asset and automatically inserts it as a stylesheet link in the document.
110/// - It expands the annotated struct with snake-case associated constants for your CSS class names.
111///
112/// ```rust, ignore
113/// // This macro usage:
114/// #[css_module("/assets/mycss.css")]
115/// struct Styles;
116///
117/// // Will expand the struct to (simplified):
118/// struct Styles {}
119///
120/// impl Styles {
121///     // Snake-cased class names can be accessed like this:
122///     pub const your_class: &str = "your_class-a1b2c3";
123/// }
124/// ```
125///
126/// # CSS Class Name Scoping
127///
128/// **The macro only processes CSS class selectors (`.class-name`).** Other selectors like IDs (`#id`),
129/// element selectors (`div`, `p`), attribute selectors, etc. are left unchanged and not exposed as
130/// Rust constants.
131///
132/// The macro collects all class selectors in your CSS file and transforms them to be globally unique
133/// by appending a hash. For example, `.myClass` becomes `.myClass-a1b2c3` where `a1b2c3` is a hash
134/// of the file path.
135///
136/// Class names are converted to snake_case for the Rust constants. For example:
137/// - `.fooBar` becomes `Styles::foo_bar`
138/// - `.my-class` becomes `Styles::my_class`
139///
140/// To prevent a class from being scoped, wrap it in `:global()`:
141/// ```css
142/// /* This class will be scoped */
143/// .my-class { color: blue; }
144///
145/// /* This class will NOT be scoped (no hash added) */
146/// :global(.global-class) { color: red; }
147///
148/// /* Element selectors and other CSS remain unchanged */
149/// div { margin: 0; }
150/// #my-id { padding: 10px; }
151/// ```
152///
153/// # Using Multiple CSS Modules
154///
155/// Multiple `css_module` attributes can be used in the same scope by applying them to different structs:
156/// ```rust, ignore
157/// // First CSS module
158/// #[css_module("/assets/styles1.css")]
159/// struct Styles;
160///
161/// // Second CSS module with a different struct name
162/// #[css_module("/assets/styles2.css")]
163/// struct OtherStyles;
164///
165/// // Access classes from both:
166/// rsx! {
167///     div { class: Styles::container }
168///     div { class: OtherStyles::button }
169/// }
170/// ```
171///
172/// # Asset Options
173///
174/// Similar to the `asset!()` macro, you can pass optional `AssetOptions` to configure processing:
175/// ```rust, ignore
176/// #[css_module(
177///     "/assets/mycss.css",
178///     AssetOptions::css_module()
179///         .with_minify(true)
180///         .with_preload(false)
181/// )]
182/// struct Styles;
183/// ```
184///
185/// # Example
186///
187/// First create a CSS file:
188/// ```css
189/// /* assets/styles.css */
190///
191/// .container {
192///     padding: 20px;
193/// }
194///
195/// .button {
196///     background-color: #373737;
197/// }
198///
199/// :global(.global-text) {
200///     font-weight: bold;
201/// }
202/// ```
203///
204/// Then use the `css_module` attribute:
205/// ```rust, ignore
206/// use dioxus::prelude::*;
207///
208/// fn app() -> Element {
209///     #[css_module("/assets/styles.css")]
210///     struct Styles;
211///     
212///     rsx! {
213///         div { class: Styles::container,
214///             button { class: Styles::button, "Click me" }
215///             span { class: Styles::global_text, "This uses global class" }
216///         }
217///     }
218/// }
219/// ```
220#[proc_macro_attribute]
221pub fn css_module(input: TokenStream, item: TokenStream) -> TokenStream {
222    let attribute = parse_macro_input!(input as CssModuleAttribute);
223    let item_struct = parse_macro_input!(item as ItemStruct);
224    let mut tokens = proc_macro2::TokenStream::new();
225    expand_css_module_struct(&mut tokens, &attribute, &item_struct);
226    tokens.into()
227}
228
229fn resolve_path(raw: &str, span: Span) -> Result<PathBuf, AssetParseError> {
230    // Get the location of the root of the crate which is where all assets are relative to
231    //
232    // IE
233    // /users/dioxus/dev/app/
234    // is the root of
235    // /users/dioxus/dev/app/assets/blah.css
236    let manifest_dir = dunce::canonicalize(
237        std::env::var("CARGO_MANIFEST_DIR")
238            .map(PathBuf::from)
239            .unwrap(),
240    )
241    .unwrap();
242
243    // 1. the input file should be a pathbuf
244    let input = PathBuf::from(raw);
245
246    let path = if raw.starts_with('.') {
247        if let Some(local_folder) = span.local_file().as_ref().and_then(|f| f.parent()) {
248            local_folder.join(raw)
249        } else {
250            // If we are running in rust analyzer, just assume the path is valid and return an error when
251            // we compile if it doesn't exist
252            if looks_like_rust_analyzer(&span) {
253                return Ok(
254                    "The asset macro was expanded under Rust Analyzer which doesn't support paths or local assets yet"
255                        .into(),
256                );
257            }
258
259            // Otherwise, return an error about the version of rust required for relative assets
260            return Err(AssetParseError::RelativeAssetPath);
261        }
262    } else {
263        manifest_dir.join(raw.trim_start_matches('/'))
264    };
265
266    // 2. absolute path to the asset
267    let Ok(path) = std::path::absolute(path) else {
268        return Err(AssetParseError::InvalidPath {
269            path: input.clone(),
270        });
271    };
272
273    // 3. Ensure the path exists
274    let Ok(path) = dunce::canonicalize(path) else {
275        return Err(AssetParseError::AssetDoesntExist {
276            path: input.clone(),
277        });
278    };
279
280    // 4. Ensure the path doesn't escape the crate dir
281    //
282    // - Note: since we called canonicalize on both paths, we can safely compare the parent dirs.
283    //   On windows, we can only compare the prefix if both paths are canonicalized (not just absolute)
284    //   https://github.com/rust-lang/rust/issues/42869
285    if path == manifest_dir || !path.starts_with(manifest_dir) {
286        return Err(AssetParseError::InvalidPath { path });
287    }
288
289    Ok(path)
290}
291
292/// Parse `T`, while also collecting the tokens it was parsed from.
293fn parse_with_tokens<T: Parse>(input: ParseStream) -> syn::Result<(T, proc_macro2::TokenStream)> {
294    let begin = input.cursor();
295    let t: T = input.parse()?;
296    let end = input.cursor();
297
298    let mut cursor = begin;
299    let mut tokens = proc_macro2::TokenStream::new();
300    while cursor != end {
301        let (tt, next) = cursor.token_tree().unwrap();
302        tokens.extend(std::iter::once(tt));
303        cursor = next;
304    }
305
306    Ok((t, tokens))
307}
308
309#[derive(Debug)]
310enum AssetParseError {
311    AssetDoesntExist { path: PathBuf },
312    InvalidPath { path: PathBuf },
313    RelativeAssetPath,
314}
315
316impl std::fmt::Display for AssetParseError {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        match self {
319            AssetParseError::AssetDoesntExist { path } => {
320                write!(f, "Asset at {} doesn't exist", path.display())
321            }
322            AssetParseError::InvalidPath { path } => {
323                write!(
324                    f,
325                    "Asset path {} is invalid. Make sure the asset exists within this crate.",
326                    path.display()
327                )
328            }
329            AssetParseError::RelativeAssetPath => write!(f, "Failed to resolve relative asset path. Relative assets are only supported in rust 1.88+."),
330        }
331    }
332}
333
334/// Rust analyzer doesn't provide a stable way to detect if macros are running under it.
335/// This function uses heuristics to determine if we are running under rust analyzer for better error
336/// messages.
337fn looks_like_rust_analyzer(span: &Span) -> bool {
338    // Rust analyzer spans have a struct debug impl compared to rustcs custom debug impl
339    // RA Example: SpanData { range: 45..58, anchor: SpanAnchor(EditionedFileId(0, Edition2024), ErasedFileAstId { kind: Fn, index: 0, hash: 9CD8 }), ctx: SyntaxContext(4294967036) }
340    // Rustc Example: #0 bytes(70..83)
341    let looks_like_rust_analyzer_span = format!("{:?}", span).contains("ctx:");
342    // The rust analyzer macro expander runs under RUST_ANALYZER_INTERNALS_DO_NOT_USE
343    let looks_like_rust_analyzer_env = std::env::var("RUST_ANALYZER_INTERNALS_DO_NOT_USE").is_ok();
344    // The rust analyzer executable is named rust-analyzer-proc-macro-srv
345    let looks_like_rust_analyzer_exe = std::env::current_exe().ok().is_some_and(|p| {
346        p.file_stem()
347            .and_then(|s| s.to_str())
348            .is_some_and(|s| s.contains("rust-analyzer"))
349    });
350    looks_like_rust_analyzer_span || looks_like_rust_analyzer_env || looks_like_rust_analyzer_exe
351}