//! Implementation of the `wasm-pack build` command. use crate::bindgen; use crate::build; use crate::cache; use crate::command::utils::{create_pkg_dir, get_crate_path}; use crate::emoji; use crate::install::{self, InstallMode, Tool}; use crate::license; use crate::lockfile::Lockfile; use crate::manifest; use crate::readme; use crate::wasm_opt; use crate::PBAR; use anyhow::{anyhow, bail, Error, Result}; use binary_install::Cache; use clap::Args; use log::info; use path_clean::PathClean; use std::fmt; use std::path::PathBuf; use std::str::FromStr; use std::time::Instant; /// Everything required to configure and run the `wasm-pack build` command. #[allow(missing_docs)] pub struct Build { pub crate_path: PathBuf, pub crate_data: manifest::CrateData, pub scope: Option, pub disable_dts: bool, pub weak_refs: bool, pub reference_types: bool, pub target: Target, pub no_pack: bool, pub no_opt: bool, pub profile: BuildProfile, pub mode: InstallMode, pub out_dir: PathBuf, pub out_name: Option, pub bindgen: Option, pub cache: Cache, pub extra_options: Vec, } /// What sort of output we're going to be generating and flags we're invoking /// `wasm-bindgen` with. #[derive(Clone, Copy, Debug)] pub enum Target { /// Default output mode or `--target bundler`, indicates output will be /// used with a bundle in a later step. Bundler, /// Correspond to `--target web` where the output is natively usable as an /// ES module in a browser and the wasm is manually instantiated. Web, /// Correspond to `--target nodejs` where the output is natively usable as /// a Node.js module loaded with `require`. Nodejs, /// Correspond to `--target no-modules` where the output is natively usable /// in a browser but pollutes the global namespace and must be manually /// instantiated. NoModules, /// Correspond to `--target deno` where the output is natively usable as /// a Deno module loaded with `import`. Deno, } impl Default for Target { fn default() -> Target { Target::Bundler } } impl fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match self { Target::Bundler => "bundler", Target::Web => "web", Target::Nodejs => "nodejs", Target::NoModules => "no-modules", Target::Deno => "deno", }; write!(f, "{}", s) } } impl FromStr for Target { type Err = Error; fn from_str(s: &str) -> Result { match s { "bundler" | "browser" => Ok(Target::Bundler), "web" => Ok(Target::Web), "nodejs" => Ok(Target::Nodejs), "no-modules" => Ok(Target::NoModules), "deno" => Ok(Target::Deno), _ => bail!("Unknown target: {}", s), } } } /// The build profile controls whether optimizations, debug info, and assertions /// are enabled or disabled. #[derive(Clone, Debug)] pub enum BuildProfile { /// Enable assertions and debug info. Disable optimizations. Dev, /// Enable optimizations. Disable assertions and debug info. Release, /// Enable optimizations and debug info. Disable assertions. Profiling, /// User-defined profile with --profile flag Custom(String), } /// Everything required to configure and run the `wasm-pack build` command. #[derive(Debug, Args)] #[command(allow_hyphen_values = true, trailing_var_arg = true)] pub struct BuildOptions { /// The path to the Rust crate. If not set, searches up the path from the current directory. #[clap()] pub path: Option, /// The npm scope to use in package.json, if any. #[clap(long = "scope", short = 's')] pub scope: Option, #[clap(long = "mode", short = 'm', default_value = "normal")] /// Sets steps to be run. [possible values: no-install, normal, force] pub mode: InstallMode, #[clap(long = "no-typescript")] /// By default a *.d.ts file is generated for the generated JS file, but /// this flag will disable generating this TypeScript file. pub disable_dts: bool, #[clap(long = "weak-refs")] /// Enable usage of the JS weak references proposal. pub weak_refs: bool, #[clap(long = "reference-types")] /// Enable usage of WebAssembly reference types. pub reference_types: bool, #[clap(long = "target", short = 't', default_value = "bundler")] /// Sets the target environment. [possible values: bundler, nodejs, web, no-modules, deno] pub target: Target, #[clap(long = "debug")] /// Deprecated. Renamed to `--dev`. pub debug: bool, #[clap(long = "dev")] /// Create a development build. Enable debug info, and disable /// optimizations. pub dev: bool, #[clap(long = "release")] /// Create a release build. Enable optimizations and disable debug info. pub release: bool, #[clap(long = "profiling")] /// Create a profiling build. Enable optimizations and debug info. pub profiling: bool, #[clap(long = "profile")] /// User-defined profile with --profile flag pub profile: Option, #[clap(long = "out-dir", short = 'd', default_value = "pkg")] /// Sets the output directory with a relative path. pub out_dir: String, #[clap(long = "out-name")] /// Sets the output file names. Defaults to package name. pub out_name: Option, #[clap(long = "no-pack", alias = "no-package")] /// Option to not generate a package.json pub no_pack: bool, #[clap(long = "no-opt", alias = "no-optimization")] /// Option to skip optimization with wasm-opt pub no_opt: bool, /// List of extra options to pass to `cargo build` pub extra_options: Vec, } impl Default for BuildOptions { fn default() -> Self { Self { path: None, scope: None, mode: InstallMode::default(), disable_dts: false, weak_refs: false, reference_types: false, target: Target::default(), debug: false, dev: false, no_pack: false, no_opt: false, release: false, profiling: false, profile: None, out_dir: String::new(), out_name: None, extra_options: Vec::new(), } } } type BuildStep = fn(&mut Build) -> Result<()>; impl Build { /// Construct a build command from the given options. pub fn try_from_opts(mut build_opts: BuildOptions) -> Result { if let Some(path) = &build_opts.path { if path.to_string_lossy().starts_with("--") { let path = build_opts.path.take().unwrap(); build_opts .extra_options .insert(0, path.to_string_lossy().into_owned()); } } let crate_path = get_crate_path(build_opts.path)?; let crate_data = manifest::CrateData::new(&crate_path, build_opts.out_name.clone())?; let out_dir = crate_path.join(PathBuf::from(build_opts.out_dir)).clean(); let dev = build_opts.dev || build_opts.debug; let profile = match ( dev, build_opts.release, build_opts.profiling, build_opts.profile, ) { (false, false, false, None) | (false, true, false, None) => BuildProfile::Release, (true, false, false, None) => BuildProfile::Dev, (false, false, true, None) => BuildProfile::Profiling, (false, false, false, Some(profile)) => BuildProfile::Custom(profile), // Unfortunately, `clap` doesn't expose clap's `conflicts_with` // functionality yet, so we have to implement it ourselves. _ => bail!("Can only supply one of the --dev, --release, --profiling, or --profile 'name' flags"), }; Ok(Build { crate_path, crate_data, scope: build_opts.scope, disable_dts: build_opts.disable_dts, weak_refs: build_opts.weak_refs, reference_types: build_opts.reference_types, target: build_opts.target, no_pack: build_opts.no_pack, no_opt: build_opts.no_opt, profile, mode: build_opts.mode, out_dir, out_name: build_opts.out_name, bindgen: None, cache: cache::get_wasm_pack_cache()?, extra_options: build_opts.extra_options, }) } /// Configures the global binary cache used for this build pub fn set_cache(&mut self, cache: Cache) { self.cache = cache; } /// Execute this `Build` command. pub fn run(&mut self) -> Result<()> { let process_steps = Build::get_process_steps(self.mode, self.no_pack, self.no_opt); let started = Instant::now(); for (_, process_step) in process_steps { process_step(self)?; } let duration = crate::command::utils::elapsed(started.elapsed()); info!("Done in {}.", &duration); info!( "Your wasm pkg is ready to publish at {}.", self.out_dir.display() ); PBAR.info(&format!("{} Done in {}", emoji::SPARKLE, &duration)); PBAR.info(&format!( "{} Your wasm pkg is ready to publish at {}.", emoji::PACKAGE, self.out_dir.display() )); Ok(()) } fn get_process_steps( mode: InstallMode, no_pack: bool, no_opt: bool, ) -> Vec<(&'static str, BuildStep)> { macro_rules! steps { ($($name:ident),+) => { { let mut steps: Vec<(&'static str, BuildStep)> = Vec::new(); $(steps.push((stringify!($name), Build::$name));)* steps } }; ($($name:ident,)*) => (steps![$($name),*]) } let mut steps = Vec::new(); match &mode { InstallMode::Force => {} _ => { steps.extend(steps![ step_check_rustc_version, step_check_crate_config, step_check_for_wasm_target, ]); } } steps.extend(steps![ step_build_wasm, step_create_dir, step_install_wasm_bindgen, step_run_wasm_bindgen, ]); if !no_opt { steps.extend(steps![step_run_wasm_opt]); } if !no_pack { steps.extend(steps![ step_create_json, step_copy_readme, step_copy_license, ]); } steps } fn step_check_rustc_version(&mut self) -> Result<()> { info!("Checking rustc version..."); let version = build::check_rustc_version()?; let msg = format!("rustc version is {}.", version); info!("{}", &msg); Ok(()) } fn step_check_crate_config(&mut self) -> Result<()> { info!("Checking crate configuration..."); self.crate_data.check_crate_config()?; info!("Crate is correctly configured."); Ok(()) } fn step_check_for_wasm_target(&mut self) -> Result<()> { info!("Checking for wasm-target..."); build::wasm_target::check_for_wasm32_target()?; info!("Checking for wasm-target was successful."); Ok(()) } fn step_build_wasm(&mut self) -> Result<()> { info!("Building wasm..."); build::cargo_build_wasm(&self.crate_path, self.profile.clone(), &self.extra_options)?; info!( "wasm built at {:#?}.", &self .crate_path .join("target") .join("wasm32-unknown-unknown") .join("release") ); Ok(()) } fn step_create_dir(&mut self) -> Result<()> { info!("Creating a pkg directory..."); create_pkg_dir(&self.out_dir)?; info!("Created a pkg directory at {:#?}.", &self.crate_path); Ok(()) } fn step_create_json(&mut self) -> Result<()> { self.crate_data.write_package_json( &self.out_dir, &self.scope, self.disable_dts, self.target, )?; info!( "Wrote a package.json at {:#?}.", &self.out_dir.join("package.json") ); Ok(()) } fn step_copy_readme(&mut self) -> Result<()> { info!("Copying readme from crate..."); readme::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?; info!("Copied readme from crate to {:#?}.", &self.out_dir); Ok(()) } fn step_copy_license(&mut self) -> Result<()> { info!("Copying license from crate..."); license::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?; info!("Copied license from crate to {:#?}.", &self.out_dir); Ok(()) } fn step_install_wasm_bindgen(&mut self) -> Result<()> { info!("Identifying wasm-bindgen dependency..."); let lockfile = Lockfile::new(&self.crate_data)?; let bindgen_version = lockfile.require_wasm_bindgen()?; info!("Installing wasm-bindgen-cli..."); let bindgen = install::download_prebuilt_or_cargo_install( Tool::WasmBindgen, &self.cache, bindgen_version, self.mode.install_permitted(), )?; self.bindgen = Some(bindgen); info!("Installing wasm-bindgen-cli was successful."); Ok(()) } fn step_run_wasm_bindgen(&mut self) -> Result<()> { info!("Building the wasm bindings..."); bindgen::wasm_bindgen_build( &self.crate_data, self.bindgen.as_ref().unwrap(), &self.out_dir, &self.out_name, self.disable_dts, self.weak_refs, self.reference_types, self.target, self.profile.clone(), &self.extra_options, )?; info!("wasm bindings were built at {:#?}.", &self.out_dir); Ok(()) } fn step_run_wasm_opt(&mut self) -> Result<()> { let mut args = match self .crate_data .configured_profile(self.profile.clone()) .wasm_opt_args() { Some(args) => args, None => return Ok(()), }; if self.reference_types { args.push("--enable-reference-types".into()); } info!("executing wasm-opt with {:?}", args); wasm_opt::run( &self.cache, &self.out_dir, &args, self.mode.install_permitted(), ).map_err(|e| { anyhow!( "{}\nTo disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.", e ) }) } }