diff --git a/Cargo.lock b/Cargo.lock index cbda72e..3803f25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,7 @@ dependencies = [ "actix-web", "anyhow", "cargo-generate", + "cc", "clap", "dialoguer", "fs_extra", @@ -498,6 +499,7 @@ dependencies = [ "semver", "serde", "serde_json", + "toml_edit", "wasm-opt", "webbrowser", ] diff --git a/Cargo.toml b/Cargo.toml index 737a967..2f8a33a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,8 +48,16 @@ actix-web = "4.9.0" # Opening the app in the browser webbrowser = "1.0.2" +# Parsing the Cargo manifest +toml_edit = "0.22.22" + # Copying directories fs_extra = "1.3.0" # Optimizing Wasm binaries wasm-opt = { version = "0.116.1", optional = true } + +[build-dependencies] +# We don't use `cc` directly, but our dependency `wasm-opt-sys` fails to compile on Windows when using a newer version. +# This can be removed when https://github.com/rust-lang/cc-rs/issues/1324 is fixed. +cc = "=1.2.2" diff --git a/src/build/args.rs b/src/build/args.rs index a769862..cbd83f9 100644 --- a/src/build/args.rs +++ b/src/build/args.rs @@ -31,7 +31,7 @@ impl BuildArgs { /// The profile used to compile the app. pub(crate) fn profile(&self) -> &str { - self.cargo_args.compilation_args.profile() + self.cargo_args.compilation_args.profile(self.is_web()) } /// The targeted platform. diff --git a/src/build/mod.rs b/src/build/mod.rs index 83e93ba..201bd4e 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -3,7 +3,10 @@ use args::BuildSubcommands; use crate::{ external_cli::{cargo, rustup, wasm_bindgen, CommandHelpers}, run::select_run_binary, - web::bundle::{create_web_bundle, PackedBundle, WebBundle}, + web::{ + bundle::{create_web_bundle, PackedBundle, WebBundle}, + profiles::configure_default_web_profiles, + }, }; pub use self::args::BuildArgs; @@ -11,17 +14,12 @@ pub use self::args::BuildArgs; mod args; pub fn build(args: &BuildArgs) -> anyhow::Result<()> { - let cargo_args = args.cargo_args_builder(); + let mut cargo_args = args.cargo_args_builder(); if let Some(BuildSubcommands::Web(web_args)) = &args.subcommand { ensure_web_setup(args.skip_prompts)?; let metadata = cargo::metadata::metadata_with_args(["--no-deps"])?; - - println!("Compiling to WebAssembly..."); - cargo::build::command().args(cargo_args).ensure_status()?; - - println!("Bundling JavaScript bindings..."); let bin_target = select_run_binary( &metadata, args.cargo_args.package_args.package.as_deref(), @@ -30,6 +28,13 @@ pub fn build(args: &BuildArgs) -> anyhow::Result<()> { args.target().as_deref(), args.profile(), )?; + + cargo_args = cargo_args.append(configure_default_web_profiles(&metadata)?); + + println!("Compiling to WebAssembly..."); + cargo::build::command().args(cargo_args).ensure_status()?; + + println!("Bundling JavaScript bindings..."); wasm_bindgen::bundle(&bin_target)?; #[cfg(feature = "wasm-opt")] diff --git a/src/external_cli/cargo/metadata.rs b/src/external_cli/cargo/metadata.rs index 9b800e6..e53559a 100644 --- a/src/external_cli/cargo/metadata.rs +++ b/src/external_cli/cargo/metadata.rs @@ -45,15 +45,16 @@ pub struct Metadata { /// List of members of the workspace. /// /// Each entry is the Package ID for the package. - pub workspace_members: Option>, + pub workspace_members: Vec, /// List of default members of the workspace. /// /// Each entry is the Package ID for the package. - pub workspace_default_members: Option>, + pub workspace_default_members: Vec, /// The absolute path to the build directory where Cargo places its output. pub target_directory: PathBuf, /// The absolute path to the root of the workspace. - pub workspace_root: Option, + /// This will be the root of the package if no workspace is used. + pub workspace_root: PathBuf, } #[derive(Debug, Deserialize)] diff --git a/src/external_cli/cargo/mod.rs b/src/external_cli/cargo/mod.rs index a128864..1b0b2b6 100644 --- a/src/external_cli/cargo/mod.rs +++ b/src/external_cli/cargo/mod.rs @@ -70,11 +70,17 @@ impl CargoCompilationArgs { /// The profile used to compile the app. /// /// This is determined by the `--release` and `--profile` arguments. - pub(crate) fn profile(&self) -> &str { - if self.is_release { - "release" - } else if let Some(profile) = &self.profile { + pub(crate) fn profile(&self, is_web: bool) -> &str { + if let Some(profile) = &self.profile { profile + } else if is_web { + if self.is_release { + "web-release" + } else { + "web" + } + } else if self.is_release { + "release" } else { "debug" } @@ -97,8 +103,7 @@ impl CargoCompilationArgs { }; ArgBuilder::new() - .add_flag_if("--release", self.is_release) - .add_opt_value("--profile", &self.profile) + .add_with_value("--profile", self.profile(is_web)) .add_opt_value("--jobs", &self.jobs.map(|jobs| jobs.to_string())) .add_flag_if("--keep-going", self.is_keep_going) .add_opt_value("--target", &target) diff --git a/src/run/args.rs b/src/run/args.rs index ac0d935..e2ea24d 100644 --- a/src/run/args.rs +++ b/src/run/args.rs @@ -31,7 +31,7 @@ impl RunArgs { /// The profile used to compile the app. pub(crate) fn profile(&self) -> &str { - self.cargo_args.compilation_args.profile() + self.cargo_args.compilation_args.profile(self.is_web()) } /// The targeted platform. diff --git a/src/run/mod.rs b/src/run/mod.rs index cc10711..30bc5cd 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -9,7 +9,10 @@ use crate::{ cargo::{self, metadata::Metadata}, wasm_bindgen, CommandHelpers, }, - web::bundle::{create_web_bundle, PackedBundle, WebBundle}, + web::{ + bundle::{create_web_bundle, PackedBundle, WebBundle}, + profiles::configure_default_web_profiles, + }, }; pub use self::args::RunArgs; @@ -18,18 +21,12 @@ mod args; mod serve; pub fn run(args: &RunArgs) -> anyhow::Result<()> { - let cargo_args = args.cargo_args_builder(); + let mut cargo_args = args.cargo_args_builder(); if let Some(RunSubcommands::Web(web_args)) = &args.subcommand { ensure_web_setup(args.skip_prompts)?; let metadata = cargo::metadata::metadata_with_args(["--no-deps"])?; - - // If targeting the web, run a web server with the WASM build - println!("Compiling to WebAssembly..."); - cargo::build::command().args(cargo_args).ensure_status()?; - - println!("Bundling JavaScript bindings..."); let bin_target = select_run_binary( &metadata, args.cargo_args.package_args.package.as_deref(), @@ -38,6 +35,14 @@ pub fn run(args: &RunArgs) -> anyhow::Result<()> { args.target().as_deref(), args.profile(), )?; + + cargo_args = cargo_args.append(configure_default_web_profiles(&metadata)?); + + // If targeting the web, run a web server with the WASM build + println!("Compiling to WebAssembly..."); + cargo::build::command().args(cargo_args).ensure_status()?; + + println!("Bundling JavaScript bindings..."); wasm_bindgen::bundle(&bin_target)?; #[cfg(feature = "wasm-opt")] diff --git a/src/web/mod.rs b/src/web/mod.rs index d4e3500..a374dc4 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -1,5 +1,6 @@ //! Utilities for building and running the app in the browser. pub(crate) mod bundle; +pub(crate) mod profiles; #[cfg(feature = "wasm-opt")] pub(crate) mod wasm_opt; diff --git a/src/web/profiles.rs b/src/web/profiles.rs new file mode 100644 index 0000000..dc68be0 --- /dev/null +++ b/src/web/profiles.rs @@ -0,0 +1,104 @@ +use std::{collections::HashMap, fs}; + +use anyhow::Context as _; +use toml_edit::DocumentMut; + +use crate::external_cli::{arg_builder::ArgBuilder, cargo::metadata::Metadata}; + +/// Create `--config` args to configure the default profiles to use when compiling for the web. +pub(crate) fn configure_default_web_profiles(metadata: &Metadata) -> anyhow::Result { + let manifest = fs::read_to_string(metadata.workspace_root.join("Cargo.toml")) + .context("failed to read workspace manifest")? + .parse::() + .context("failed to parse workspace manifest")?; + + let mut args = ArgBuilder::new(); + + if !is_profile_defined_in_manifest(&manifest, "web") { + args = args.append(configure_web_profile()); + } + + if !is_profile_defined_in_manifest(&manifest, "web-release") { + args = args.append(configure_web_release_profile()); + } + + Ok(args) +} + +fn is_profile_defined_in_manifest(manifest: &DocumentMut, profile: &str) -> bool { + manifest + .get("profile") + .is_some_and(|profiles| profiles.get(profile).is_some()) +} + +/// Configure the default profile for web debug builds. +/// +/// It is optimized for fast iteration speeds. +fn configure_web_profile() -> ArgBuilder { + configure_profile("web", "dev", HashMap::new()) +} + +/// Configure the default profile for web release builds. +/// +/// It is optimized both for run time performance and loading times. +fn configure_web_release_profile() -> ArgBuilder { + let config = HashMap::from_iter([ + // Optimize for size, greatly reducing loading times + ("opt-level", "s"), + // Remove debug information, reducing file size further + ("strip", "debuginfo"), + ]); + configure_profile("web-release", "release", config) +} + +/// Create `--config` args for `cargo` to configure a new compilation profile. +/// +/// Equivalent to a `Cargo.toml` like this: +/// +/// ```toml +/// [profile.{profile}] +/// inherits = "{inherits}" +/// # config +/// key = "value" +/// ``` +fn configure_profile(profile: &str, inherits: &str, config: HashMap<&str, &str>) -> ArgBuilder { + let mut args = ArgBuilder::new().add_with_value( + "--config", + format!(r#"profile.{profile}.inherits="{inherits}""#), + ); + + for (key, value) in config { + args = args.add_with_value("--config", format!(r#"profile.{profile}.{key}="{value}""#)); + } + + args +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_detect_defined_profile() { + let manifest = r#" + [profile.web] + inherits = "dev" + "# + .parse() + .unwrap(); + + assert!(is_profile_defined_in_manifest(&manifest, "web")); + } + + #[test] + fn should_detect_missing_profile() { + let manifest = r#" + [profile.foo] + inherits = "dev" + "# + .parse() + .unwrap(); + + assert!(!is_profile_defined_in_manifest(&manifest, "web")); + } +}