Hotreloading of for/if/body, formatted strings, literals, component props, nested rsx, light CLI rewrite, cli TUI (#2258)
Hotreload the contents of for loops, if chains, component bodies, props, attributes, and any literals discovered in rsx! Add a TUI renderer to the CLI. Improve the CLI build system to be async and parallel. Refactor RSX to allow partial expansion of expressions. Merge autofmt implementations for consistency. Merge the representation of elements and components under the hood. Add a diagnostics system for rsx for improved error messages. Drop interprocess and move to websockets for communication between the CLI and the server. Assign IDs to nodes and attributes in a stable way to be used in non compiler contexts. Add hotreloading to any body of component/for loop/if chain/etc. --------- Co-authored-by: Evan Almloff <evanalmloff@gmail.com> Co-authored-by: Liam Mitchell <liamkarlmitchell@gmail.com>
This commit is contained in:
181 files changed
+12755
-10806
No files matched your search
@@ -0,0 +1,224 @@
|
||||
use super::web::install_web_build_tooling;
|
||||
use super::BuildRequest;
|
||||
use super::BuildResult;
|
||||
use crate::assets::copy_dir_to;
|
||||
use crate::assets::create_assets_head;
|
||||
use crate::assets::{asset_manifest, process_assets, AssetConfigDropGuard};
|
||||
use crate::builder::progress::build_cargo;
|
||||
use crate::builder::progress::CargoBuildResult;
|
||||
use crate::builder::progress::Stage;
|
||||
use crate::builder::progress::UpdateBuildProgress;
|
||||
use crate::builder::progress::UpdateStage;
|
||||
use crate::link::LinkCommand;
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use manganis_cli_support::ManganisSupportGuard;
|
||||
use std::fs::create_dir_all;
|
||||
|
||||
impl BuildRequest {
|
||||
/// Create a list of arguments for cargo builds
|
||||
pub(crate) fn build_arguments(&self) -> Vec<String> {
|
||||
let mut cargo_args = Vec::new();
|
||||
|
||||
if self.build_arguments.release {
|
||||
cargo_args.push("--release".to_string());
|
||||
}
|
||||
if self.build_arguments.verbose {
|
||||
cargo_args.push("--verbose".to_string());
|
||||
} else {
|
||||
cargo_args.push("--quiet".to_string());
|
||||
}
|
||||
|
||||
if let Some(custom_profile) = &self.build_arguments.profile {
|
||||
cargo_args.push("--profile".to_string());
|
||||
cargo_args.push(custom_profile.to_string());
|
||||
}
|
||||
|
||||
if !self.build_arguments.target_args.features.is_empty() {
|
||||
let features_str = self.build_arguments.target_args.features.join(" ");
|
||||
cargo_args.push("--features".to_string());
|
||||
cargo_args.push(features_str);
|
||||
}
|
||||
|
||||
if let Some(target) = self.web.then_some("wasm32-unknown-unknown").or(self
|
||||
.build_arguments
|
||||
.target_args
|
||||
.target
|
||||
.as_deref())
|
||||
{
|
||||
cargo_args.push("--target".to_string());
|
||||
cargo_args.push(target.to_string());
|
||||
}
|
||||
|
||||
cargo_args.append(&mut self.build_arguments.cargo_args.clone());
|
||||
|
||||
match self.dioxus_crate.executable_type() {
|
||||
krates::cm::TargetKind::Bin => {
|
||||
cargo_args.push("--bin".to_string());
|
||||
}
|
||||
krates::cm::TargetKind::Lib => {
|
||||
cargo_args.push("--lib".to_string());
|
||||
}
|
||||
krates::cm::TargetKind::Example => {
|
||||
cargo_args.push("--example".to_string());
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
cargo_args.push(self.dioxus_crate.executable_name().to_string());
|
||||
|
||||
cargo_args
|
||||
}
|
||||
|
||||
/// Create a build command for cargo
|
||||
fn prepare_build_command(&self) -> Result<(tokio::process::Command, Vec<String>)> {
|
||||
let mut cmd = tokio::process::Command::new("cargo");
|
||||
cmd.arg("rustc");
|
||||
if let Some(target_dir) = &self.target_dir {
|
||||
cmd.env("CARGO_TARGET_DIR", target_dir);
|
||||
}
|
||||
cmd.current_dir(self.dioxus_crate.crate_dir())
|
||||
.arg("--message-format")
|
||||
.arg("json-diagnostic-rendered-ansi");
|
||||
|
||||
let cargo_args = self.build_arguments();
|
||||
cmd.args(&cargo_args);
|
||||
|
||||
cmd.arg("--").args(self.rust_flags.clone());
|
||||
|
||||
Ok((cmd, cargo_args))
|
||||
}
|
||||
|
||||
pub async fn build(
|
||||
&self,
|
||||
mut progress: UnboundedSender<UpdateBuildProgress>,
|
||||
) -> Result<BuildResult> {
|
||||
tracing::info!("🚅 Running build [Desktop] command...");
|
||||
|
||||
// Set up runtime guards
|
||||
let _guard = dioxus_cli_config::__private::save_config(&self.dioxus_crate.dioxus_config);
|
||||
let _manganis_support = ManganisSupportGuard::default();
|
||||
let _asset_guard = AssetConfigDropGuard::new();
|
||||
|
||||
// If this is a web, build make sure we have the web build tooling set up
|
||||
if self.web {
|
||||
install_web_build_tooling(&mut progress).await?;
|
||||
}
|
||||
|
||||
// Create the build command
|
||||
let (cmd, cargo_args) = self.prepare_build_command()?;
|
||||
|
||||
// Run the build command with a pretty loader
|
||||
let crate_count = self.get_unit_count_estimate().await;
|
||||
let cargo_result = build_cargo(crate_count, cmd, &mut progress).await?;
|
||||
|
||||
// Post process the build result
|
||||
let build_result = self
|
||||
.post_process_build(cargo_args, &cargo_result, &mut progress)
|
||||
.await
|
||||
.context("Failed to post process build")?;
|
||||
|
||||
tracing::info!(
|
||||
"🚩 Build completed: [./{}]",
|
||||
self.dioxus_crate
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.display()
|
||||
);
|
||||
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::Finished,
|
||||
update: UpdateStage::Start,
|
||||
});
|
||||
|
||||
Ok(build_result)
|
||||
}
|
||||
|
||||
async fn post_process_build(
|
||||
&self,
|
||||
cargo_args: Vec<String>,
|
||||
cargo_build_result: &CargoBuildResult,
|
||||
progress: &mut UnboundedSender<UpdateBuildProgress>,
|
||||
) -> Result<BuildResult> {
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::OptimizingAssets,
|
||||
update: UpdateStage::Start,
|
||||
});
|
||||
|
||||
// Start Manganis linker intercept.
|
||||
let linker_args = vec![format!("{}", self.dioxus_crate.out_dir().display())];
|
||||
|
||||
manganis_cli_support::start_linker_intercept(
|
||||
&LinkCommand::command_name(),
|
||||
cargo_args,
|
||||
Some(linker_args),
|
||||
)?;
|
||||
|
||||
let file_name = self.dioxus_crate.executable_name();
|
||||
|
||||
// Move the final output executable into the dist folder
|
||||
let out_dir = self.dioxus_crate.out_dir();
|
||||
if !out_dir.is_dir() {
|
||||
create_dir_all(&out_dir)?;
|
||||
}
|
||||
let mut output_path = out_dir.join(file_name);
|
||||
if self.web {
|
||||
output_path.set_extension("wasm");
|
||||
} else if cfg!(windows) {
|
||||
output_path.set_extension("exe");
|
||||
}
|
||||
if let Some(res_path) = &cargo_build_result.output_location {
|
||||
std::fs::copy(res_path, &output_path)?;
|
||||
}
|
||||
|
||||
self.copy_assets_dir()?;
|
||||
|
||||
let assets = if !self.build_arguments.skip_assets {
|
||||
let assets = asset_manifest(&self.dioxus_crate);
|
||||
// Collect assets
|
||||
process_assets(&self.dioxus_crate, &assets, progress)?;
|
||||
// Create the __assets_head.html file for bundling
|
||||
create_assets_head(&self.dioxus_crate, &assets)?;
|
||||
Some(assets)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create the build result
|
||||
let build_result = BuildResult {
|
||||
executable: output_path,
|
||||
web: self.web,
|
||||
platform: self
|
||||
.build_arguments
|
||||
.platform
|
||||
.expect("To be resolved by now"),
|
||||
};
|
||||
|
||||
// If this is a web build, run web post processing steps
|
||||
if self.web {
|
||||
self.post_process_web_build(&build_result, assets.as_ref(), progress)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(build_result)
|
||||
}
|
||||
|
||||
pub fn copy_assets_dir(&self) -> anyhow::Result<()> {
|
||||
tracing::info!("Copying public assets to the output directory...");
|
||||
let out_dir = self.dioxus_crate.out_dir();
|
||||
let asset_dir = self.dioxus_crate.asset_dir();
|
||||
|
||||
if asset_dir.is_dir() {
|
||||
// Only pre-compress the assets from the web build. Desktop assets are not served, so they don't need to be pre_compressed
|
||||
let pre_compress = self.web
|
||||
&& self
|
||||
.dioxus_crate
|
||||
.should_pre_compress_web_assets(self.build_arguments.release);
|
||||
|
||||
copy_dir_to(asset_dir, out_dir, pre_compress)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use crate::builder::Build;
|
||||
use crate::dioxus_crate::DioxusCrate;
|
||||
use dioxus_cli_config::Platform;
|
||||
|
||||
use crate::builder::BuildRequest;
|
||||
use std::path::PathBuf;
|
||||
|
||||
static CLIENT_RUST_FLAGS: &[&str] = &["-Cdebuginfo=none", "-Cstrip=debuginfo"];
|
||||
// The `opt-level=2` increases build times, but can noticeably decrease time
|
||||
// between saving changes and being able to interact with an app. The "overall"
|
||||
// time difference (between having and not having the optimization) can be
|
||||
// almost imperceptible (~1 s) but also can be very noticeable (~6 s) — depends
|
||||
// on setup (hardware, OS, browser, idle load).
|
||||
static SERVER_RUST_FLAGS: &[&str] = &["-O"];
|
||||
static DEBUG_RUST_FLAG: &str = "-Cdebug-assertions";
|
||||
|
||||
fn add_debug_rust_flags(build: &Build, flags: &mut Vec<String>) {
|
||||
if !build.release {
|
||||
flags.push(DEBUG_RUST_FLAG.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn fullstack_rust_flags(build: &Build, base_flags: &[&str]) -> Vec<String> {
|
||||
// If we are forcing debug mode, don't add any debug flags
|
||||
if build.force_debug {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
let mut rust_flags = base_flags.iter().map(ToString::to_string).collect();
|
||||
add_debug_rust_flags(build, &mut rust_flags);
|
||||
rust_flags
|
||||
}
|
||||
|
||||
// Fullstack builds run the server and client builds parallel by default
|
||||
// To make them run in parallel, we need to set up different target directories for the server and client within /.dioxus
|
||||
fn get_target_directory(build: &Build, target: PathBuf) -> Option<PathBuf> {
|
||||
(!build.force_sequential).then_some(target)
|
||||
}
|
||||
|
||||
impl BuildRequest {
|
||||
pub(crate) fn new_fullstack(
|
||||
config: DioxusCrate,
|
||||
build_arguments: Build,
|
||||
serve: bool,
|
||||
) -> Vec<Self> {
|
||||
vec![
|
||||
Self::new_server(serve, &config, &build_arguments),
|
||||
Self::new_client(serve, &config, &build_arguments),
|
||||
]
|
||||
}
|
||||
|
||||
fn new_with_target_directory_rust_flags_and_features(
|
||||
serve: bool,
|
||||
config: &DioxusCrate,
|
||||
build: &Build,
|
||||
target_directory: PathBuf,
|
||||
rust_flags: &[&str],
|
||||
feature: String,
|
||||
web: bool,
|
||||
) -> Self {
|
||||
let config = config.clone();
|
||||
let mut build = build.clone();
|
||||
build.platform = Some(if web {
|
||||
Platform::Web
|
||||
} else {
|
||||
Platform::Desktop
|
||||
});
|
||||
// Set the target directory we are building the server in
|
||||
let target_dir = get_target_directory(&build, target_directory);
|
||||
// Add the server feature to the features we pass to the build
|
||||
build.target_args.features.push(feature);
|
||||
|
||||
// Add the server flags to the build arguments
|
||||
let rust_flags = fullstack_rust_flags(&build, rust_flags);
|
||||
|
||||
Self {
|
||||
web,
|
||||
serve,
|
||||
build_arguments: build.clone(),
|
||||
dioxus_crate: config,
|
||||
rust_flags,
|
||||
target_dir,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_server(serve: bool, config: &DioxusCrate, build: &Build) -> Self {
|
||||
Self::new_with_target_directory_rust_flags_and_features(
|
||||
serve,
|
||||
config,
|
||||
build,
|
||||
config.server_target_dir(),
|
||||
SERVER_RUST_FLAGS,
|
||||
build.target_args.server_feature.clone(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn new_client(serve: bool, config: &DioxusCrate, build: &Build) -> Self {
|
||||
Self::new_with_target_directory_rust_flags_and_features(
|
||||
serve,
|
||||
config,
|
||||
build,
|
||||
config.client_target_dir(),
|
||||
CLIENT_RUST_FLAGS,
|
||||
build.target_args.client_feature.clone(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
use crate::build::Build;
|
||||
use crate::cli::serve::ServeArguments;
|
||||
use crate::dioxus_crate::DioxusCrate;
|
||||
use crate::Result;
|
||||
use dioxus_cli_config::{Platform, RuntimeCLIArguments};
|
||||
use futures_util::stream::select_all;
|
||||
use futures_util::StreamExt;
|
||||
use std::net::SocketAddr;
|
||||
use std::{path::PathBuf, process::Stdio};
|
||||
use tokio::process::{Child, Command};
|
||||
|
||||
mod cargo;
|
||||
mod fullstack;
|
||||
mod prepare_html;
|
||||
mod progress;
|
||||
mod web;
|
||||
pub use progress::{BuildMessage, MessageType, Stage, UpdateBuildProgress, UpdateStage};
|
||||
|
||||
/// A request for a project to be built
|
||||
pub struct BuildRequest {
|
||||
/// Whether the build is for serving the application
|
||||
pub serve: bool,
|
||||
/// Whether this is a web build
|
||||
pub web: bool,
|
||||
/// The configuration for the crate we are building
|
||||
pub dioxus_crate: DioxusCrate,
|
||||
/// The arguments for the build
|
||||
pub build_arguments: Build,
|
||||
/// The rustc flags to pass to the build
|
||||
pub rust_flags: Vec<String>,
|
||||
/// The target directory for the build
|
||||
pub target_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl BuildRequest {
|
||||
pub fn create(
|
||||
serve: bool,
|
||||
dioxus_crate: &DioxusCrate,
|
||||
build_arguments: impl Into<Build>,
|
||||
) -> Vec<Self> {
|
||||
let build_arguments = build_arguments.into();
|
||||
let dioxus_crate = dioxus_crate.clone();
|
||||
let platform = build_arguments.platform();
|
||||
match platform {
|
||||
Platform::Web | Platform::Desktop => {
|
||||
let web = platform == Platform::Web;
|
||||
vec![Self {
|
||||
serve,
|
||||
web,
|
||||
dioxus_crate,
|
||||
build_arguments,
|
||||
rust_flags: Default::default(),
|
||||
target_dir: Default::default(),
|
||||
}]
|
||||
}
|
||||
Platform::StaticGeneration | Platform::Fullstack => {
|
||||
Self::new_fullstack(dioxus_crate, build_arguments, serve)
|
||||
}
|
||||
_ => unimplemented!("Unknown platform: {platform:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_all_parallel(build_requests: Vec<BuildRequest>) -> Result<Vec<BuildResult>> {
|
||||
let multi_platform_build = build_requests.len() > 1;
|
||||
let mut build_progress = Vec::new();
|
||||
let mut set = tokio::task::JoinSet::new();
|
||||
for build_request in build_requests {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
build_progress.push((build_request.build_arguments.platform(), rx));
|
||||
set.spawn(async move { build_request.build(tx).await });
|
||||
}
|
||||
|
||||
// Watch the build progress as it comes in
|
||||
loop {
|
||||
let mut next = select_all(
|
||||
build_progress
|
||||
.iter_mut()
|
||||
.map(|(platform, rx)| rx.map(move |update| (*platform, update))),
|
||||
);
|
||||
match next.next().await {
|
||||
Some((platform, update)) => {
|
||||
if multi_platform_build {
|
||||
print!("{platform} build: ");
|
||||
update.to_std_out();
|
||||
} else {
|
||||
update.to_std_out();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut all_results = Vec::new();
|
||||
|
||||
while let Some(result) = set.join_next().await {
|
||||
let result = result
|
||||
.map_err(|_| crate::Error::Unique("Failed to build project".to_owned()))??;
|
||||
all_results.push(result);
|
||||
}
|
||||
|
||||
Ok(all_results)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct BuildResult {
|
||||
pub executable: PathBuf,
|
||||
pub web: bool,
|
||||
pub platform: Platform,
|
||||
}
|
||||
|
||||
impl BuildResult {
|
||||
/// Open the executable if this is a native build
|
||||
pub fn open(
|
||||
&self,
|
||||
serve: &ServeArguments,
|
||||
fullstack_address: Option<SocketAddr>,
|
||||
) -> std::io::Result<Option<Child>> {
|
||||
if self.web {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let arguments = RuntimeCLIArguments::new(serve.address.address(), fullstack_address);
|
||||
let executable = self.executable.canonicalize()?;
|
||||
// This is the /dist folder generally
|
||||
let output_folder = executable.parent().unwrap();
|
||||
// This is the workspace folder
|
||||
let workspace_folder = output_folder.parent().unwrap();
|
||||
Ok(Some(
|
||||
Command::new(&executable)
|
||||
// When building the fullstack server, we need to forward the serve arguments (like port) to the fullstack server through env vars
|
||||
.env(
|
||||
dioxus_cli_config::__private::SERVE_ENV,
|
||||
serde_json::to_string(&arguments).unwrap(),
|
||||
)
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.current_dir(workspace_folder)
|
||||
.spawn()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! Build the HTML file to load a web application. The index.html file may be created from scratch or modified from the `index.html` file in the crate root.
|
||||
|
||||
use super::BuildRequest;
|
||||
use crate::Result;
|
||||
use manganis_cli_support::AssetManifest;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
const DEFAULT_HTML: &str = include_str!("../../assets/index.html");
|
||||
|
||||
impl BuildRequest {
|
||||
pub(crate) fn prepare_html(&self, assets: Option<&AssetManifest>) -> Result<String> {
|
||||
let mut html = html_or_default(&self.dioxus_crate.crate_dir());
|
||||
|
||||
// Inject any resources from the config into the html
|
||||
self.inject_resources(&mut html, assets)?;
|
||||
|
||||
// Inject loading scripts if they are not already present
|
||||
self.inject_loading_scripts(&mut html);
|
||||
|
||||
// Replace any special placeholders in the HTML with resolved values
|
||||
self.replace_template_placeholders(&mut html);
|
||||
|
||||
let title = self.dioxus_crate.dioxus_config.web.app.title.clone();
|
||||
|
||||
replace_or_insert_before("{app_title}", "</title", &title, &mut html);
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
// Inject any resources from the config into the html
|
||||
fn inject_resources(&self, html: &mut String, assets: Option<&AssetManifest>) -> Result<()> {
|
||||
// Collect all resources into a list of styles and scripts
|
||||
let resources = &self.dioxus_crate.dioxus_config.web.resource;
|
||||
let mut style_list = resources.style.clone().unwrap_or_default();
|
||||
let mut script_list = resources.script.clone().unwrap_or_default();
|
||||
|
||||
if self.serve {
|
||||
style_list.extend(resources.dev.style.iter().cloned());
|
||||
script_list.extend(resources.dev.script.iter().cloned());
|
||||
}
|
||||
|
||||
let mut head_resources = String::new();
|
||||
// Add all styles to the head
|
||||
for style in style_list {
|
||||
writeln!(
|
||||
&mut head_resources,
|
||||
"<link rel=\"stylesheet\" href=\"{}\">",
|
||||
&style.to_str().unwrap(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Add all scripts to the head
|
||||
for script in script_list {
|
||||
writeln!(
|
||||
&mut head_resources,
|
||||
"<script src=\"{}\"></script>",
|
||||
&script.to_str().unwrap(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Inject any resources from manganis into the head
|
||||
if let Some(assets) = assets {
|
||||
head_resources.push_str(&assets.head());
|
||||
}
|
||||
|
||||
replace_or_insert_before("{style_include}", "</head", &head_resources, html);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inject loading scripts if they are not already present
|
||||
fn inject_loading_scripts(&self, html: &mut String) {
|
||||
// If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
|
||||
if !self.build_arguments.inject_loading_scripts || html.contains("__wbindgen_start") {
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, insert the script
|
||||
*html = html.replace(
|
||||
"</body",
|
||||
r#"<script>
|
||||
// We can't use a module script here because we need to start the script immediately when streaming
|
||||
import("/{base_path}/assets/dioxus/{app_name}.js").then(
|
||||
({ default: init }) => {
|
||||
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then((wasm) => {
|
||||
if (wasm.__wbindgen_start == undefined) {
|
||||
wasm.main();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
</script></body"#,
|
||||
);
|
||||
|
||||
// And try to insert preload links for the wasm and js files
|
||||
*html = html.replace(
|
||||
"</head",
|
||||
r#"<link rel="preload" href="/{base_path}/assets/dioxus/{app_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<link rel="preload" href="/{base_path}/assets/dioxus/{app_name}.js" as="script">
|
||||
</head"#);
|
||||
}
|
||||
|
||||
/// Replace any special placeholders in the HTML with resolved values
|
||||
fn replace_template_placeholders(&self, html: &mut String) {
|
||||
let base_path = self.dioxus_crate.dioxus_config.web.app.base_path();
|
||||
*html = html.replace("{base_path}", base_path);
|
||||
|
||||
let app_name = &self.dioxus_crate.dioxus_config.application.name;
|
||||
*html = html.replace("{app_name}", app_name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the html file from the crate root or use the default html file
|
||||
fn html_or_default(crate_root: &Path) -> String {
|
||||
let custom_html_file = crate_root.join("index.html");
|
||||
std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(DEFAULT_HTML))
|
||||
}
|
||||
|
||||
/// Replace a string or insert the new contents before a marker
|
||||
fn replace_or_insert_before(
|
||||
replace: &str,
|
||||
or_insert_before: &str,
|
||||
with: &str,
|
||||
content: &mut String,
|
||||
) {
|
||||
if content.contains(replace) {
|
||||
*content = content.replace(replace, with);
|
||||
} else if let Some(pos) = content.find(or_insert_before) {
|
||||
content.insert_str(pos, with);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
//! Report progress about the build to the user. We use channels to report progress back to the CLI.
|
||||
use anyhow::Context;
|
||||
use cargo_metadata::{diagnostic::Diagnostic, Message};
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tracing::Level;
|
||||
|
||||
use super::BuildRequest;
|
||||
|
||||
#[derive(Default, Debug, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub enum Stage {
|
||||
#[default]
|
||||
Initializing = 0,
|
||||
InstallingWasmTooling = 1,
|
||||
Compiling = 2,
|
||||
OptimizingWasm = 3,
|
||||
OptimizingAssets = 4,
|
||||
Finished = 5,
|
||||
}
|
||||
|
||||
impl Deref for Stage {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Stage::Initializing => "Initializing",
|
||||
Stage::InstallingWasmTooling => "Installing Wasm Tooling",
|
||||
Stage::Compiling => "Compiling",
|
||||
Stage::OptimizingWasm => "Optimizing Wasm",
|
||||
Stage::OptimizingAssets => "Optimizing Assets",
|
||||
Stage::Finished => "Finished",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Stage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.deref())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateBuildProgress {
|
||||
pub stage: Stage,
|
||||
pub update: UpdateStage,
|
||||
}
|
||||
|
||||
impl UpdateBuildProgress {
|
||||
pub fn to_std_out(&self) {
|
||||
match &self.update {
|
||||
UpdateStage::Start => println!("--- {} ---", self.stage),
|
||||
UpdateStage::AddMessage(message) => match &message.message {
|
||||
MessageType::Cargo(message) => {
|
||||
println!("{}", message.rendered.clone().unwrap_or_default());
|
||||
}
|
||||
MessageType::Text(message) => {
|
||||
println!("{}", message);
|
||||
}
|
||||
},
|
||||
UpdateStage::SetProgress(progress) => {
|
||||
println!("Build progress {:0.0}%", progress * 100.0);
|
||||
}
|
||||
UpdateStage::Failed(message) => {
|
||||
println!("Build failed: {}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum UpdateStage {
|
||||
Start,
|
||||
AddMessage(BuildMessage),
|
||||
SetProgress(f64),
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BuildMessage {
|
||||
pub level: Level,
|
||||
pub message: MessageType,
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MessageType {
|
||||
Cargo(Diagnostic),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl From<Diagnostic> for BuildMessage {
|
||||
fn from(message: Diagnostic) -> Self {
|
||||
Self {
|
||||
level: match message.level {
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Ice
|
||||
| cargo_metadata::diagnostic::DiagnosticLevel::FailureNote
|
||||
| cargo_metadata::diagnostic::DiagnosticLevel::Error => Level::ERROR,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Warning => Level::WARN,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Note => Level::INFO,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Help => Level::DEBUG,
|
||||
_ => Level::DEBUG,
|
||||
},
|
||||
source: Some("cargo".to_string()),
|
||||
message: MessageType::Cargo(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_cargo(
|
||||
crate_count: usize,
|
||||
mut cmd: tokio::process::Command,
|
||||
progress: &mut UnboundedSender<UpdateBuildProgress>,
|
||||
) -> anyhow::Result<CargoBuildResult> {
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::Compiling,
|
||||
update: UpdateStage::Start,
|
||||
});
|
||||
|
||||
let mut child = cmd
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn cargo build")?;
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
let stdout = tokio::io::BufReader::new(stdout);
|
||||
let stderr = tokio::io::BufReader::new(stderr);
|
||||
let mut output_location = None;
|
||||
|
||||
let mut stdout = stdout.lines();
|
||||
let mut stderr = stderr.lines();
|
||||
let mut units_compiled = 0;
|
||||
let mut errors = Vec::new();
|
||||
loop {
|
||||
let line = tokio::select! {
|
||||
line = stdout.next_line() => {
|
||||
line
|
||||
}
|
||||
line = stderr.next_line() => {
|
||||
line
|
||||
}
|
||||
};
|
||||
let Some(line) = line? else {
|
||||
break;
|
||||
};
|
||||
let mut deserializer = serde_json::Deserializer::from_str(line.trim());
|
||||
deserializer.disable_recursion_limit();
|
||||
|
||||
let message = Message::deserialize(&mut deserializer).unwrap_or(Message::TextLine(line));
|
||||
match message {
|
||||
Message::CompilerMessage(msg) => {
|
||||
let message = msg.message;
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::Compiling,
|
||||
update: UpdateStage::AddMessage(message.clone().into()),
|
||||
});
|
||||
const WARNING_LEVELS: &[cargo_metadata::diagnostic::DiagnosticLevel] = &[
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Help,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Note,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Warning,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Error,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::FailureNote,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Ice,
|
||||
];
|
||||
const FATAL_LEVELS: &[cargo_metadata::diagnostic::DiagnosticLevel] = &[
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Error,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::FailureNote,
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Ice,
|
||||
];
|
||||
if WARNING_LEVELS.contains(&message.level) {
|
||||
if let Some(rendered) = message.rendered {
|
||||
errors.push(rendered);
|
||||
}
|
||||
}
|
||||
if FATAL_LEVELS.contains(&message.level) {
|
||||
return Err(anyhow::anyhow!(errors.join("\n")));
|
||||
}
|
||||
}
|
||||
Message::CompilerArtifact(artifact) => {
|
||||
units_compiled += 1;
|
||||
if let Some(executable) = artifact.executable {
|
||||
output_location = Some(executable.into());
|
||||
} else {
|
||||
let build_progress = units_compiled as f64 / crate_count as f64;
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::Compiling,
|
||||
update: UpdateStage::SetProgress((build_progress).clamp(0.0, 1.00)),
|
||||
});
|
||||
}
|
||||
}
|
||||
Message::BuildScriptExecuted(_) => {
|
||||
units_compiled += 1;
|
||||
}
|
||||
Message::BuildFinished(finished) => {
|
||||
if !finished.success {
|
||||
return Err(anyhow::anyhow!("Build failed"));
|
||||
}
|
||||
}
|
||||
Message::TextLine(line) => {
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::Compiling,
|
||||
update: UpdateStage::AddMessage(BuildMessage {
|
||||
level: Level::DEBUG,
|
||||
message: MessageType::Text(line),
|
||||
source: None,
|
||||
}),
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Unknown message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CargoBuildResult { output_location })
|
||||
}
|
||||
|
||||
pub(crate) struct CargoBuildResult {
|
||||
pub(crate) output_location: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl BuildRequest {
|
||||
/// Try to get the unit graph for the crate. This is a nightly only feature which may not be available with the current version of rustc the user has installed.
|
||||
async fn get_unit_count(&self) -> Option<usize> {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UnitGraph {
|
||||
units: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
let mut cmd = tokio::process::Command::new("cargo");
|
||||
cmd.arg("+nightly");
|
||||
cmd.arg("build");
|
||||
cmd.arg("--unit-graph");
|
||||
cmd.arg("-Z").arg("unstable-options");
|
||||
|
||||
cmd.args(self.build_arguments());
|
||||
|
||||
let output = cmd
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let output_text = String::from_utf8(output.stdout).ok()?;
|
||||
let graph: UnitGraph = serde_json::from_str(&output_text).ok()?;
|
||||
|
||||
Some(graph.units.len())
|
||||
}
|
||||
|
||||
/// Get an estimate of the number of units in the crate. If nightly rustc is not available, this will return an estimate of the number of units in the crate based on cargo metadata.
|
||||
/// TODO: always use https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph once it is stable
|
||||
pub(crate) async fn get_unit_count_estimate(&self) -> usize {
|
||||
// Try to get it from nightly
|
||||
self.get_unit_count().await.unwrap_or_else(|| {
|
||||
// Otherwise, use cargo metadata
|
||||
(self
|
||||
.dioxus_crate
|
||||
.krates
|
||||
.krates_filtered(krates::DepKind::Dev)
|
||||
.iter()
|
||||
.map(|k| k.targets.len())
|
||||
.sum::<usize>() as f64
|
||||
/ 3.5) as usize
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use super::BuildRequest;
|
||||
use super::BuildResult;
|
||||
use crate::assets::pre_compress_folder;
|
||||
use crate::builder::progress::Stage;
|
||||
use crate::builder::progress::UpdateBuildProgress;
|
||||
use crate::builder::progress::UpdateStage;
|
||||
use crate::error::{Error, Result};
|
||||
use dioxus_cli_config::WasmOptLevel;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use manganis_cli_support::AssetManifest;
|
||||
use std::path::Path;
|
||||
use tokio::process::Command;
|
||||
use wasm_bindgen_cli_support::Bindgen;
|
||||
|
||||
// Attempt to automatically recover from a bindgen failure by updating the wasm-bindgen version
|
||||
async fn update_wasm_bindgen_version() -> Result<()> {
|
||||
let cli_bindgen_version = wasm_bindgen_shared::version();
|
||||
tracing::info!("Attempting to recover from bindgen failure by setting the wasm-bindgen version to {cli_bindgen_version}...");
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.args([
|
||||
"update",
|
||||
"-p",
|
||||
"wasm-bindgen",
|
||||
"--precise",
|
||||
&cli_bindgen_version,
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
let mut error_message = None;
|
||||
if let Ok(output) = output {
|
||||
if output.status.success() {
|
||||
tracing::info!("Successfully updated wasm-bindgen to {cli_bindgen_version}");
|
||||
return Ok(());
|
||||
} else {
|
||||
error_message = Some(output);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output) = error_message {
|
||||
tracing::error!("Failed to update wasm-bindgen: {:#?}", output);
|
||||
}
|
||||
|
||||
Err(Error::BuildFailed(format!("WASM bindgen build failed!\nThis is probably due to the Bindgen version, dioxus-cli is using `{cli_bindgen_version}` which is not compatible with your crate.\nPlease reinstall the dioxus cli to fix this issue.\nYou can reinstall the dioxus cli by running `cargo install dioxus-cli --force` and then rebuild your project")))
|
||||
}
|
||||
|
||||
/// Check if the wasm32-unknown-unknown target is installed and try to install it if not
|
||||
pub(crate) async fn install_web_build_tooling(
|
||||
progress: &mut UnboundedSender<UpdateBuildProgress>,
|
||||
) -> Result<()> {
|
||||
// If the user has rustup, we can check if the wasm32-unknown-unknown target is installed
|
||||
// Otherwise we can just assume it is installed - which is not great...
|
||||
// Eventually we can poke at the errors and let the user know they need to install the target
|
||||
if let Ok(wasm_check_command) = Command::new("rustup").args(["show"]).output().await {
|
||||
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
|
||||
if !wasm_check_output.contains("wasm32-unknown-unknown") {
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::InstallingWasmTooling,
|
||||
update: UpdateStage::Start,
|
||||
});
|
||||
tracing::info!("wasm32-unknown-unknown target not detected, installing..");
|
||||
let _ = Command::new("rustup")
|
||||
.args(["target", "add", "wasm32-unknown-unknown"])
|
||||
.output()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl BuildRequest {
|
||||
async fn run_wasm_bindgen(&self, input_path: &Path, bindgen_outdir: &Path) -> Result<()> {
|
||||
tracing::info!("Running wasm-bindgen");
|
||||
let input_path = input_path.to_path_buf();
|
||||
let bindgen_outdir = bindgen_outdir.to_path_buf();
|
||||
let keep_debug =
|
||||
self.dioxus_crate.dioxus_config.web.wasm_opt.debug || (!self.build_arguments.release);
|
||||
let name = self.dioxus_crate.dioxus_config.application.name.clone();
|
||||
let run_wasm_bindgen = move || {
|
||||
// [3] Bindgen the final binary for use easy linking
|
||||
let mut bindgen_builder = Bindgen::new();
|
||||
|
||||
bindgen_builder
|
||||
.input_path(&input_path)
|
||||
.web(true)
|
||||
.unwrap()
|
||||
.debug(keep_debug)
|
||||
.demangle(keep_debug)
|
||||
.keep_debug(keep_debug)
|
||||
.reference_types(true)
|
||||
.remove_name_section(!keep_debug)
|
||||
.remove_producers_section(!keep_debug)
|
||||
.out_name(&name)
|
||||
.generate(&bindgen_outdir)
|
||||
.unwrap();
|
||||
};
|
||||
let bindgen_result = tokio::task::spawn_blocking(run_wasm_bindgen.clone()).await;
|
||||
|
||||
// WASM bindgen requires the exact version of the bindgen schema to match the version the CLI was built with
|
||||
// If we get an error, we can try to recover by pinning the user's wasm-bindgen version to the version we used
|
||||
if let Err(err) = bindgen_result {
|
||||
tracing::error!("Bindgen build failed: {:?}", err);
|
||||
update_wasm_bindgen_version().await?;
|
||||
run_wasm_bindgen();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Post process the WASM build artifacts
|
||||
pub(crate) async fn post_process_web_build(
|
||||
&self,
|
||||
build_result: &BuildResult,
|
||||
assets: Option<&AssetManifest>,
|
||||
progress: &mut UnboundedSender<UpdateBuildProgress>,
|
||||
) -> Result<()> {
|
||||
_ = progress.start_send(UpdateBuildProgress {
|
||||
stage: Stage::OptimizingWasm,
|
||||
update: UpdateStage::Start,
|
||||
});
|
||||
|
||||
// Create the index.html file
|
||||
let html = self.prepare_html(assets)?;
|
||||
let html_path = self.dioxus_crate.out_dir().join("index.html");
|
||||
std::fs::write(&html_path, html)?;
|
||||
|
||||
// Find the wasm file
|
||||
let output_location = build_result.executable.clone();
|
||||
let input_path = output_location.with_extension("wasm");
|
||||
|
||||
// Create the directory where the bindgen output will be placed
|
||||
let bindgen_outdir = self.dioxus_crate.out_dir().join("assets").join("dioxus");
|
||||
|
||||
// Run wasm-bindgen
|
||||
self.run_wasm_bindgen(&input_path, &bindgen_outdir).await?;
|
||||
|
||||
// Run wasm-opt if this is a release build
|
||||
if self.build_arguments.release {
|
||||
tracing::info!("Running optimization with wasm-opt...");
|
||||
let mut options = match self.dioxus_crate.dioxus_config.web.wasm_opt.level {
|
||||
WasmOptLevel::Z => {
|
||||
wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively()
|
||||
}
|
||||
WasmOptLevel::S => wasm_opt::OptimizationOptions::new_optimize_for_size(),
|
||||
WasmOptLevel::Zero => wasm_opt::OptimizationOptions::new_opt_level_0(),
|
||||
WasmOptLevel::One => wasm_opt::OptimizationOptions::new_opt_level_1(),
|
||||
WasmOptLevel::Two => wasm_opt::OptimizationOptions::new_opt_level_2(),
|
||||
WasmOptLevel::Three => wasm_opt::OptimizationOptions::new_opt_level_3(),
|
||||
WasmOptLevel::Four => wasm_opt::OptimizationOptions::new_opt_level_4(),
|
||||
};
|
||||
let wasm_file = bindgen_outdir.join(format!(
|
||||
"{}_bg.wasm",
|
||||
self.dioxus_crate.dioxus_config.application.name
|
||||
));
|
||||
let old_size = wasm_file.metadata()?.len();
|
||||
options
|
||||
// WASM bindgen relies on reference types
|
||||
.enable_feature(wasm_opt::Feature::ReferenceTypes)
|
||||
.debug_info(self.dioxus_crate.dioxus_config.web.wasm_opt.debug)
|
||||
.run(&wasm_file, &wasm_file)
|
||||
.map_err(|err| Error::Other(anyhow::anyhow!(err)))?;
|
||||
let new_size = wasm_file.metadata()?.len();
|
||||
tracing::info!(
|
||||
"wasm-opt reduced WASM size from {} to {} ({:2}%)",
|
||||
old_size,
|
||||
new_size,
|
||||
(new_size as f64 - old_size as f64) / old_size as f64 * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
// If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
|
||||
let pre_compress = self
|
||||
.dioxus_crate
|
||||
.should_pre_compress_web_assets(self.build_arguments.release);
|
||||
tokio::task::spawn_blocking(move || pre_compress_folder(&bindgen_outdir, pre_compress))
|
||||
.await
|
||||
.unwrap()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user