Skip to main content

xtask_wasm_run_example/
lib.rs

1use quote::{quote, quote_spanned};
2use syn::spanned::Spanned;
3use syn::{parse, parse_macro_input};
4
5/// This macro helps to run an example in the project's `examples/` directory using a development
6/// server.
7///
8/// The macro expands into:
9///
10/// * A `#[wasm_bindgen(start)]` entry-point (compiled only for `wasm32`) that runs your function
11///   body.
12/// * A native `main` (compiled for all other targets) with `dist` and `start` subcommands, so
13///   `cargo run --example my_example` automatically builds and serves the Wasm bundle without any
14///   separate `xtask/` crate.
15///
16/// # Usage
17///
18/// ## Minimal example (no arguments)
19///
20/// When no arguments are passed the macro auto-generates a minimal `index.html` that loads
21/// `app.js`. This is the recommended approach for most projects.
22///
23/// * `examples/my_example.rs`:
24///
25///   ```rust,ignore
26///   use wasm_bindgen::prelude::*;
27///
28///   #[wasm_bindgen]
29///   extern "C" {
30///       #[wasm_bindgen(js_namespace = console)]
31///       fn log(s: &str);
32///   }
33///
34///   #[xtask_wasm::run_example]
35///   fn run_app() {
36///       log("Hello from Wasm!");
37///   }
38///   ```
39///
40/// * `Cargo.toml` (dev-dependency, wasm32 target only so native builds stay clean):
41///
42///   ```toml
43///   [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
44///   xtask-wasm = { version = "*", features = ["run-example"] }
45///   ```
46///
47/// * Run the development server:
48///
49///   ```console
50///   cargo run --example my_example
51///   ```
52///
53/// ## egui / eframe example
54///
55/// [eframe](https://crates.io/crates/eframe) builds on top of `wasm_bindgen` and uses
56/// `web_sys::HtmlCanvasElement` directly — no canvas ID string. The pattern below is compatible
57/// with eframe 0.29+.
58///
59/// * `examples/webapp.rs`:
60///
61///   ```rust,ignore
62///   use eframe::wasm_bindgen::JsCast as _;
63///
64///   #[xtask_wasm::run_example]
65///   fn run() {
66///       // Create a <canvas> element dynamically — no index.html canvas needed.
67///       let document = web_sys::window().unwrap().document().unwrap();
68///       let body = document.body().unwrap();
69///       let canvas = document
70///           .create_element("canvas").unwrap()
71///           .dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
72///       canvas.set_id("the_canvas_id");
73///       canvas.set_attribute("style", "width:100%;height:100%").unwrap();
74///       body.append_child(&canvas).unwrap();
75///
76///       let runner = eframe::WebRunner::new();
77///       // `spawn_local` drives the async start() future on the wasm32 executor.
78///       wasm_bindgen_futures::spawn_local(async move {
79///           runner
80///               .start(canvas, eframe::WebOptions::default(), Box::new(|_cc| {
81///                   Ok(Box::new(MyApp::default()))
82///               }))
83///               .await
84///               .expect("failed to start eframe");
85///       });
86///   }
87///   ```
88///
89/// * `Cargo.toml`:
90///
91///   ```toml
92///   [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
93///   xtask-wasm = { version = "*", features = ["run-example"] }
94///   eframe = { version = "0.29", default-features = false, features = ["glow"] }
95///   wasm-bindgen-futures = "0.4"
96///   web-sys = { version = "0.3", features = ["HtmlCanvasElement", "Document", "Window", "Element", "HtmlElement"] }
97///   ```
98///
99/// ## Arguments
100///
101/// You can give arguments to the macro to customise the example:
102///
103/// * `app_name` - Override the app name used by [`xtask_wasm::Dist`].
104/// * `index` - Provide the full content of a custom `index.html` as a string expression.
105/// * `assets_dir` - Path to a custom assets directory.
106///
107/// > **Warning — `app_name` / `assets_dir` suppress the auto-generated `index.html`.**
108/// >
109/// > When either `app_name` or `assets_dir` is set (and `index` is not), the macro skips writing
110/// > `index.html` into the dist directory. You must then supply your own `index.html` — either via
111/// > the `index` argument or by placing it in the `assets_dir`. Forgetting this results in a blank
112/// > page with no errors, which can be confusing. If you don't need a custom app name or assets
113/// > directory, omit all arguments so the macro generates a working HTML page automatically.
114#[proc_macro_attribute]
115pub fn run_example(
116    attr: proc_macro::TokenStream,
117    item: proc_macro::TokenStream,
118) -> proc_macro::TokenStream {
119    let item = parse_macro_input!(item as syn::ItemFn);
120    let attr = parse_macro_input!(attr with RunExample::parse);
121
122    attr.generate(item)
123        .unwrap_or_else(|err| err.to_compile_error())
124        .into()
125}
126
127struct RunExample {
128    index: Option<syn::Expr>,
129    assets_dir: Option<syn::Expr>,
130    app_name: Option<syn::Expr>,
131}
132
133impl RunExample {
134    fn parse(input: parse::ParseStream) -> parse::Result<Self> {
135        let mut index = None;
136        let mut assets_dir = None;
137        let mut app_name = None;
138
139        while !input.is_empty() {
140            let ident: syn::Ident = input.parse()?;
141            let _eq_token: syn::Token![=] = input.parse()?;
142            let expr: syn::Expr = input.parse()?;
143
144            match ident.to_string().as_str() {
145                "index" => index = Some(expr),
146                "assets_dir" => assets_dir = Some(expr),
147                "app_name" => app_name = Some(expr),
148                _ => return Err(parse::Error::new(ident.span(), "unrecognized argument")),
149            }
150
151            let _comma_token: syn::Token![,] = match input.parse() {
152                Ok(x) => x,
153                Err(_) if input.is_empty() => break,
154                Err(err) => return Err(err),
155            };
156        }
157
158        Ok(RunExample {
159            index,
160            assets_dir,
161            app_name,
162        })
163    }
164
165    fn generate(self, item: syn::ItemFn) -> syn::Result<proc_macro2::TokenStream> {
166        let fn_block = item.block;
167
168        let index = if let Some(expr) = &self.index {
169            quote_spanned! { expr.span()=> std::fs::write(dist_dir.join("index.html"), #expr)?; }
170        } else if self.assets_dir.is_some() || self.app_name.is_some() {
171            quote! {}
172        } else {
173            quote! {
174                if let Ok(mut f) = std::fs::OpenOptions::new()
175                    .write(true)
176                    .create_new(true)
177                    .open(dist_dir.join("index.html"))
178                {
179                    std::io::Write::write_all(
180                        &mut f,
181                        b"<!DOCTYPE html><html><head><meta charset=\"utf-8\"/><script type=\"module\">import init from \"./app.js\";init();</script></head><body></body></html>",
182                    )?;
183                }
184            }
185        };
186
187        let app_name = if let Some(expr) = &self.app_name {
188            quote_spanned! { expr.span()=> .app_name(#expr) }
189        } else {
190            quote! {}
191        };
192
193        let assets_dir = if let Some(expr) = self.assets_dir {
194            quote_spanned! { expr.span()=> .assets_dir(#expr) }
195        } else {
196            quote! {}
197        };
198
199        #[cfg(feature = "wasm-opt")]
200        let optimize_wasm = quote! { .optimize_wasm(xtask_wasm::WasmOpt::level(1).shrink(2)) };
201        #[cfg(not(feature = "wasm-opt"))]
202        let optimize_wasm = quote! {};
203
204        Ok(quote! {
205            #[cfg(target_arch = "wasm32")]
206            pub mod xtask_wasm_run_example {
207                use super::*;
208                use xtask_wasm::wasm_bindgen;
209
210                #[xtask_wasm::wasm_bindgen::prelude::wasm_bindgen(start)]
211                pub fn run_app() -> Result<(), xtask_wasm::wasm_bindgen::JsValue> {
212                    xtask_wasm::console_error_panic_hook::set_once();
213
214                    #fn_block
215
216                    Ok(())
217                }
218            }
219
220            #[cfg(not(target_arch = "wasm32"))]
221            fn main() -> xtask_wasm::anyhow::Result<()> {
222                use xtask_wasm::{env_logger, log, clap};
223
224                #[derive(clap::Parser)]
225                struct Cli {
226                    #[clap(subcommand)]
227                    command: Option<Command>,
228                }
229
230                #[derive(clap::Parser)]
231                enum Command {
232                    Dist(xtask_wasm::Dist),
233                    Start(xtask_wasm::DevServer),
234                }
235
236                env_logger::builder()
237                    .filter(Some(module_path!()), log::LevelFilter::Info)
238                    .filter(Some("xtask"), log::LevelFilter::Info)
239                    .init();
240
241                let cli: Cli = clap::Parser::parse();
242
243                match cli.command {
244                    Some(Command::Dist(mut dist)) => {
245                        let dist_dir = dist
246                            .example(module_path!())
247                            #app_name
248                            #assets_dir
249                            #optimize_wasm
250                            .build(env!("CARGO_PKG_NAME"))?;
251
252                        #index
253
254                        Ok(())
255                    }
256                    Some(Command::Start(dev_server)) => {
257                        dev_server.xtask("dist").start()
258                    }
259                    None => {
260                        let dev_server: xtask_wasm::DevServer = clap::Parser::parse();
261                        dev_server.xtask("dist").start()
262                    }
263                }
264            }
265
266            #[cfg(target_arch = "wasm32")]
267            fn main() {}
268        })
269    }
270}