wasm_run/
lib.rs

1//! ![Rust](https://github.com/IMI-eRnD-Be/wasm-run/workflows/main/badge.svg)
2//! [![Latest Version](https://img.shields.io/crates/v/wasm-run.svg)](https://crates.io/crates/wasm-run)
3//! [![Docs.rs](https://docs.rs/wasm-run/badge.svg)](https://docs.rs/wasm-run)
4//! [![LOC](https://tokei.rs/b1/github/IMI-eRnD-Be/wasm-run)](https://github.com/IMI-eRnD-Be/wasm-run)
5//! [![Dependency Status](https://deps.rs/repo/github/IMI-eRnD-Be/wasm-run/status.svg)](https://deps.rs/repo/github/IMI-eRnD-Be/wasm-run)
6//! ![License](https://img.shields.io/crates/l/wasm-run)
7//!
8//! # Synopsis
9//!
10//! Build tool that replaces `cargo run` to build WASM projects. Just like webpack, `wasm-run`
11//! offers a great deal of customization.
12//!
13//! To build your WASM project you normally need an external tool like `wasm-bindgen`, `wasm-pack`
14//! or `cargo-wasm`. `wasm-run` takes a different approach: it's a library that you install as a
15//! dependency to your project. Because of that you don't need any external tool, the
16//! tooling is built as part of your dependencies, which makes the CI easier to set up and reduce
17//! the hassle for new comers to start working on the project.
18//!
19//! To build your project for production you can use the command `cargo run -- build`. You can also
20//! run a development server that rebuilds automatically when the code changes:
21//! `cargo run -- serve`. It doesn't rebuild everything, only the backend if the backend changed or
22//! the frontend if the frontend changed.
23//!
24//! **Please note that there is a space between `--` and `build` and between `--` and `serve`!**
25//!
26//! One of the main advantage of this library is that it provides greater customization: you can
27//! set a few hooks during the build process in order to customize the build directory or use a
28//! template to generate your index.html, download some CSS, ... you name it. I personally use it
29//! to reduce the amount of files by bundling the CSS and the JS into the `index.html` so I had
30//! only two files (`index.html`, `app_bg.wasm`).
31//!
32//! # Examples
33//!
34//! There are 3 basic examples to help you get started quickly:
35//!
36//!  -  a ["frontend-only"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/frontend-only)
37//!     example for a frontend only app that rebuilds the app when a file change is detected;
38//!  -  a ["backend-and-frontend"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/backend-and-frontend)
39//!     example using the web framework Rocket (backend) which uses Rocket itself to serve the file
40//!     during the development (any file change is also detected and it rebuilds and restart
41//!     automatically).
42//!  -  a ["custom-cli-command"](https://github.com/IMI-eRnD-Be/wasm-run/tree/main/examples/custom-cli-command)
43//!     example that adds a custom CLI command named `build-docker-image` which build the backend,
44//!     the frontend and package the whole thing in a container image.
45//!
46//! # Usage
47//!
48//! All the details about the hooks can be found on the macro [`main`].
49//!
50//! # Additional Information
51//!
52//!  *  You can use this library to build examples in the `examples/` directory of your project.
53//!     `cargo run --example your_example -- serve`. But you will need to specify the name of the
54//!     WASM crate in your project and it must be present in the workspace. Please check the
55//!     ["run-an-example"](https://github.com/IMI-eRnD-Be/wasm-run/blob/main/examples/run-an-example.rs)
56//!     example.
57//!  *  If you want to use your own backend you will need to disable the `dev-server` feature
58//!     by disabling the default features. You can use the `full-restart` feature to force the
59//!     backend to also be recompiled when a file changes (otherwise only the frontend is
60//!     re-compiled). You will also need to specify `run_server` to the macro arguments to run your
61//!     backend.
62//!  *  You can add commands to the CLI by adding variants in the `enum`.
63//!  *  You can add parameters to the `Build` and `Serve` commands by overriding them. Please check
64//!     the documentation on the macro `main`.
65//!  *  If you run `cargo run -- serve --profiling`, the WASM will be optimized.
66//!
67//! # Features
68//!
69//!  *  `prebuilt-wasm-opt`: if you disable the default features and enable this feature, a binary
70//!     of wasm-opt will be downloaded from GitHub and used to optimize the WASM. By default,
71//!     wasm-opt is compiled among the dependencies (`binaryen`). This is useful if you run into
72//!     troubles for building `binaryen-sys`. (`binaryen` cannot be built on Netlify at the
73//!     moment.)
74//!  *  `sass`: support for SASS and SCSS. All SASS and SCSS files found in the directories
75//!     `styles/`, `assets/`, `sass/` and `css/` will be automatically transpiled to CSS and placed
76//!     in the build directory. This can be configured by overriding:
77//!     [`BuildArgs::build_sass_from_dir`], [`BuildArgs::sass_lookup_directories`],
78//!     [`BuildArgs::sass_options`] or completely overriden in the [`Hooks::post_build`] hook.
79//!     `sass-rs` is re-exported in the prelude of `wasm-run` for this purpose.
80//!  *  `full-restart`: when this feature is active, the command is entirely restarted when changes
81//!     are detected when serving files for development (`cargo run -- serve`). This is useful with
82//!     custom `serve` command that uses a custom backend and if you need to detect changes in the
83//!     backend code itself.
84
85#![warn(missing_docs)]
86
87#[cfg(feature = "prebuilt-wasm-opt")]
88mod prebuilt_wasm_opt;
89
90use anyhow::{anyhow, bail, Context, Result};
91use cargo_metadata::{Metadata, MetadataCommand, Package};
92use downcast_rs::*;
93use fs_extra::dir;
94use notify::RecommendedWatcher;
95use once_cell::sync::OnceCell;
96use std::collections::{HashMap, HashSet};
97use std::fs;
98use std::io::BufReader;
99use std::iter;
100use std::iter::FromIterator;
101use std::path::{Path, PathBuf};
102#[cfg(feature = "dev-server")]
103use std::pin::Pin;
104use std::process::{Child, ChildStdout, Command, Stdio};
105use std::sync::mpsc;
106use std::time;
107use structopt::StructOpt;
108#[cfg(feature = "dev-server")]
109use tide::Server;
110
111pub use wasm_run_proc_macro::*;
112
113#[doc(hidden)]
114pub use structopt;
115
116const DEFAULT_INDEX: &str = 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>"#;
117
118static METADATA: OnceCell<Metadata> = OnceCell::new();
119static DEFAULT_BUILD_PATH: OnceCell<PathBuf> = OnceCell::new();
120static FRONTEND_PACKAGE: OnceCell<&Package> = OnceCell::new();
121static BACKEND_PACKAGE: OnceCell<Option<&Package>> = OnceCell::new();
122static HOOKS: OnceCell<Hooks> = OnceCell::new();
123
124#[derive(Debug, PartialEq, Clone, Copy)]
125/// A build profile for the WASM.
126pub enum BuildProfile {
127    /// Development profile (no `--release`, no optimization).
128    Dev,
129    /// Release profile (`--profile`, `-O2 -Os`).
130    Release,
131    /// Release profile (`--profile`, `-O2 --debuginfo`).
132    Profiling,
133}
134
135/// This function is called early before any command starts. This is not part of the public API.
136#[doc(hidden)]
137pub fn wasm_run_init(
138    pkg_name: &str,
139    backend_pkg_name: Option<&str>,
140    default_build_path: Option<Box<dyn FnOnce(&Metadata, &Package) -> PathBuf>>,
141    hooks: Hooks,
142) -> Result<(&'static Metadata, &'static Package)> {
143    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
144
145    let metadata = MetadataCommand::new()
146        .exec()
147        .context("this binary is not meant to be ran outside of its workspace")?;
148
149    METADATA
150        .set(metadata)
151        .expect("the cell is initially empty; qed");
152
153    let metadata = METADATA.get().unwrap();
154
155    let frontend_package = METADATA
156        .get()
157        .unwrap()
158        .packages
159        .iter()
160        .find(|x| x.name == pkg_name)
161        .expect("the frontend package existence has been checked during compile time; qed");
162
163    FRONTEND_PACKAGE
164        .set(frontend_package)
165        .expect("the cell is initially empty; qed");
166
167    let frontend_package = FRONTEND_PACKAGE.get().unwrap();
168
169    if let Some(name) = backend_pkg_name {
170        let backend_package = METADATA
171            .get()
172            .unwrap()
173            .packages
174            .iter()
175            .find(|x| x.name == name)
176            .expect("the backend package existence has been checked during compile time; qed");
177
178        BACKEND_PACKAGE
179            .set(Some(backend_package))
180            .expect("the cell is initially empty; qed");
181    } else {
182        BACKEND_PACKAGE
183            .set(None)
184            .expect("the cell is initially empty; qed");
185    }
186
187    DEFAULT_BUILD_PATH
188        .set(if let Some(default_build_path) = default_build_path {
189            default_build_path(metadata, frontend_package)
190        } else {
191            metadata.workspace_root.join("build")
192        })
193        .expect("the cell is initially empty; qed");
194
195    if HOOKS.set(hooks).is_err() {
196        panic!("the cell is initially empty; qed");
197    }
198
199    Ok((metadata, frontend_package))
200}
201
202/// Build arguments.
203#[derive(StructOpt, Debug)]
204pub struct DefaultBuildArgs {
205    /// Build directory output.
206    #[structopt(long)]
207    pub build_path: Option<PathBuf>,
208
209    /// Create a profiling build. Enable optimizations and debug info.
210    #[structopt(long)]
211    pub profiling: bool,
212}
213
214/// A trait that allows overriding the `build` command.
215pub trait BuildArgs: Downcast {
216    /// Build directory output.
217    fn build_path(&self) -> &PathBuf;
218
219    /// Default path for the build/public directory.
220    fn default_build_path(&self) -> &PathBuf {
221        DEFAULT_BUILD_PATH
222            .get()
223            .expect("default_build_path has been initialized on startup; qed")
224    }
225
226    /// Path to the `target` directory.
227    fn target_path(&self) -> &PathBuf {
228        &self.metadata().target_directory
229    }
230
231    /// Metadata of the project.
232    fn metadata(&self) -> &Metadata {
233        METADATA
234            .get()
235            .expect("metadata has been initialized on startup; qed")
236    }
237
238    /// Package metadata.
239    fn frontend_package(&self) -> &Package {
240        FRONTEND_PACKAGE
241            .get()
242            .expect("frontend_package has been initialized on startup; qed")
243    }
244
245    /// Backend frontend_package metadata.
246    fn backend_package(&self) -> Option<&Package> {
247        BACKEND_PACKAGE
248            .get()
249            .expect("frontend_package has been initialized on startup; qed")
250            .to_owned()
251    }
252
253    /// Create a profiling build. Enable optimizations and debug info.
254    fn profiling(&self) -> bool;
255
256    /// Transpile SASS and SCSS files to CSS in the build directory.
257    #[cfg(feature = "sass")]
258    fn build_sass_from_dir(
259        &self,
260        input_dir: &std::path::Path,
261        options: sass_rs::Options,
262    ) -> Result<()> {
263        use walkdir::{DirEntry, WalkDir};
264
265        let build_path = self.build_path();
266
267        fn is_sass(entry: &DirEntry) -> bool {
268            matches!(
269                entry.path().extension().map(|x| x.to_str()).flatten(),
270                Some("sass") | Some("scss")
271            )
272        }
273
274        fn should_ignore(entry: &DirEntry) -> bool {
275            entry
276                .file_name()
277                .to_str()
278                .map(|x| x.starts_with("_"))
279                .unwrap_or(false)
280        }
281
282        log::info!("Building SASS from {:?}", input_dir);
283
284        let walker = WalkDir::new(&input_dir).into_iter();
285        for entry in walker
286            .filter_map(|x| match x {
287                Ok(x) => Some(x),
288                Err(err) => {
289                    log::warn!(
290                        "Could not walk into directory `{}`: {}",
291                        input_dir.display(),
292                        err,
293                    );
294                    None
295                }
296            })
297            .filter(|x| x.path().is_file() && is_sass(x) && !should_ignore(x))
298        {
299            let file_path = entry.path();
300            let css_path = build_path
301                .join(file_path.strip_prefix(&input_dir).unwrap())
302                .with_extension("css");
303
304            match sass_rs::compile_file(file_path, options.clone()) {
305                Ok(css) => {
306                    let _ = fs::create_dir_all(css_path.parent().unwrap());
307                    fs::write(&css_path, css).with_context(|| {
308                        format!("could not write CSS to file `{}`", css_path.display())
309                    })?;
310                }
311                Err(err) => bail!(
312                    "could not convert SASS file `{}` to `{}`: {}",
313                    file_path.display(),
314                    css_path.display(),
315                    err,
316                ),
317            }
318        }
319
320        Ok(())
321    }
322
323    /// Returns a list of directories to lookup to transpile SASS and SCSS files to CSS.
324    #[cfg(feature = "sass")]
325    fn sass_lookup_directories(&self, _profile: BuildProfile) -> Vec<PathBuf> {
326        const STYLE_CANDIDATES: &[&str] = &["assets", "styles", "css", "sass"];
327
328        let package_path = self.frontend_package().manifest_path.parent().unwrap();
329
330        STYLE_CANDIDATES
331            .iter()
332            .map(|x| package_path.join(x))
333            .filter(|x| x.exists())
334            .collect()
335    }
336
337    /// Default profile to transpile SASS and SCSS files to CSS.
338    #[cfg(feature = "sass")]
339    fn sass_options(&self, profile: BuildProfile) -> sass_rs::Options {
340        sass_rs::Options {
341            output_style: match profile {
342                BuildProfile::Release | BuildProfile::Profiling => sass_rs::OutputStyle::Compressed,
343                _ => sass_rs::OutputStyle::Nested,
344            },
345            ..sass_rs::Options::default()
346        }
347    }
348
349    /// Run the `build` command.
350    fn run(self) -> Result<PathBuf>
351    where
352        Self: Sized + 'static,
353    {
354        let hooks = HOOKS.get().expect("wasm_run_init() has not been called");
355        build(BuildProfile::Release, &self, hooks)?;
356        Ok(self.build_path().to_owned())
357    }
358}
359
360impl_downcast!(BuildArgs);
361
362impl BuildArgs for DefaultBuildArgs {
363    fn build_path(&self) -> &PathBuf {
364        self.build_path
365            .as_ref()
366            .unwrap_or_else(|| self.default_build_path())
367    }
368
369    fn profiling(&self) -> bool {
370        self.profiling
371    }
372}
373
374/// Serve arguments.
375#[derive(StructOpt, Debug)]
376pub struct DefaultServeArgs {
377    /// Activate HTTP logs.
378    #[structopt(long)]
379    pub log: bool,
380
381    /// IP address to bind.
382    ///
383    /// Use 0.0.0.0 to expose the server to your network.
384    #[structopt(long, short = "h", default_value = "127.0.0.1")]
385    pub ip: String,
386
387    /// Port number.
388    #[structopt(long, short = "p", default_value = "3000")]
389    pub port: u16,
390
391    /// Build arguments.
392    #[structopt(flatten)]
393    pub build_args: DefaultBuildArgs,
394}
395
396/// A trait that allows overriding the `serve` command.
397pub trait ServeArgs: Downcast + Send {
398    /// Activate HTTP logs.
399    #[cfg(feature = "dev-server")]
400    fn log(&self) -> bool;
401
402    /// IP address to bind.
403    ///
404    /// Use 0.0.0.0 to expose the server to your network.
405    #[cfg(feature = "dev-server")]
406    fn ip(&self) -> &str;
407
408    /// Port number.
409    #[cfg(feature = "dev-server")]
410    fn port(&self) -> u16;
411
412    /// Build arguments.
413    fn build_args(&self) -> &dyn BuildArgs;
414
415    /// Run the `serve` command.
416    fn run(self) -> Result<()>
417    where
418        Self: Sync + Sized + 'static,
419    {
420        let hooks = HOOKS.get().expect("wasm_run_init() has not been called");
421        // NOTE: the first step for serving is to call `build` a first time. The build directory
422        //       must be present before we start watching files there.
423        build(BuildProfile::Dev, self.build_args(), hooks)?;
424        #[cfg(feature = "dev-server")]
425        {
426            async_std::task::block_on(async {
427                let t1 = async_std::task::spawn(serve_frontend(&self, hooks)?);
428                let t2 = async_std::task::spawn_blocking(move || watch_frontend(&self, hooks));
429                futures::try_join!(t1, t2)?;
430                Err(anyhow!("server and watcher unexpectedly exited"))
431            })
432        }
433        #[cfg(not(feature = "dev-server"))]
434        {
435            use std::sync::Arc;
436            use std::thread;
437
438            if self.build_args().backend_package().is_none() {
439                bail!("missing backend crate name");
440            }
441
442            let args = Arc::new(self);
443            let t1 = {
444                let args = Arc::clone(&args);
445                thread::spawn(move || watch_frontend(&*args, hooks))
446            };
447            let t2 = thread::spawn(move || watch_backend(&*args, hooks));
448            let _ = t1.join();
449            let _ = t2.join();
450
451            Err(anyhow!("server and watcher unexpectedly exited"))
452        }
453    }
454}
455
456impl_downcast!(ServeArgs);
457
458impl ServeArgs for DefaultServeArgs {
459    #[cfg(feature = "dev-server")]
460    fn log(&self) -> bool {
461        self.log
462    }
463
464    #[cfg(feature = "dev-server")]
465    fn ip(&self) -> &str {
466        &self.ip
467    }
468
469    #[cfg(feature = "dev-server")]
470    fn port(&self) -> u16 {
471        self.port
472    }
473
474    fn build_args(&self) -> &dyn BuildArgs {
475        &self.build_args
476    }
477}
478
479/// Hooks.
480///
481/// Check the code of [`Hooks::default()`] implementation to see what they do by default.
482///
483/// If you don't provide your own hook, the default code will be executed. But if you do provide a
484/// hook, the code will be *replaced*.
485pub struct Hooks {
486    /// This hook will be run before the WASM is compiled. It does nothing by default.
487    /// You can tweak the command-line arguments of the build command here or create additional
488    /// files in the build directory.
489    pub pre_build:
490        Box<dyn Fn(&dyn BuildArgs, BuildProfile, &mut Command) -> Result<()> + Send + Sync>,
491
492    /// This hook will be run after the WASM is compiled and optimized.
493    /// By default it copies the static files to the build directory.
494    #[allow(clippy::type_complexity)]
495    pub post_build:
496        Box<dyn Fn(&dyn BuildArgs, BuildProfile, String, Vec<u8>) -> Result<()> + Send + Sync>,
497
498    /// This hook will be run before running the HTTP server.
499    /// By default it will add routes to the files in the build directory.
500    #[cfg(feature = "dev-server")]
501    #[allow(clippy::type_complexity)]
502    pub serve: Box<dyn Fn(&dyn ServeArgs, &mut Server<()>) -> Result<()> + Send + Sync>,
503
504    /// This hook will be run before starting to watch for changes in files.
505    /// By default it will add all the `src/` directories and `Cargo.toml` files of all the crates
506    /// in the workspace plus the `static/` directory if it exists in the frontend crate.
507    pub frontend_watch:
508        Box<dyn Fn(&dyn ServeArgs, &mut RecommendedWatcher) -> Result<()> + Send + Sync>,
509
510    /// This hook will be run before starting to watch for changes in files.
511    /// By default it will add the backend crate directory and all its dependencies. But it
512    /// excludes the target directory.
513    pub backend_watch:
514        Box<dyn Fn(&dyn ServeArgs, &mut RecommendedWatcher) -> Result<()> + Send + Sync>,
515
516    /// This hook will be run before (re-)starting the backend.
517    /// You can tweak the cargo command that is run here: adding/removing environment variables or
518    /// adding arguments.
519    /// By default it will do `cargo run -p <backend_crate>`.
520    pub backend_command: Box<dyn Fn(&dyn ServeArgs, &mut Command) -> Result<()> + Send + Sync>,
521}
522
523impl Default for Hooks {
524    fn default() -> Self {
525        Self {
526            backend_command: Box::new(|args, command| {
527                command.args(&[
528                    "run",
529                    "-p",
530                    &args
531                        .build_args()
532                        .backend_package()
533                        .context("missing backend crate name")?
534                        .name,
535                ]);
536                Ok(())
537            }),
538            backend_watch: Box::new(|args, watcher| {
539                use notify::{RecursiveMode, Watcher};
540
541                let metadata = args.build_args().metadata();
542                let backend = args
543                    .build_args()
544                    .backend_package()
545                    .context("missing backend crate name")?;
546                let packages: HashMap<_, _> = metadata
547                    .packages
548                    .iter()
549                    .map(|x| (x.name.as_str(), x))
550                    .collect();
551                let members: HashSet<_> = HashSet::from_iter(&metadata.workspace_members);
552
553                backend
554                    .dependencies
555                    .iter()
556                    .map(|x| packages.get(x.name.as_str()).unwrap())
557                    .filter(|x| members.contains(&x.id))
558                    .map(|x| x.manifest_path.parent().unwrap())
559                    .chain(iter::once(backend.manifest_path.parent().unwrap()))
560                    .try_for_each(|x| watcher.watch(x, RecursiveMode::Recursive))?;
561
562                Ok(())
563            }),
564            frontend_watch: Box::new(|args, watcher| {
565                use notify::{RecursiveMode, Watcher};
566
567                let metadata = args.build_args().metadata();
568                let frontend = args.build_args().frontend_package();
569                let packages: HashMap<_, _> = metadata
570                    .packages
571                    .iter()
572                    .map(|x| (x.name.as_str(), x))
573                    .collect();
574                let members: HashSet<_> = HashSet::from_iter(&metadata.workspace_members);
575
576                frontend
577                    .dependencies
578                    .iter()
579                    .filter_map(|x| packages.get(x.name.as_str()))
580                    .filter(|x| members.contains(&x.id))
581                    .map(|x| x.manifest_path.parent().unwrap())
582                    .chain(iter::once(frontend.manifest_path.parent().unwrap()))
583                    .try_for_each(|x| watcher.watch(x, RecursiveMode::Recursive))?;
584
585                Ok(())
586            }),
587            pre_build: Box::new(|_, _, _| Ok(())),
588            post_build: Box::new(
589                |args, #[allow(unused_variables)] profile, wasm_js, wasm_bin| {
590                    let build_path = args.build_path();
591                    let wasm_js_path = build_path.join("app.js");
592                    let wasm_bin_path = build_path.join("app_bg.wasm");
593
594                    fs::write(&wasm_js_path, wasm_js).with_context(|| {
595                        format!("could not write JS file to `{}`", wasm_js_path.display())
596                    })?;
597                    fs::write(&wasm_bin_path, wasm_bin).with_context(|| {
598                        format!("could not write WASM file to `{}`", wasm_bin_path.display())
599                    })?;
600
601                    let index_path = build_path.join("index.html");
602                    let static_dir = args
603                        .frontend_package()
604                        .manifest_path
605                        .parent()
606                        .unwrap()
607                        .join("static");
608
609                    if index_path.exists() {
610                        fs::copy("index.html", &index_path).context(format!(
611                            "could not copy index.html to `{}`",
612                            index_path.display()
613                        ))?;
614                    } else if static_dir.exists() {
615                        dir::copy(
616                            &static_dir,
617                            &build_path,
618                            &dir::CopyOptions {
619                                content_only: true,
620                                ..dir::CopyOptions::new()
621                            },
622                        )
623                        .with_context(|| {
624                            format!(
625                                "could not copy content of directory static: `{}` to `{}`",
626                                static_dir.display(),
627                                build_path.display()
628                            )
629                        })?;
630                    } else {
631                        fs::write(&index_path, DEFAULT_INDEX).with_context(|| {
632                            format!(
633                                "could not write default index.html to `{}`",
634                                index_path.display()
635                            )
636                        })?;
637                    }
638
639                    #[cfg(feature = "sass")]
640                    {
641                        let options = args.sass_options(profile);
642                        for style_path in args.sass_lookup_directories(profile) {
643                            args.build_sass_from_dir(&style_path, options.clone())?;
644                        }
645                    }
646
647                    Ok(())
648                },
649            ),
650            #[cfg(feature = "dev-server")]
651            serve: Box::new(|args, server| {
652                use tide::{Body, Request, Response};
653
654                let build_path = args.build_args().build_path().to_owned();
655                let index_path = build_path.join("index.html");
656
657                server.at("/").serve_dir(args.build_args().build_path())?;
658                server.at("/").get(move |_| {
659                    let index_path = index_path.clone();
660                    async move { Ok(Response::from(Body::from_file(index_path).await?)) }
661                });
662                server.at("/*path").get(move |req: Request<()>| {
663                    let build_path = build_path.clone();
664                    async move {
665                        match Body::from_file(build_path.join(req.param("path").unwrap())).await {
666                            Ok(body) => Ok(Response::from(body)),
667                            Err(_) => Ok(Response::from(
668                                Body::from_file(build_path.join("index.html")).await?,
669                            )),
670                        }
671                    }
672                });
673
674                Ok(())
675            }),
676        }
677    }
678}
679
680fn build(mut profile: BuildProfile, args: &dyn BuildArgs, hooks: &Hooks) -> Result<()> {
681    use wasm_bindgen_cli_support::Bindgen;
682
683    if args.profiling() {
684        profile = BuildProfile::Profiling;
685    }
686
687    let frontend_package = args.frontend_package();
688
689    let build_path = args.build_path();
690    let _ = fs::remove_dir_all(build_path);
691    fs::create_dir_all(build_path).with_context(|| {
692        format!(
693            "could not create build directory `{}`",
694            build_path.display()
695        )
696    })?;
697
698    let mut command = Command::new("cargo");
699
700    command
701        .args(&[
702            "build",
703            "--lib",
704            "--target",
705            "wasm32-unknown-unknown",
706            "--manifest-path",
707        ])
708        .arg(&frontend_package.manifest_path)
709        .args(match profile {
710            BuildProfile::Profiling => &["--release"] as &[&str],
711            BuildProfile::Release => &["--release"],
712            BuildProfile::Dev => &[],
713        });
714
715    log::info!("Running pre-build hook");
716    (hooks.pre_build)(args, profile, &mut command)?;
717
718    log::info!("Building frontend");
719    let status = command.status().context("could not start build process")?;
720
721    if !status.success() {
722        if let Some(code) = status.code() {
723            bail!("build process exit with code {}", code);
724        } else {
725            bail!("build process has been terminated by a signal");
726        }
727    }
728
729    let wasm_path = args
730        .target_path()
731        .join("wasm32-unknown-unknown")
732        .join(match profile {
733            BuildProfile::Profiling => "release",
734            BuildProfile::Release => "release",
735            BuildProfile::Dev => "debug",
736        })
737        .join(frontend_package.name.replace("-", "_"))
738        .with_extension("wasm");
739
740    let mut output = Bindgen::new()
741        .input_path(wasm_path)
742        .out_name("app")
743        .web(true)
744        .expect("fails only if multiple modes specified; qed")
745        .debug(!matches!(profile, BuildProfile::Release))
746        .generate_output()
747        .context("could not generate WASM bindgen file")?;
748
749    let wasm_js = output.js().to_owned();
750    let wasm_bin = output.wasm_mut().emit_wasm();
751
752    let wasm_bin = match profile {
753        BuildProfile::Profiling => wasm_opt(wasm_bin, 0, 2, true, args.target_path())?,
754        BuildProfile::Release => wasm_opt(wasm_bin, 1, 2, false, args.target_path())?,
755        BuildProfile::Dev => wasm_bin,
756    };
757
758    log::info!("Running post-build hook");
759    (hooks.post_build)(args, profile, wasm_js, wasm_bin)?;
760
761    Ok(())
762}
763
764#[cfg(feature = "dev-server")]
765fn serve_frontend(
766    args: &dyn ServeArgs,
767    hooks: &Hooks,
768) -> Result<Pin<Box<impl std::future::Future<Output = Result<()>> + Send + 'static>>> {
769    use futures::TryFutureExt;
770
771    if args.log() {
772        tide::log::start();
773    }
774    let mut app = tide::new();
775
776    (hooks.serve)(args, &mut app)?;
777
778    log::info!(
779        "Development server started: http://{}:{}",
780        args.ip(),
781        args.port()
782    );
783
784    Ok(Box::pin(
785        app.listen(format!("{}:{}", args.ip(), args.port()))
786            .map_err(Into::into),
787    ))
788}
789
790#[cfg(not(feature = "dev-server"))]
791fn watch_backend(args: &dyn ServeArgs, hooks: &Hooks) -> Result<()> {
792    let (tx, rx) = mpsc::channel();
793
794    let mut watcher: RecommendedWatcher = notify::Watcher::new(tx, time::Duration::from_secs(2))
795        .context("could not initialize watcher")?;
796
797    (hooks.backend_watch)(args, &mut watcher)?;
798
799    struct BackgroundProcess(std::process::Child);
800
801    impl Drop for BackgroundProcess {
802        fn drop(&mut self) {
803            // TODO: cleaner exit on Unix
804            let _ = self.0.kill();
805            let _ = self.0.wait();
806        }
807    }
808
809    let run_server = || -> Result<BackgroundProcess> {
810        let mut command = Command::new("cargo");
811        (hooks.backend_command)(args, &mut command)?;
812        Ok(command.spawn().map(BackgroundProcess)?)
813    };
814
815    let mut process_guard = Some(run_server()?);
816
817    watch_loop(args, rx, || {
818        drop(process_guard.take());
819        process_guard.replace(run_server()?);
820        Ok(())
821    });
822}
823
824fn watch_frontend(args: &dyn ServeArgs, hooks: &Hooks) -> Result<()> {
825    let (tx, rx) = mpsc::channel();
826
827    let mut watcher: RecommendedWatcher = notify::Watcher::new(tx, time::Duration::from_secs(2))
828        .context("could not initialize watcher")?;
829
830    (hooks.frontend_watch)(args, &mut watcher)?;
831
832    let build_args = args.build_args();
833
834    watch_loop(args, rx, || build(BuildProfile::Dev, build_args, hooks));
835}
836
837fn watch_loop(
838    args: &dyn ServeArgs,
839    rx: mpsc::Receiver<notify::DebouncedEvent>,
840    mut callback: impl FnMut() -> Result<()>,
841) -> ! {
842    loop {
843        use notify::DebouncedEvent::*;
844
845        let message = rx.recv();
846        match &message {
847            Ok(Create(path)) | Ok(Write(path)) | Ok(Remove(path)) | Ok(Rename(_, path))
848                if !path.starts_with(args.build_args().build_path())
849                    && !path.starts_with(args.build_args().target_path())
850                    && !path
851                        .file_name()
852                        .and_then(|x| x.to_str())
853                        .map(|x| x.starts_with('.'))
854                        .unwrap_or(false) =>
855            {
856                if let Err(err) = callback() {
857                    log::error!("{}", err);
858                }
859            }
860            Ok(_) => {}
861            Err(e) => log::error!("Watch error: {}", e),
862        }
863    }
864}
865
866#[allow(unused_variables, unreachable_code)]
867fn wasm_opt(
868    binary: Vec<u8>,
869    shrink_level: u32,
870    optimization_level: u32,
871    debug_info: bool,
872    target_path: impl AsRef<Path>,
873) -> Result<Vec<u8>> {
874    #[cfg(feature = "binaryen")]
875    return match binaryen::Module::read(&binary) {
876        Ok(mut module) => {
877            module.optimize(&binaryen::CodegenConfig {
878                shrink_level,
879                optimization_level,
880                debug_info,
881            });
882            Ok(module.write())
883        }
884        Err(()) => bail!("could not load WASM module"),
885    };
886
887    #[cfg(feature = "prebuilt-wasm-opt")]
888    return {
889        let wasm_opt = prebuilt_wasm_opt::install_wasm_opt(target_path)?;
890
891        let mut command = Command::new(&wasm_opt);
892        command
893            .stderr(Stdio::inherit())
894            .args(&["-o", "-", "-O"])
895            .args(&["-ol", &optimization_level.to_string()])
896            .args(&["-s", &shrink_level.to_string()]);
897        if debug_info {
898            command.arg("-g");
899        }
900
901        #[cfg(target_os = "macos")]
902        {
903            command.env("DYLD_LIBRARY_PATH", wasm_opt.parent().unwrap());
904        }
905
906        #[cfg(windows)]
907        let delete_guard = {
908            use std::io::Write;
909
910            let tmp = tempfile::NamedTempFile::new()?;
911            tmp.as_file().write_all(&binary)?;
912            command.arg(tmp.path());
913            tmp
914        };
915
916        #[cfg(unix)]
917        {
918            use std::io::{Seek, SeekFrom, Write};
919
920            let mut file = tempfile::tempfile()?;
921            file.write_all(&binary)?;
922            file.seek(SeekFrom::Start(0))?;
923            command.stdin(file);
924        }
925
926        let output = command.output()?;
927        if !output.status.success() {
928            bail!("command `wasm-opt` failed.");
929        }
930        Ok(output.stdout)
931    };
932
933    log::warn!("No optimization has been done on the WASM");
934    Ok(binary)
935}
936
937/// An extension for [`Package`] and for [`Metadata`] to run a cargo command a bit more easily.
938/// Ideal for scripting.
939pub trait PackageExt {
940    /// Run the cargo command in the package's directory if ran on a [`Package`] or in the
941    /// workspace root if ran on a [`Metadata`].
942    fn cargo(&self, builder: impl FnOnce(&mut Command)) -> Result<CargoChild>;
943}
944
945impl PackageExt for Package {
946    fn cargo(&self, builder: impl FnOnce(&mut Command)) -> Result<CargoChild> {
947        let mut command = Command::new("cargo");
948        command
949            .current_dir(self.manifest_path.parent().unwrap())
950            .stdout(Stdio::piped());
951
952        builder(&mut command);
953
954        Ok(CargoChild(command.spawn()?))
955    }
956}
957
958impl PackageExt for Metadata {
959    fn cargo(&self, builder: impl FnOnce(&mut Command)) -> Result<CargoChild> {
960        let mut command = Command::new("cargo");
961        command
962            .current_dir(&self.workspace_root)
963            .stdout(Stdio::piped());
964
965        builder(&mut command);
966
967        Ok(CargoChild(command.spawn()?))
968    }
969}
970
971/// A cargo child process.
972///
973/// The child process is killed and waited if the instance is dropped.
974pub struct CargoChild(Child);
975
976impl CargoChild {
977    /// Wait for the child process to finish and return an `Err(_)` if it didn't ended
978    /// successfully.
979    pub fn wait_success(&mut self) -> Result<()> {
980        let status = self.0.wait()?;
981
982        if let Some(code) = status.code() {
983            if !status.success() {
984                bail!("cargo exited with status: {}", code)
985            }
986        }
987
988        if !status.success() {
989            bail!("cargo exited with error")
990        }
991
992        Ok(())
993    }
994
995    /// Creates an iterator of Message from a Read outputting a stream of JSON messages. For usage
996    /// information, look at the top-level documentation of [`cargo_metadata`].
997    pub fn iter(&mut self) -> cargo_metadata::MessageIter<BufReader<ChildStdout>> {
998        let reader = BufReader::new(self.0.stdout.take().unwrap());
999        cargo_metadata::Message::parse_stream(reader)
1000    }
1001}
1002
1003impl Drop for CargoChild {
1004    fn drop(&mut self) {
1005        let _ = self.0.kill();
1006        let _ = self.0.wait();
1007    }
1008}
1009
1010/// The wasm-run Prelude
1011///
1012/// The purpose of this module is to alleviate imports of many common types:
1013///
1014/// ```
1015/// # #![allow(unused_imports)]
1016/// use wasm_run::prelude::*;
1017/// ```
1018pub mod prelude {
1019    pub use wasm_run_proc_macro::*;
1020
1021    pub use anyhow;
1022    #[cfg(feature = "dev-server")]
1023    pub use async_std;
1024    pub use cargo_metadata;
1025    pub use cargo_metadata::{Message, Metadata, Package};
1026    pub use fs_extra;
1027    #[cfg(feature = "dev-server")]
1028    pub use futures;
1029    pub use notify;
1030    pub use notify::RecommendedWatcher;
1031    #[cfg(feature = "sass")]
1032    pub use sass_rs;
1033    #[cfg(feature = "dev-server")]
1034    pub use tide;
1035    #[cfg(feature = "dev-server")]
1036    pub use tide::Server;
1037
1038    pub use super::{
1039        BuildArgs, BuildProfile, CargoChild, DefaultBuildArgs, DefaultServeArgs, Hooks, PackageExt,
1040        ServeArgs,
1041    };
1042}