manganis_macro/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4use std::{
5    hash::Hasher,
6    io::Read,
7    path::{Path, PathBuf},
8};
9
10use css_module::CssModuleParser;
11use proc_macro::TokenStream;
12use proc_macro2::Span;
13use quote::{quote, ToTokens};
14use syn::{
15    parse::{Parse, ParseStream},
16    parse_macro_input,
17};
18
19pub(crate) mod asset;
20pub(crate) mod css_module;
21pub(crate) mod linker;
22
23use linker::generate_link_section;
24
25/// The asset macro collects assets that will be included in the final binary
26///
27/// # Files
28///
29/// The file builder collects an arbitrary file. Relative paths are resolved relative to the package root
30/// ```rust
31/// # use manganis::{asset, Asset};
32/// const _: Asset = asset!("/assets/asset.txt");
33/// ```
34/// Macros like `concat!` and `env!` are supported in the asset path.
35/// ```rust
36/// # use manganis::{asset, Asset};
37/// const _: Asset = asset!(concat!("/assets/", env!("CARGO_CRATE_NAME"), ".dat"));
38/// ```
39///
40/// # Images
41///
42/// You can collect images which will be automatically optimized with the image builder:
43/// ```rust
44/// # use manganis::{asset, Asset};
45/// const _: Asset = asset!("/assets/image.png");
46/// ```
47/// Resize the image at compile time to make the assets file size smaller:
48/// ```rust
49/// # use manganis::{asset, Asset, AssetOptions, ImageSize};
50/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_size(ImageSize::Manual { width: 52, height: 52 }));
51/// ```
52/// Or convert the image at compile time to a web friendly format:
53/// ```rust
54/// # use manganis::{asset, Asset, AssetOptions, ImageSize, ImageFormat};
55/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_format(ImageFormat::Avif));
56/// ```
57/// You can mark images as preloaded to make them load faster in your app
58/// ```rust
59/// # use manganis::{asset, Asset, AssetOptions};
60/// const _: Asset = asset!("/assets/image.png", AssetOptions::image().with_preload(true));
61/// ```
62#[proc_macro]
63pub fn asset(input: TokenStream) -> TokenStream {
64    let asset = parse_macro_input!(input as asset::AssetParser);
65
66    quote! { #asset }.into_token_stream().into()
67}
68
69/// Resolve an asset at compile time, returning `None` if the asset does not exist.
70///
71/// This behaves like the `asset!` macro when the asset can be resolved, but mirrors
72/// [`option_env!`](core::option_env) by returning an `Option` instead of emitting a compile error
73/// when the asset is missing.
74///
75/// ```rust
76/// # use manganis::{asset, option_asset, Asset};
77/// const REQUIRED: Asset = asset!("/assets/style.css");
78/// const OPTIONAL: Option<Asset> = option_asset!("/assets/maybe.css");
79/// ```
80#[proc_macro]
81pub fn option_asset(input: TokenStream) -> TokenStream {
82    let asset = parse_macro_input!(input as asset::AssetParser);
83
84    asset.expand_option_tokens().into()
85}
86
87/// Generate type-safe and globally-unique CSS identifiers from a CSS module.
88///
89/// 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.
90/// The `css_module!()` macro allows you to utilize CSS modules in your Rust projects.
91///
92/// # Syntax
93///
94/// The `css_module!()` macro takes a few items.
95/// - A styles struct identifier. This is the `struct` you use to access your type-safe CSS identifiers in Rust.
96/// - The asset string path. This is the absolute path (from the crate root) to your CSS module.
97/// - An optional `CssModuleAssetOptions` struct to configure the processing of your CSS module.
98///
99/// ```rust, ignore
100/// css_module!(StylesIdent = "/my.module.css", AssetOptions::css_module());
101/// ```
102///
103/// The styles struct can be made public by appending `pub` before the identifier.
104/// Read the [Variable Visibility](#variable-visibility) section for more information.
105///
106/// # Generation
107///
108/// The `css_module!()` macro does two few things:
109/// - It generates an asset using the `asset!()` macro and automatically inserts it into the app meta.
110/// - It generates a struct with snake-case associated constants of your CSS idents.
111///
112/// ```rust, ignore
113/// // This macro usage:
114/// css_module!(Styles = "/mycss.module.css");
115///
116/// // Will generate this (simplified):
117/// struct Styles {}
118///
119/// impl Styles {
120///     // This can be accessed with `Styles::your_ident`
121///     pub const your_ident: &str = "abc";
122/// }
123/// ```
124///
125/// # CSS Identifier Collection
126/// The macro will collect all identifiers used in your CSS module, convert them into snake_case, and generate a struct and fields around those identifier names.
127///
128/// For example, `#fooBar` will become `foo_bar`.
129///
130/// Identifier used only inside of a media query, will not be collected (not yet supported). To get around this, you can use an empty block for the identifier:
131/// ```css
132/// /* Empty ident block to ensure collection */
133/// #foo {}
134///
135/// @media ... {
136///     #foo { ... }
137/// }
138/// ```
139///
140/// # Variable Visibility
141/// If you want your asset or styles constant to be public, you can add the `pub` keyword in front of them.
142/// Restricted visibility (`pub(super)`, `pub(crate)`, etc) is also supported.
143/// ```rust, ignore
144/// css_module!(pub Styles = "/mycss.module.css");
145/// ```
146///
147/// # Asset Options
148/// Similar to the  `asset!()` macro, you can pass an optional `CssModuleAssetOptions` to configure a few processing settings.
149/// ```rust, ignore
150/// use manganis::CssModuleAssetOptions;
151///
152/// css_module!(Styles = "/mycss.module.css",
153///     AssetOptions::css_module()
154///         .with_minify(true)
155///         .with_preload(false),
156/// );
157/// ```
158///
159/// # Examples
160/// First you need a CSS module:
161/// ```css
162/// /* mycss.module.css */
163///
164/// #header {
165///     padding: 50px;
166/// }
167///
168/// .header {
169///     margin: 20px;
170/// }
171///
172/// .button {
173///     background-color: #373737;
174/// }
175/// ```
176/// Then you can use the `css_module!()` macro in your Rust project:
177/// ```rust, ignore
178/// css_module!(Styles = "/mycss.module.css");
179///
180/// println!("{}", Styles::header);
181/// println!("{}", Styles::header_class);
182/// println!("{}", Styles::button);
183/// ```
184#[proc_macro]
185#[doc(hidden)]
186pub fn css_module(input: TokenStream) -> TokenStream {
187    let style = parse_macro_input!(input as CssModuleParser);
188    quote! { #style }.into_token_stream().into()
189}
190
191fn resolve_path(raw: &str, span: Span) -> Result<PathBuf, AssetParseError> {
192    // Get the location of the root of the crate which is where all assets are relative to
193    //
194    // IE
195    // /users/dioxus/dev/app/
196    // is the root of
197    // /users/dioxus/dev/app/assets/blah.css
198    let manifest_dir = dunce::canonicalize(
199        std::env::var("CARGO_MANIFEST_DIR")
200            .map(PathBuf::from)
201            .unwrap(),
202    )
203    .unwrap();
204
205    // 1. the input file should be a pathbuf
206    let input = PathBuf::from(raw);
207
208    let path = if raw.starts_with('.') {
209        if let Some(local_folder) = span.local_file().as_ref().and_then(|f| f.parent()) {
210            local_folder.join(raw)
211        } else {
212            // If we are running in rust analyzer, just assume the path is valid and return an error when
213            // we compile if it doesn't exist
214            if looks_like_rust_analyzer(&span) {
215                return Ok(
216                    "The asset macro was expanded under Rust Analyzer which doesn't support paths or local assets yet"
217                        .into(),
218                );
219            }
220
221            // Otherwise, return an error about the version of rust required for relative assets
222            return Err(AssetParseError::RelativeAssetPath);
223        }
224    } else {
225        manifest_dir.join(raw.trim_start_matches('/'))
226    };
227
228    // 2. absolute path to the asset
229    let Ok(path) = std::path::absolute(path) else {
230        return Err(AssetParseError::InvalidPath {
231            path: input.clone(),
232        });
233    };
234
235    // 3. Ensure the path exists
236    let Ok(path) = dunce::canonicalize(path) else {
237        return Err(AssetParseError::AssetDoesntExist {
238            path: input.clone(),
239        });
240    };
241
242    // 4. Ensure the path doesn't escape the crate dir
243    //
244    // - Note: since we called canonicalize on both paths, we can safely compare the parent dirs.
245    //   On windows, we can only compare the prefix if both paths are canonicalized (not just absolute)
246    //   https://github.com/rust-lang/rust/issues/42869
247    if path == manifest_dir || !path.starts_with(manifest_dir) {
248        return Err(AssetParseError::InvalidPath { path });
249    }
250
251    Ok(path)
252}
253
254fn hash_file_contents(file_path: &Path) -> Result<u64, AssetParseError> {
255    // Create a hasher
256    let mut hash = std::collections::hash_map::DefaultHasher::new();
257
258    // If this is a folder, hash the folder contents
259    if file_path.is_dir() {
260        let files = std::fs::read_dir(file_path).map_err(|err| AssetParseError::IoError {
261            err,
262            path: file_path.to_path_buf(),
263        })?;
264        for file in files.flatten() {
265            let path = file.path();
266            hash_file_contents(&path)?;
267        }
268        return Ok(hash.finish());
269    }
270
271    // Otherwise, open the file to get its contents
272    let mut file = std::fs::File::open(file_path).map_err(|err| AssetParseError::IoError {
273        err,
274        path: file_path.to_path_buf(),
275    })?;
276
277    // We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
278    // The hash includes the file contents, the options, and the version of manganis. From the macro, we just
279    // know the file contents, so we only include that hash
280    let mut buffer = [0; 8192];
281    loop {
282        let read = file
283            .read(&mut buffer)
284            .map_err(AssetParseError::FailedToReadAsset)?;
285        if read == 0 {
286            break;
287        }
288        hash.write(&buffer[..read]);
289    }
290
291    Ok(hash.finish())
292}
293
294/// Parse `T`, while also collecting the tokens it was parsed from.
295fn parse_with_tokens<T: Parse>(input: ParseStream) -> syn::Result<(T, proc_macro2::TokenStream)> {
296    let begin = input.cursor();
297    let t: T = input.parse()?;
298    let end = input.cursor();
299
300    let mut cursor = begin;
301    let mut tokens = proc_macro2::TokenStream::new();
302    while cursor != end {
303        let (tt, next) = cursor.token_tree().unwrap();
304        tokens.extend(std::iter::once(tt));
305        cursor = next;
306    }
307
308    Ok((t, tokens))
309}
310
311#[derive(Debug)]
312enum AssetParseError {
313    AssetDoesntExist { path: PathBuf },
314    IoError { err: std::io::Error, path: PathBuf },
315    InvalidPath { path: PathBuf },
316    FailedToReadAsset(std::io::Error),
317    RelativeAssetPath,
318}
319
320impl std::fmt::Display for AssetParseError {
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        match self {
323            AssetParseError::AssetDoesntExist { path } => {
324                write!(f, "Asset at {} doesn't exist", path.display())
325            }
326            AssetParseError::IoError { path, err } => {
327                write!(f, "Failed to read file: {}; {}", path.display(), err)
328            }
329            AssetParseError::InvalidPath { path } => {
330                write!(
331                    f,
332                    "Asset path {} is invalid. Make sure the asset exists within this crate.",
333                    path.display()
334                )
335            }
336            AssetParseError::FailedToReadAsset(err) => write!(f, "Failed to read asset: {}", err),
337            AssetParseError::RelativeAssetPath => write!(f, "Failed to resolve relative asset path. Relative assets are only supported in rust 1.88+."),
338        }
339    }
340}
341
342/// Rust analyzer doesn't provide a stable way to detect if macros are running under it.
343/// This function uses heuristics to determine if we are running under rust analyzer for better error
344/// messages.
345fn looks_like_rust_analyzer(span: &Span) -> bool {
346    // Rust analyzer spans have a struct debug impl compared to rustcs custom debug impl
347    // RA Example: SpanData { range: 45..58, anchor: SpanAnchor(EditionedFileId(0, Edition2024), ErasedFileAstId { kind: Fn, index: 0, hash: 9CD8 }), ctx: SyntaxContext(4294967036) }
348    // Rustc Example: #0 bytes(70..83)
349    let looks_like_rust_analyzer_span = format!("{:?}", span).contains("ctx:");
350    // The rust analyzer macro expander runs under RUST_ANALYZER_INTERNALS_DO_NOT_USE
351    let looks_like_rust_analyzer_env = std::env::var("RUST_ANALYZER_INTERNALS_DO_NOT_USE").is_ok();
352    // The rust analyzer executable is named rust-analyzer-proc-macro-srv
353    let looks_like_rust_analyzer_exe = std::env::current_exe().ok().is_some_and(|p| {
354        p.file_stem()
355            .and_then(|s| s.to_str())
356            .is_some_and(|s| s.contains("rust-analyzer"))
357    });
358    looks_like_rust_analyzer_span || looks_like_rust_analyzer_env || looks_like_rust_analyzer_exe
359}