1use crate::{
2    anyhow::{ensure, Context, Result},
3    camino, clap, default_build_command, metadata,
4};
5use lazy_static::lazy_static;
6use std::{fs, path::PathBuf, process};
7use wasm_bindgen_cli_support::Bindgen;
8
9#[non_exhaustive]
47#[derive(Debug, clap::Parser)]
48#[clap(
49    about = "Generate the distributed package.",
50    long_about = "Generate the distributed package.\n\
51        It will build and package the project for WASM."
52)]
53pub struct Dist {
54    #[clap(short, long)]
56    pub quiet: bool,
57    #[clap(short, long)]
59    pub jobs: Option<String>,
60    #[clap(long)]
62    pub profile: Option<String>,
63    #[clap(long)]
65    pub release: bool,
66    #[clap(long)]
68    pub features: Vec<String>,
69    #[clap(long)]
71    pub all_features: bool,
72    #[clap(long)]
74    pub no_default_features: bool,
75    #[clap(short, long)]
77    pub verbose: bool,
78    #[clap(long)]
80    pub color: Option<String>,
81    #[clap(long)]
83    pub frozen: bool,
84    #[clap(long)]
86    pub locked: bool,
87    #[clap(long)]
89    pub offline: bool,
90    #[clap(long)]
92    pub ignore_rust_version: bool,
93    #[clap(long)]
95    pub example: Option<String>,
96
97    #[clap(skip = default_build_command())]
99    pub build_command: process::Command,
100    #[clap(skip)]
102    pub dist_dir_path: Option<PathBuf>,
103    #[clap(skip)]
105    pub static_dir_path: Option<PathBuf>,
106    #[clap(skip)]
108    pub app_name: Option<String>,
109    #[clap(skip = true)]
111    pub run_in_workspace: bool,
112    #[cfg(feature = "sass")]
114    #[clap(skip)]
115    pub sass_options: sass_rs::Options,
116}
117
118impl Dist {
119    pub fn build_command(mut self, command: process::Command) -> Self {
123        self.build_command = command;
124        self
125    }
126
127    pub fn dist_dir_path(mut self, path: impl Into<PathBuf>) -> Self {
132        self.dist_dir_path = Some(path.into());
133        self
134    }
135
136    pub fn static_dir_path(mut self, path: impl Into<PathBuf>) -> Self {
138        self.static_dir_path = Some(path.into());
139        self
140    }
141
142    pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
146        self.app_name = Some(app_name.into());
147        self
148    }
149
150    pub fn run_in_workspace(mut self, res: bool) -> Self {
152        self.run_in_workspace = res;
153        self
154    }
155
156    #[cfg(feature = "sass")]
157    pub fn sass_options(mut self, output_style: sass_rs::Options) -> Self {
159        self.sass_options = output_style;
160        self
161    }
162
163    pub fn example(mut self, example: impl Into<String>) -> Self {
165        self.example = Some(example.into());
166        self
167    }
168
169    pub fn run(self, package_name: &str) -> Result<PathBuf> {
178        log::trace!("Getting package's metadata");
179        let metadata = metadata();
180
181        let dist_dir_path = self
182            .dist_dir_path
183            .unwrap_or_else(|| default_dist_dir(self.release).as_std_path().to_path_buf());
184
185        log::trace!("Initializing dist process");
186        let mut build_command = self.build_command;
187
188        if self.run_in_workspace {
189            build_command.current_dir(&metadata.workspace_root);
190        }
191
192        if self.quiet {
193            build_command.arg("--quiet");
194        }
195
196        if let Some(number) = self.jobs {
197            build_command.args(["--jobs", &number]);
198        }
199
200        if let Some(profile) = self.profile {
201            build_command.args(["--profile", &profile]);
202        }
203
204        if self.release {
205            build_command.arg("--release");
206        }
207
208        for feature in &self.features {
209            build_command.args(["--features", feature]);
210        }
211
212        if self.all_features {
213            build_command.arg("--all-features");
214        }
215
216        if self.no_default_features {
217            build_command.arg("--no-default-features");
218        }
219
220        if self.verbose {
221            build_command.arg("--verbose");
222        }
223
224        if let Some(color) = self.color {
225            build_command.args(["--color", &color]);
226        }
227
228        if self.frozen {
229            build_command.arg("--frozen");
230        }
231
232        if self.locked {
233            build_command.arg("--locked");
234        }
235
236        if self.offline {
237            build_command.arg("--offline");
238        }
239
240        if self.ignore_rust_version {
241            build_command.arg("--ignore-rust-version");
242        }
243
244        build_command.args(["--package", package_name]);
245
246        if let Some(example) = &self.example {
247            build_command.args(["--example", example]);
248        }
249
250        let build_dir = metadata
251            .target_directory
252            .join("wasm32-unknown-unknown")
253            .join(if self.release { "release" } else { "debug" });
254        let input_path = if let Some(example) = &self.example {
255            build_dir
256                .join("examples")
257                .join(example.replace('-', "_"))
258                .with_extension("wasm")
259        } else {
260            build_dir
261                .join(package_name.replace('-', "_"))
262                .with_extension("wasm")
263        };
264
265        if input_path.exists() {
266            log::trace!("Removing existing target directory");
267            fs::remove_file(&input_path).context("cannot remove existing target")?;
268        }
269
270        log::trace!("Spawning build process");
271        ensure!(
272            build_command
273                .status()
274                .context("could not start cargo")?
275                .success(),
276            "cargo command failed"
277        );
278
279        let app_name = self.app_name.unwrap_or_else(|| "app".to_string());
280
281        log::trace!("Generating Wasm output");
282        let mut output = Bindgen::new()
283            .omit_default_module_path(false)
284            .input_path(input_path)
285            .out_name(&app_name)
286            .web(true)
287            .expect("web have panic")
288            .debug(!self.release)
289            .generate_output()
290            .context("could not generate Wasm bindgen file")?;
291
292        if dist_dir_path.exists() {
293            log::trace!("Removing already existing dist directory");
294            fs::remove_dir_all(&dist_dir_path)?;
295        }
296
297        log::trace!("Writing outputs to dist directory");
298        output.emit(&dist_dir_path)?;
299
300        if let Some(static_dir) = self.static_dir_path {
301            #[cfg(feature = "sass")]
302            {
303                log::trace!("Generating CSS files from SASS/SCSS");
304                sass(&static_dir, &dist_dir_path, &self.sass_options)?;
305            }
306
307            #[cfg(not(feature = "sass"))]
308            {
309                let mut copy_options = fs_extra::dir::CopyOptions::new();
310                copy_options.overwrite = true;
311                copy_options.content_only = true;
312
313                log::trace!("Copying static directory into dist directory");
314                fs_extra::dir::copy(static_dir, &dist_dir_path, ©_options)
315                    .context("cannot copy static directory")?;
316            }
317        }
318
319        log::info!("Successfully built in {}", dist_dir_path.display());
320
321        Ok(dist_dir_path)
322    }
323}
324
325impl Default for Dist {
326    fn default() -> Dist {
327        Dist {
328            quiet: Default::default(),
329            jobs: Default::default(),
330            profile: Default::default(),
331            release: Default::default(),
332            features: Default::default(),
333            all_features: Default::default(),
334            no_default_features: Default::default(),
335            verbose: Default::default(),
336            color: Default::default(),
337            frozen: Default::default(),
338            locked: Default::default(),
339            offline: Default::default(),
340            ignore_rust_version: Default::default(),
341            example: Default::default(),
342            build_command: default_build_command(),
343            dist_dir_path: Default::default(),
344            static_dir_path: Default::default(),
345            app_name: Default::default(),
346            run_in_workspace: Default::default(),
347            #[cfg(feature = "sass")]
348            sass_options: Default::default(),
349        }
350    }
351}
352
353#[cfg(feature = "sass")]
354fn sass(
355    static_dir: &std::path::Path,
356    dist_dir: &std::path::Path,
357    options: &sass_rs::Options,
358) -> Result<()> {
359    fn is_sass(path: &std::path::Path) -> bool {
360        matches!(
361            path.extension()
362                .and_then(|x| x.to_str().map(|x| x.to_lowercase()))
363                .as_deref(),
364            Some("sass") | Some("scss")
365        )
366    }
367
368    fn should_ignore(path: &std::path::Path) -> bool {
369        path.file_name()
370            .expect("WalkDir does not yield paths ending with `..`  or `.`")
371            .to_str()
372            .map(|x| x.starts_with('_'))
373            .unwrap_or(false)
374    }
375
376    log::trace!("Generating dist artifacts");
377    let walker = walkdir::WalkDir::new(static_dir);
378    for entry in walker {
379        let entry = entry
380            .with_context(|| format!("cannot walk into directory `{}`", &static_dir.display()))?;
381        let source = entry.path();
382        let dest = dist_dir.join(source.strip_prefix(static_dir).unwrap());
383        let _ = fs::create_dir_all(dest.parent().unwrap());
384
385        if !source.is_file() {
386            continue;
387        } else if is_sass(source) {
388            if !should_ignore(source) {
389                let dest = dest.with_extension("css");
390
391                let css = sass_rs::compile_file(source, options.clone())
392                    .expect("could not convert SASS/ file");
393                fs::write(&dest, css)
394                    .with_context(|| format!("could not write CSS to file `{}`", dest.display()))?;
395            }
396        } else {
397            fs::copy(source, &dest).with_context(|| {
398                format!("cannot move `{}` to `{}`", source.display(), dest.display())
399            })?;
400        }
401    }
402
403    Ok(())
404}
405
406pub fn default_dist_dir(release: bool) -> &'static camino::Utf8Path {
411    lazy_static! {
412        static ref DEFAULT_RELEASE_PATH: camino::Utf8PathBuf =
413            metadata().target_directory.join("release").join("dist");
414        static ref DEFAULT_DEBUG_PATH: camino::Utf8PathBuf =
415            metadata().target_directory.join("debug").join("dist");
416    }
417
418    if release {
419        &DEFAULT_RELEASE_PATH
420    } else {
421        &DEFAULT_DEBUG_PATH
422    }
423}