use crate::{
anyhow::{ensure, Context, Result},
camino, clap, default_build_command, metadata,
};
use lazy_static::lazy_static;
use std::{fs, path::PathBuf, process};
use wasm_bindgen_cli_support::Bindgen;
#[non_exhaustive]
#[derive(Debug, clap::Parser)]
#[clap(
about = "Generate the distributed package.",
long_about = "Generate the distributed package.\n\
It will build and package the project for WASM."
)]
pub struct Dist {
#[clap(short, long)]
pub quiet: bool,
#[clap(short, long)]
pub jobs: Option<String>,
#[clap(long)]
pub profile: Option<String>,
#[clap(long)]
pub release: bool,
#[clap(long)]
pub features: Vec<String>,
#[clap(long)]
pub all_features: bool,
#[clap(long)]
pub no_default_features: bool,
#[clap(short, long)]
pub verbose: bool,
#[clap(long)]
pub color: Option<String>,
#[clap(long)]
pub frozen: bool,
#[clap(long)]
pub locked: bool,
#[clap(long)]
pub offline: bool,
#[clap(long)]
pub ignore_rust_version: bool,
#[clap(long)]
pub example: Option<String>,
#[clap(skip = default_build_command())]
pub build_command: process::Command,
#[clap(skip)]
pub dist_dir_path: Option<PathBuf>,
#[clap(skip)]
pub static_dir_path: Option<PathBuf>,
#[clap(skip)]
pub app_name: Option<String>,
#[clap(skip = true)]
pub run_in_workspace: bool,
#[cfg(feature = "sass")]
#[clap(skip)]
pub sass_options: sass_rs::Options,
}
impl Dist {
pub fn build_command(mut self, command: process::Command) -> Self {
self.build_command = command;
self
}
pub fn dist_dir_path(mut self, path: impl Into<PathBuf>) -> Self {
self.dist_dir_path = Some(path.into());
self
}
pub fn static_dir_path(mut self, path: impl Into<PathBuf>) -> Self {
self.static_dir_path = Some(path.into());
self
}
pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
self.app_name = Some(app_name.into());
self
}
pub fn run_in_workspace(mut self, res: bool) -> Self {
self.run_in_workspace = res;
self
}
#[cfg(feature = "sass")]
pub fn sass_options(mut self, output_style: sass_rs::Options) -> Self {
self.sass_options = output_style;
self
}
pub fn example(mut self, example: impl Into<String>) -> Self {
self.example = Some(example.into());
self
}
pub fn run(self, package_name: &str) -> Result<PathBuf> {
log::trace!("Getting package's metadata");
let metadata = metadata();
let dist_dir_path = self
.dist_dir_path
.unwrap_or_else(|| default_dist_dir(self.release).as_std_path().to_path_buf());
log::trace!("Initializing dist process");
let mut build_command = self.build_command;
if self.run_in_workspace {
build_command.current_dir(&metadata.workspace_root);
}
if self.quiet {
build_command.arg("--quiet");
}
if let Some(number) = self.jobs {
build_command.args(["--jobs", &number]);
}
if let Some(profile) = self.profile {
build_command.args(["--profile", &profile]);
}
if self.release {
build_command.arg("--release");
}
for feature in &self.features {
build_command.args(["--features", feature]);
}
if self.all_features {
build_command.arg("--all-features");
}
if self.no_default_features {
build_command.arg("--no-default-features");
}
if self.verbose {
build_command.arg("--verbose");
}
if let Some(color) = self.color {
build_command.args(["--color", &color]);
}
if self.frozen {
build_command.arg("--frozen");
}
if self.locked {
build_command.arg("--locked");
}
if self.offline {
build_command.arg("--offline");
}
if self.ignore_rust_version {
build_command.arg("--ignore-rust-version");
}
build_command.args(["--package", package_name]);
if let Some(example) = &self.example {
build_command.args(["--example", example]);
}
let build_dir = metadata
.target_directory
.join("wasm32-unknown-unknown")
.join(if self.release { "release" } else { "debug" });
let input_path = if let Some(example) = &self.example {
build_dir
.join("examples")
.join(example.replace('-', "_"))
.with_extension("wasm")
} else {
build_dir
.join(package_name.replace('-', "_"))
.with_extension("wasm")
};
if input_path.exists() {
log::trace!("Removing existing target directory");
fs::remove_file(&input_path).context("cannot remove existing target")?;
}
log::trace!("Spawning build process");
ensure!(
build_command
.status()
.context("could not start cargo")?
.success(),
"cargo command failed"
);
let app_name = self.app_name.unwrap_or_else(|| "app".to_string());
log::trace!("Generating Wasm output");
let mut output = Bindgen::new()
.input_path(input_path)
.out_name(&app_name)
.web(true)
.expect("web have panic")
.debug(!self.release)
.generate_output()
.context("could not generate Wasm bindgen file")?;
if dist_dir_path.exists() {
log::trace!("Removing already existing dist directory");
fs::remove_dir_all(&dist_dir_path)?;
}
log::trace!("Writing outputs to dist directory");
output.emit(&dist_dir_path)?;
if let Some(static_dir) = self.static_dir_path {
#[cfg(feature = "sass")]
{
log::trace!("Generating CSS files from SASS/SCSS");
sass(&static_dir, &dist_dir_path, &self.sass_options)?;
}
#[cfg(not(feature = "sass"))]
{
let mut copy_options = fs_extra::dir::CopyOptions::new();
copy_options.overwrite = true;
copy_options.content_only = true;
log::trace!("Copying static directory into dist directory");
fs_extra::dir::copy(static_dir, &dist_dir_path, ©_options)
.context("cannot copy static directory")?;
}
}
log::info!("Successfully built in {}", dist_dir_path.display());
Ok(dist_dir_path)
}
}
impl Default for Dist {
fn default() -> Dist {
Dist {
quiet: Default::default(),
jobs: Default::default(),
profile: Default::default(),
release: Default::default(),
features: Default::default(),
all_features: Default::default(),
no_default_features: Default::default(),
verbose: Default::default(),
color: Default::default(),
frozen: Default::default(),
locked: Default::default(),
offline: Default::default(),
ignore_rust_version: Default::default(),
example: Default::default(),
build_command: default_build_command(),
dist_dir_path: Default::default(),
static_dir_path: Default::default(),
app_name: Default::default(),
run_in_workspace: Default::default(),
#[cfg(feature = "sass")]
sass_options: Default::default(),
}
}
}
#[cfg(feature = "sass")]
fn sass(
static_dir: &std::path::Path,
dist_dir: &std::path::Path,
options: &sass_rs::Options,
) -> Result<()> {
fn is_sass(path: &std::path::Path) -> bool {
matches!(
path.extension()
.and_then(|x| x.to_str().map(|x| x.to_lowercase()))
.as_deref(),
Some("sass") | Some("scss")
)
}
fn should_ignore(path: &std::path::Path) -> bool {
path.file_name()
.expect("WalkDir does not yield paths ending with `..` or `.`")
.to_str()
.map(|x| x.starts_with('_'))
.unwrap_or(false)
}
log::trace!("Generating dist artifacts");
let walker = walkdir::WalkDir::new(static_dir);
for entry in walker {
let entry = entry
.with_context(|| format!("cannot walk into directory `{}`", &static_dir.display()))?;
let source = entry.path();
let dest = dist_dir.join(source.strip_prefix(static_dir).unwrap());
let _ = fs::create_dir_all(dest.parent().unwrap());
if !source.is_file() {
continue;
} else if is_sass(source) {
if !should_ignore(source) {
let dest = dest.with_extension("css");
let css = sass_rs::compile_file(source, options.clone())
.expect("could not convert SASS/ file");
fs::write(&dest, css)
.with_context(|| format!("could not write CSS to file `{}`", dest.display()))?;
}
} else {
fs::copy(source, &dest).with_context(|| {
format!("cannot move `{}` to `{}`", source.display(), dest.display())
})?;
}
}
Ok(())
}
pub fn default_dist_dir(release: bool) -> &'static camino::Utf8Path {
lazy_static! {
static ref DEFAULT_RELEASE_PATH: camino::Utf8PathBuf =
metadata().target_directory.join("release").join("dist");
static ref DEFAULT_DEBUG_PATH: camino::Utf8PathBuf =
metadata().target_directory.join("debug").join("dist");
}
if release {
&DEFAULT_RELEASE_PATH
} else {
&DEFAULT_DEBUG_PATH
}
}