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}