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