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}