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/// # 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/// * `assets_dir` - Path to a custom assets 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    assets_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 assets_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                "assets_dir" => assets_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            assets_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.assets_dir.is_some() || self.app_name.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 assets_dir = if let Some(expr) = self.assets_dir {
121            quote_spanned! { expr.span()=> .assets_dir(#expr) }
122        } else {
123            quote! {}
124        };
125
126        #[cfg(feature = "wasm-opt")]
127        let optimize_wasm = quote! { .optimize_wasm(xtask_wasm::WasmOpt::level(1).shrink(2)) };
128        #[cfg(not(feature = "wasm-opt"))]
129        let optimize_wasm = quote! {};
130
131        Ok(quote! {
132            #[cfg(target_arch = "wasm32")]
133            pub mod xtask_wasm_run_example {
134                use super::*;
135                use xtask_wasm::wasm_bindgen;
136
137                #[xtask_wasm::wasm_bindgen::prelude::wasm_bindgen(start)]
138                pub fn run_app() -> Result<(), xtask_wasm::wasm_bindgen::JsValue> {
139                    xtask_wasm::console_error_panic_hook::set_once();
140
141                    #fn_block
142
143                    Ok(())
144                }
145            }
146
147            #[cfg(not(target_arch = "wasm32"))]
148            fn main() -> xtask_wasm::anyhow::Result<()> {
149                use xtask_wasm::{env_logger, log, clap};
150
151                #[derive(clap::Parser)]
152                struct Cli {
153                    #[clap(subcommand)]
154                    command: Option<Command>,
155                }
156
157                #[derive(clap::Parser)]
158                enum Command {
159                    Dist(xtask_wasm::Dist),
160                    Start(xtask_wasm::DevServer),
161                }
162
163                env_logger::builder()
164                    .filter(Some(module_path!()), log::LevelFilter::Info)
165                    .filter(Some("xtask"), log::LevelFilter::Info)
166                    .init();
167
168                let cli: Cli = clap::Parser::parse();
169
170                match cli.command {
171                    Some(Command::Dist(mut dist)) => {
172                        let dist_dir = dist
173                            .example(module_path!())
174                            #app_name
175                            #assets_dir
176                            #optimize_wasm
177                            .build(env!("CARGO_PKG_NAME"))?;
178
179                        #index
180
181                        Ok(())
182                    }
183                    Some(Command::Start(dev_server)) => {
184                        dev_server.xtask("dist").start()
185                    }
186                    None => {
187                        let dev_server: xtask_wasm::DevServer = clap::Parser::parse();
188                        dev_server.xtask("dist").start()
189                    }
190                }
191            }
192
193            #[cfg(target_arch = "wasm32")]
194            fn main() {}
195        })
196    }
197}