Skip to main content

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