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/// # Usage
9///
10/// * In the file `examples/my_example.rs`, create your example:
11///
12///   ```rust,ignore
13///   use wasm_bindgen::prelude::*;
14///
15///   #[wasm_bindgen]
16///   extern "C" {
17///       #[wasm_bindgen(js_namespace = console)]
18///       fn log(message: &str);
19///   }
20///
21///   #[xtask_wasm::run_example]
22///   fn run_app() {
23///       log::("Hello World!");
24///   }
25///   ```
26///
27/// * In the project's `Cargo.toml`:
28///
29///   ```toml
30///   [dev-dependencies]
31///   xtask-wasm = { version = "*", features = ["run-example"] }
32///   ```
33///
34/// * Then to run the development server with the example:
35///
36///     ```console
37///     cargo run --example my_example
38///     ```
39///
40/// ## Arguments
41///
42/// You can give arguments to the macro to customize the example:
43///
44/// * `app_name` - Change the app name.
45/// * `index` - Content of a custom `index.html`.
46/// * `static_dir` - Path to a custom static directory.
47#[proc_macro_attribute]
48pub fn run_example(
49    attr: proc_macro::TokenStream,
50    item: proc_macro::TokenStream,
51) -> proc_macro::TokenStream {
52    let item = parse_macro_input!(item as syn::ItemFn);
53    let attr = parse_macro_input!(attr with RunExample::parse);
54
55    attr.generate(item)
56        .unwrap_or_else(|err| err.to_compile_error())
57        .into()
58}
59
60struct RunExample {
61    index: Option<syn::Expr>,
62    static_dir: Option<syn::Expr>,
63    app_name: Option<syn::Expr>,
64}
65
66impl RunExample {
67    fn parse(input: parse::ParseStream) -> parse::Result<Self> {
68        let mut index = None;
69        let mut static_dir = None;
70        let mut app_name = None;
71
72        while !input.is_empty() {
73            let ident: syn::Ident = input.parse()?;
74            let _eq_token: syn::Token![=] = input.parse()?;
75            let expr: syn::Expr = input.parse()?;
76
77            match ident.to_string().as_str() {
78                "index" => index = Some(expr),
79                "static_dir" => static_dir = Some(expr),
80                "app_name" => app_name = Some(expr),
81                _ => return Err(parse::Error::new(ident.span(), "unrecognized argument")),
82            }
83
84            let _comma_token: syn::Token![,] = match input.parse() {
85                Ok(x) => x,
86                Err(_) if input.is_empty() => break,
87                Err(err) => return Err(err),
88            };
89        }
90
91        Ok(RunExample {
92            index,
93            static_dir,
94            app_name,
95        })
96    }
97
98    fn generate(self, item: syn::ItemFn) -> syn::Result<proc_macro2::TokenStream> {
99        let fn_block = item.block;
100
101        let index = if let Some(expr) = &self.index {
102            quote_spanned! { expr.span()=> std::fs::write(dist_dir.join("index.html"), #expr)?; }
103        } else if self.static_dir.is_some() {
104            quote! {}
105        } else {
106            quote! {
107                std::fs::write(
108                    dist_dir.join("index.html"),
109                    r#"<!DOCTYPE html><html><head><meta charset="utf-8"/><script type="module">import init from "/app.js";init(new URL('app_bg.wasm', import.meta.url));</script></head><body></body></html>"#,
110                )?;
111            }
112        };
113
114        let app_name = if let Some(expr) = &self.app_name {
115            quote_spanned! { expr.span()=> .app_name(#expr) }
116        } else {
117            quote! {}
118        };
119
120        let static_dir = if let Some(expr) = self.static_dir {
121            quote_spanned! { expr.span()=> .static_dir_path(#expr) }
122        } else {
123            quote! {}
124        };
125
126        Ok(quote! {
127            #[cfg(target_arch = "wasm32")]
128            pub mod xtask_wasm_run_example {
129                use super::*;
130                use xtask_wasm::wasm_bindgen;
131
132                #[xtask_wasm::wasm_bindgen::prelude::wasm_bindgen(start)]
133                pub fn run_app() -> Result<(), xtask_wasm::wasm_bindgen::JsValue> {
134                    xtask_wasm::console_error_panic_hook::set_once();
135
136                    #fn_block
137
138                    Ok(())
139                }
140            }
141
142            #[cfg(not(target_arch = "wasm32"))]
143            fn main() -> xtask_wasm::anyhow::Result<()> {
144                use xtask_wasm::{env_logger, log, clap};
145
146                #[derive(clap::Parser)]
147                struct Cli {
148                    #[clap(subcommand)]
149                    command: Option<Command>,
150                }
151
152                #[derive(clap::Parser)]
153                enum Command {
154                    Dist(xtask_wasm::Dist),
155                    Start(xtask_wasm::DevServer),
156                }
157
158                env_logger::builder()
159                    .filter(Some(module_path!()), log::LevelFilter::Info)
160                    .filter(Some("xtask"), log::LevelFilter::Info)
161                    .init();
162
163                let cli: Cli = clap::Parser::parse();
164                let mut dist_command = xtask_wasm::xtask_command();
165                dist_command.arg("dist");
166
167                match cli.command {
168                    Some(Command::Dist(mut dist)) => {
169                        let dist_dir = dist
170                            .example(module_path!())
171                            #app_name
172                            #static_dir
173                            .run(env!("CARGO_PKG_NAME"))?;
174
175                        #index
176
177                        Ok(())
178                    }
179                    Some(Command::Start(dev_server)) => {
180                        let served_path = xtask_wasm::default_dist_dir(false);
181                        dev_server.command(dist_command).start(served_path)
182                    }
183                    None => {
184                        let dev_server: xtask_wasm::DevServer = clap::Parser::parse();
185                        let served_path = xtask_wasm::default_dist_dir(false);
186                        dev_server.command(dist_command).start(served_path)
187                    }
188                }
189            }
190
191            #[cfg(target_arch = "wasm32")]
192            fn main() {}
193        })
194    }
195}