wasm_pack/command/
build.rs

1//! Implementation of the `wasm-pack build` command.
2
3use crate::bindgen;
4use crate::build;
5use crate::cache;
6use crate::command::utils::{create_pkg_dir, get_crate_path};
7use crate::emoji;
8use crate::install::{self, InstallMode, Tool};
9use crate::license;
10use crate::lockfile::Lockfile;
11use crate::manifest;
12use crate::readme;
13use crate::wasm_opt;
14use crate::PBAR;
15use anyhow::{anyhow, bail, Error, Result};
16use binary_install::Cache;
17use clap::Args;
18use log::info;
19use path_clean::PathClean;
20use std::fmt;
21use std::path::PathBuf;
22use std::str::FromStr;
23use std::time::Instant;
24
25/// Everything required to configure and run the `wasm-pack build` command.
26#[allow(missing_docs)]
27pub struct Build {
28    pub crate_path: PathBuf,
29    pub crate_data: manifest::CrateData,
30    pub scope: Option<String>,
31    pub disable_dts: bool,
32    pub weak_refs: bool,
33    pub reference_types: bool,
34    pub target: Target,
35    pub no_pack: bool,
36    pub no_opt: bool,
37    pub profile: BuildProfile,
38    pub mode: InstallMode,
39    pub out_dir: PathBuf,
40    pub out_name: Option<String>,
41    pub bindgen: Option<install::Status>,
42    pub cache: Cache,
43    pub extra_options: Vec<String>,
44    target_triple: String,
45    wasm_path: Option<String>,
46}
47
48/// What sort of output we're going to be generating and flags we're invoking
49/// `wasm-bindgen` with.
50#[derive(Clone, Copy, Debug)]
51pub enum Target {
52    /// Default output mode or `--target bundler`, indicates output will be
53    /// used with a bundle in a later step.
54    Bundler,
55    /// Correspond to `--target web` where the output is natively usable as an
56    /// ES module in a browser and the wasm is manually instantiated.
57    Web,
58    /// Correspond to `--target nodejs` where the output is natively usable as
59    /// a Node.js module loaded with `require`.
60    Nodejs,
61    /// Correspond to `--target no-modules` where the output is natively usable
62    /// in a browser but pollutes the global namespace and must be manually
63    /// instantiated.
64    NoModules,
65    /// Correspond to `--target deno` where the output is natively usable as
66    /// a Deno module loaded with `import`.
67    Deno,
68}
69
70impl Default for Target {
71    fn default() -> Target {
72        Target::Bundler
73    }
74}
75
76impl fmt::Display for Target {
77    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78        let s = match self {
79            Target::Bundler => "bundler",
80            Target::Web => "web",
81            Target::Nodejs => "nodejs",
82            Target::NoModules => "no-modules",
83            Target::Deno => "deno",
84        };
85        write!(f, "{}", s)
86    }
87}
88
89impl FromStr for Target {
90    type Err = Error;
91    fn from_str(s: &str) -> Result<Self> {
92        match s {
93            "bundler" | "browser" => Ok(Target::Bundler),
94            "web" => Ok(Target::Web),
95            "nodejs" => Ok(Target::Nodejs),
96            "no-modules" => Ok(Target::NoModules),
97            "deno" => Ok(Target::Deno),
98            _ => bail!("Unknown target: {}", s),
99        }
100    }
101}
102
103/// The build profile controls whether optimizations, debug info, and assertions
104/// are enabled or disabled.
105#[derive(Clone, Debug)]
106pub enum BuildProfile {
107    /// Enable assertions and debug info. Disable optimizations.
108    Dev,
109    /// Enable optimizations. Disable assertions and debug info.
110    Release,
111    /// Enable optimizations and debug info. Disable assertions.
112    Profiling,
113    /// User-defined profile with --profile flag
114    Custom(String),
115}
116
117/// Everything required to configure and run the `wasm-pack build` command.
118#[derive(Debug, Args)]
119#[command(allow_hyphen_values = true, trailing_var_arg = true)]
120pub struct BuildOptions {
121    /// The path to the Rust crate. If not set, searches up the path from the current directory.
122    #[clap()]
123    pub path: Option<PathBuf>,
124
125    /// The npm scope to use in package.json, if any.
126    #[clap(long = "scope", short = 's')]
127    pub scope: Option<String>,
128
129    #[clap(long = "mode", short = 'm', default_value = "normal")]
130    /// Sets steps to be run. [possible values: no-install, normal, force]
131    pub mode: InstallMode,
132
133    #[clap(long = "no-typescript")]
134    /// By default a *.d.ts file is generated for the generated JS file, but
135    /// this flag will disable generating this TypeScript file.
136    pub disable_dts: bool,
137
138    #[clap(long = "weak-refs")]
139    /// Enable usage of the JS weak references proposal.
140    pub weak_refs: bool,
141
142    #[clap(long = "reference-types")]
143    /// Enable usage of WebAssembly reference types.
144    pub reference_types: bool,
145
146    #[clap(long = "target", short = 't', default_value = "bundler")]
147    /// Sets the target environment. [possible values: bundler, nodejs, web, no-modules, deno]
148    pub target: Target,
149
150    #[clap(long = "debug")]
151    /// Deprecated. Renamed to `--dev`.
152    pub debug: bool,
153
154    #[clap(long = "dev")]
155    /// Create a development build. Enable debug info, and disable
156    /// optimizations.
157    pub dev: bool,
158
159    #[clap(long = "release")]
160    /// Create a release build. Enable optimizations and disable debug info.
161    pub release: bool,
162
163    #[clap(long = "profiling")]
164    /// Create a profiling build. Enable optimizations and debug info.
165    pub profiling: bool,
166
167    #[clap(long = "profile")]
168    /// User-defined profile with --profile flag
169    pub profile: Option<String>,
170
171    #[clap(long = "out-dir", short = 'd', default_value = "pkg")]
172    /// Sets the output directory with a relative path.
173    pub out_dir: String,
174
175    #[clap(long = "out-name")]
176    /// Sets the output file names. Defaults to package name.
177    pub out_name: Option<String>,
178
179    #[clap(long = "no-pack", alias = "no-package")]
180    /// Option to not generate a package.json
181    pub no_pack: bool,
182
183    #[clap(long = "no-opt", alias = "no-optimization")]
184    /// Option to skip optimization with wasm-opt
185    pub no_opt: bool,
186
187    /// List of extra options to pass to `cargo build`
188    pub extra_options: Vec<String>,
189}
190
191impl Default for BuildOptions {
192    fn default() -> Self {
193        Self {
194            path: None,
195            scope: None,
196            mode: InstallMode::default(),
197            disable_dts: false,
198            weak_refs: false,
199            reference_types: false,
200            target: Target::default(),
201            debug: false,
202            dev: false,
203            no_pack: false,
204            no_opt: false,
205            release: false,
206            profiling: false,
207            profile: None,
208            out_dir: String::new(),
209            out_name: None,
210            extra_options: Vec::new(),
211        }
212    }
213}
214
215type BuildStep = fn(&mut Build) -> Result<()>;
216
217impl Build {
218    /// Construct a build command from the given options.
219    pub fn try_from_opts(mut build_opts: BuildOptions) -> Result<Self> {
220        if let Some(path) = &build_opts.path {
221            if path.to_string_lossy().starts_with("--") {
222                let path = build_opts.path.take().unwrap();
223                build_opts
224                    .extra_options
225                    .insert(0, path.to_string_lossy().into_owned());
226            }
227        }
228        let crate_path = get_crate_path(build_opts.path)?;
229        let crate_data = manifest::CrateData::new(&crate_path, build_opts.out_name.clone())?;
230        let out_dir = crate_path.join(PathBuf::from(build_opts.out_dir)).clean();
231
232        let dev = build_opts.dev || build_opts.debug;
233        let profile = match (
234            dev,
235            build_opts.release,
236            build_opts.profiling,
237            build_opts.profile,
238        ) {
239            (false, false, false, None) | (false, true, false, None) => BuildProfile::Release,
240            (true, false, false, None) => BuildProfile::Dev,
241            (false, false, true, None) => BuildProfile::Profiling,
242            (false, false, false, Some(profile)) => BuildProfile::Custom(profile),
243            // Unfortunately, `clap` doesn't expose clap's `conflicts_with`
244            // functionality yet, so we have to implement it ourselves.
245            _ => bail!("Can only supply one of the --dev, --release, --profiling, or --profile 'name' flags"),
246        };
247
248        let extra_options = build_opts.extra_options;
249
250        let target_triple = {
251            let mut extra_options_iter = extra_options.iter();
252            if extra_options_iter
253                .by_ref()
254                .any(|option| option == "--target")
255            {
256                extra_options_iter.next().map(|s| s.as_str())
257            } else {
258                None
259            }
260            .unwrap_or("wasm32-unknown-unknown")
261        };
262
263        Ok(Build {
264            crate_path,
265            crate_data,
266            scope: build_opts.scope,
267            disable_dts: build_opts.disable_dts,
268            weak_refs: build_opts.weak_refs,
269            reference_types: build_opts.reference_types,
270            target: build_opts.target,
271            no_pack: build_opts.no_pack,
272            no_opt: build_opts.no_opt,
273            profile,
274            mode: build_opts.mode,
275            out_dir,
276            out_name: build_opts.out_name,
277            bindgen: None,
278            cache: cache::get_wasm_pack_cache()?,
279            target_triple: target_triple.to_owned(),
280            extra_options,
281            wasm_path: None,
282        })
283    }
284
285    /// Configures the global binary cache used for this build
286    pub fn set_cache(&mut self, cache: Cache) {
287        self.cache = cache;
288    }
289
290    /// Execute this `Build` command.
291    pub fn run(&mut self) -> Result<()> {
292        let process_steps = Build::get_process_steps(self.mode, self.no_pack, self.no_opt);
293
294        let started = Instant::now();
295
296        for (_, process_step) in process_steps {
297            process_step(self)?;
298        }
299
300        let duration = crate::command::utils::elapsed(started.elapsed());
301        info!("Done in {}.", &duration);
302        info!(
303            "Your wasm pkg is ready to publish at {}.",
304            self.out_dir.display()
305        );
306
307        PBAR.info(&format!("{} Done in {}", emoji::SPARKLE, &duration));
308
309        PBAR.info(&format!(
310            "{} Your wasm pkg is ready to publish at {}.",
311            emoji::PACKAGE,
312            self.out_dir.display()
313        ));
314        Ok(())
315    }
316
317    fn get_process_steps(
318        mode: InstallMode,
319        no_pack: bool,
320        no_opt: bool,
321    ) -> Vec<(&'static str, BuildStep)> {
322        macro_rules! steps {
323            ($($name:ident),+) => {
324                {
325                let mut steps: Vec<(&'static str, BuildStep)> = Vec::new();
326                    $(steps.push((stringify!($name), Build::$name));)*
327                        steps
328                    }
329                };
330            ($($name:ident,)*) => (steps![$($name),*])
331        }
332        let mut steps = Vec::new();
333        match &mode {
334            InstallMode::Force => {}
335            _ => {
336                steps.extend(steps![
337                    step_check_rustc_version,
338                    step_check_crate_config,
339                    step_check_for_wasm_target,
340                ]);
341            }
342        }
343
344        steps.extend(steps![
345            step_build_wasm,
346            step_create_dir,
347            step_install_wasm_bindgen,
348            step_run_wasm_bindgen,
349        ]);
350
351        if !no_opt {
352            steps.extend(steps![step_run_wasm_opt]);
353        }
354
355        if !no_pack {
356            steps.extend(steps![
357                step_create_json,
358                step_copy_readme,
359                step_copy_license,
360            ]);
361        }
362
363        steps
364    }
365
366    fn step_check_rustc_version(&mut self) -> Result<()> {
367        info!("Checking rustc version...");
368        let version = build::check_rustc_version()?;
369        let msg = format!("rustc version is {}.", version);
370        info!("{}", &msg);
371        Ok(())
372    }
373
374    fn step_check_crate_config(&mut self) -> Result<()> {
375        info!("Checking crate configuration...");
376        self.crate_data.check_crate_config()?;
377        info!("Crate is correctly configured.");
378        Ok(())
379    }
380
381    fn step_check_for_wasm_target(&mut self) -> Result<()> {
382        info!("Checking for wasm-target...");
383        build::wasm_target::check_for_wasm_target(&self.target_triple)?;
384        info!("Checking for wasm-target was successful.");
385        Ok(())
386    }
387
388    fn step_build_wasm(&mut self) -> Result<()> {
389        info!("Building wasm...");
390        let wasm_path =
391            build::cargo_build_wasm(&self.crate_path, self.profile.clone(), &self.extra_options)?;
392        info!("wasm built at {wasm_path:#?}.");
393        self.wasm_path = Some(wasm_path);
394        Ok(())
395    }
396
397    fn step_create_dir(&mut self) -> Result<()> {
398        info!("Creating a pkg directory...");
399        create_pkg_dir(&self.out_dir)?;
400        info!("Created a pkg directory at {:#?}.", &self.crate_path);
401        Ok(())
402    }
403
404    fn step_create_json(&mut self) -> Result<()> {
405        self.crate_data.write_package_json(
406            &self.out_dir,
407            &self.scope,
408            self.disable_dts,
409            self.target,
410        )?;
411        info!(
412            "Wrote a package.json at {:#?}.",
413            &self.out_dir.join("package.json")
414        );
415        Ok(())
416    }
417
418    fn step_copy_readme(&mut self) -> Result<()> {
419        info!("Copying readme from crate...");
420        readme::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?;
421        info!("Copied readme from crate to {:#?}.", &self.out_dir);
422        Ok(())
423    }
424
425    fn step_copy_license(&mut self) -> Result<()> {
426        info!("Copying license from crate...");
427        license::copy_from_crate(&self.crate_data, &self.crate_path, &self.out_dir)?;
428        info!("Copied license from crate to {:#?}.", &self.out_dir);
429        Ok(())
430    }
431
432    fn step_install_wasm_bindgen(&mut self) -> Result<()> {
433        info!("Identifying wasm-bindgen dependency...");
434        let lockfile = Lockfile::new(&self.crate_data)?;
435        let bindgen_version = lockfile.require_wasm_bindgen()?;
436        info!("Installing wasm-bindgen-cli...");
437        let bindgen = install::download_prebuilt_or_cargo_install(
438            Tool::WasmBindgen,
439            &self.cache,
440            bindgen_version,
441            self.mode.install_permitted(),
442        )?;
443        self.bindgen = Some(bindgen);
444        info!("Installing wasm-bindgen-cli was successful.");
445        Ok(())
446    }
447
448    fn step_run_wasm_bindgen(&mut self) -> Result<()> {
449        info!("Building the wasm bindings...");
450        bindgen::wasm_bindgen_build(
451            self.wasm_path.as_ref().unwrap(),
452            &self.crate_data,
453            self.bindgen.as_ref().unwrap(),
454            &self.out_dir,
455            &self.out_name,
456            self.disable_dts,
457            self.weak_refs,
458            self.reference_types,
459            self.target,
460            self.profile.clone(),
461        )?;
462        info!("wasm bindings were built at {:#?}.", &self.out_dir);
463        Ok(())
464    }
465
466    fn step_run_wasm_opt(&mut self) -> Result<()> {
467        let mut args = match self
468            .crate_data
469            .configured_profile(self.profile.clone())
470            .wasm_opt_args()
471        {
472            Some(args) => args,
473            None => return Ok(()),
474        };
475        if self.reference_types {
476            args.push("--enable-reference-types".into());
477        }
478        info!("executing wasm-opt with {:?}", args);
479        wasm_opt::run(
480            &self.cache,
481            &self.out_dir,
482            &args,
483            self.mode.install_permitted(),
484        ).map_err(|e| {
485            anyhow!(
486                "{}\nTo disable `wasm-opt`, add `wasm-opt = false` to your package metadata in your `Cargo.toml`.", e
487            )
488        })
489    }
490}