diff --git a/Cargo.toml b/Cargo.toml index 2e4d21c2d..5f5ded9be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,12 @@ license = "MIT/Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + # cli core clap = { version = "3.0.14", features = ["derive"] } thiserror = "1.0.30" wasm-bindgen-cli-support = "0.2.79" + # features log = "0.4.14" fern = { version = "0.6.0", features = ["colored"] } @@ -37,10 +39,13 @@ hyper = "0.14.17" axum = { version = "0.4.5", features = ["ws", "headers"] } tower-http = { version = "0.2.2", features = ["fs", "trace"] } headers = "0.3.7" +# tools download +dirs = "4.0.0" +reqwest = { version = "0.11", features = ["rustls-tls", "stream", "trust-dns"] } +flate2 = "1.0.22" +tar = "0.4.38" tower = "0.4.12" -# hyper = { version = "0.14.11", features = ["full"] } - [[bin]] path = "src/main.rs" name = "dioxus" diff --git a/Dioxus.toml b/Dioxus.toml new file mode 100644 index 000000000..c3e0dd8a0 --- /dev/null +++ b/Dioxus.toml @@ -0,0 +1,45 @@ +[application] + +# dioxus project name +name = "dioxus-cli" + +# default platfrom +# you can also use `dioxus serve/build --platform XXX` to use other platform +# value: web | desktop +default_platform = "desktop" + +# Web `build` & `serve` dist path +out_dir = "dist" + +# resource (static) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "dioxus | ⛺" + +[web.watcher] + +watch_path = ["src"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = [] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] + +[application.tools] + +# use binaryen.wasm-opt for output Wasm file +# binaryen just will trigger in `web` platform +binaryen = { wasm_opt = true } \ No newline at end of file diff --git a/src/assets/autoreload.js b/src/assets/autoreload.js index da62e5eae..fffb3e26c 100644 --- a/src/assets/autoreload.js +++ b/src/assets/autoreload.js @@ -1,3 +1,6 @@ +// Dioxus-CLI +// https://github.com/DioxusLabs/cli + (function () { var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; var url = protocol + '//' + window.location.host + '/_dioxus/ws'; @@ -15,7 +18,6 @@ var ws = new WebSocket(url); ws.onmessage = (ev) => { if (ev.data == "reload") { - // alert("reload!!!"); window.location.reload(); } }; diff --git a/src/assets/dioxus.toml b/src/assets/dioxus.toml index d3f160242..ab85abe86 100644 --- a/src/assets/dioxus.toml +++ b/src/assets/dioxus.toml @@ -6,7 +6,7 @@ name = "{{project-name}}" # default platfrom # you can also use `dioxus serve/build --platform XXX` to use other platform # value: web | desktop -default_platform = "web" +default_platform = "{{default-platform}}" # Web `build` & `serve` dist path out_dir = "dist" @@ -36,4 +36,10 @@ script = [] # Javascript code file # serve: [dev-server] only -script = [] \ No newline at end of file +script = [] + +[application.tools] + +# use binaryen.wasm-opt for output Wasm file +# binaryen just will trigger in `web` platform +binaryen = { wasm_opt = [".PROJECT_OUT"] } \ No newline at end of file diff --git a/src/assets/index.html b/src/assets/index.html index f907aa281..f57acbd34 100644 --- a/src/assets/index.html +++ b/src/assets/index.html @@ -1,14 +1,11 @@ - {app_title} {style_include} - -
@@ -22,5 +19,4 @@ {script_include} - \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index bacfcd664..aaaa0278a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -4,7 +4,8 @@ use crate::{ DioxusConfig, }; use std::{ - fs::{copy, create_dir_all, remove_dir_all}, + fs::{copy, create_dir_all, remove_dir_all, File}, + io::Read, panic, path::PathBuf, process::Command, @@ -95,8 +96,43 @@ pub fn build(config: &CrateConfig) -> Result<()> { }); if bindgen_result.is_err() { log::error!("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.79` Bindgen crate."); + return Ok(()); } + // check binaryen:wasm-opt tool + let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default(); + if dioxus_tools.contains_key("binaryen") { + let info = dioxus_tools.get("binaryen").unwrap(); + let binaryen = crate::tools::Tool::Binaryen; + + if binaryen.is_installed() { + if let Some(sub) = info.as_table() { + if sub.contains_key("wasm_opt") + && sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false) + { + let target_file = out_dir + .join("assets") + .join("dioxus") + .join(format!("{}_bg.wasm", dioxus_config.application.name)); + if target_file.is_file() { + binaryen.call( + "wasm-opt", + vec![ + target_file.to_str().unwrap(), + "-o", + target_file.to_str().unwrap(), + ], + )?; + } + } + } + } else { + log::warn!( + "Binaryen tool not found, you can use `dioxus tool add binaryen` to install it." + ); + } + } + // this code will copy all public file to the output dir let copy_options = fs_extra::dir::CopyOptions { overwrite: true, @@ -237,7 +273,19 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> { } pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { - let mut html = String::from(include_str!("./assets/index.html")); + let crate_root = crate::cargo::crate_root().unwrap(); + let custom_html_file = crate_root.join("index.html"); + let mut html = if custom_html_file.is_file() { + let mut buf = String::new(); + let mut file = File::open(custom_html_file).unwrap(); + if file.read_to_string(&mut buf).is_ok() { + buf + } else { + String::from(include_str!("./assets/index.html")) + } + } else { + String::from(include_str!("./assets/index.html")) + }; let resouces = config.web.resource.clone(); diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index 4c043091f..3b4d343af 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -12,6 +12,7 @@ impl Build { pub fn build(self) -> Result<()> { let mut crate_config = crate::CrateConfig::new()?; + // change the release state. crate_config.with_release(self.build.release); diff --git a/src/cli/config/mod.rs b/src/cli/config/mod.rs index 6d672b7a5..fb1e046fb 100644 --- a/src/cli/config/mod.rs +++ b/src/cli/config/mod.rs @@ -13,14 +13,26 @@ pub enum Config { #[clap(long)] #[serde(default)] force: bool, + + /// Project default platform + #[clap(long, default_value = "web")] + platform: String, }, + /// Format print Dioxus config. + FormatPrint {}, + /// Create a custom html file. + CustomHtml {} } impl Config { pub fn config(self) -> Result<()> { let crate_root = crate::cargo::crate_root()?; match self { - Config::Init { name, force } => { + Config::Init { + name, + force, + platform, + } => { let conf_path = crate_root.join("Dioxus.toml"); if conf_path.is_file() && !force { log::warn!( @@ -30,10 +42,21 @@ impl Config { } let mut file = File::create(conf_path)?; let content = String::from(include_str!("../../assets/dioxus.toml")) - .replace("{{project-name}}", &name); + .replace("{{project-name}}", &name) + .replace("{{default-platform}}", &platform); file.write_all(content.as_bytes())?; log::info!("🚩 Init config file completed."); } + Config::FormatPrint {} => { + println!("{:#?}", crate::CrateConfig::new()?.dioxus_config); + } + Config::CustomHtml {} => { + let html_path = crate_root.join("index.html"); + let mut file = File::create(html_path)?; + let content = include_str!("../../assets/index.html"); + file.write_all(content.as_bytes())?; + log::info!("🚩 Create custom html file done."); + } } Ok(()) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1b5ba0be7..2c91c94dd 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -5,6 +5,7 @@ pub mod config; pub mod create; pub mod serve; pub mod translate; +pub mod tool; use crate::{ cfg::{ConfigOptsBuild, ConfigOptsServe}, @@ -55,4 +56,7 @@ pub enum Commands { /// Dioxus config file controls. #[clap(subcommand)] Config(config::Config), + /// Install & Manage tools for Dioxus-cli. + #[clap(subcommand)] + Tool(tool::Tool), } diff --git a/src/cli/tool/mod.rs b/src/cli/tool/mod.rs new file mode 100644 index 000000000..51e41e384 --- /dev/null +++ b/src/cli/tool/mod.rs @@ -0,0 +1,66 @@ +use crate::tools; + +use super::*; + +/// Build the Rust WASM app and all of its assets. +#[derive(Clone, Debug, Deserialize, Subcommand)] +#[clap(name = "tool")] +pub enum Tool { + /// Return all dioxus-cli support tools. + List {}, + /// Get default app install path. + AppPath {}, + /// Install a new tool. + Add { + name: String + } +} + +impl Tool { + pub async fn tool(self) -> Result<()> { + match self { + Tool::List {} => { + for item in tools::tool_list() { + if tools::Tool::from_str(item).unwrap().is_installed() { + println!("{item} [installed]"); + } else { + println!("{item}"); + } + } + } + Tool::AppPath {} => { + println!("{}", tools::tools_path().to_str().unwrap()); + } + Tool::Add { name } => { + let tool_list = tools::tool_list(); + + if !tool_list.contains(&name.as_str()) { + log::error!("Tool {name} not found."); + return Ok(()); + } + let target_tool = tools::Tool::from_str(&name).unwrap(); + + if target_tool.is_installed() { + log::warn!("Tool {name} is installed."); + return Ok(()); + } + + log::info!("Start to download tool package..."); + if let Err(e) = target_tool.download_package().await { + log::error!("Tool download failed: {e}"); + return Ok(()); + } + + log::info!("Start to install tool package..."); + if let Err(e) = target_tool.install_package().await { + log::error!("Tool install failed: {e}"); + return Ok(()); + } + + log::info!("Tool {name} install successfully!"); + } + } + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index 9e8b47f7a..0cdd0bd8b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use crate::error::Result; use serde::{Deserialize, Serialize}; -use std::{fs::File, io::Read, path::PathBuf}; +use std::{fs::File, io::Read, path::PathBuf, collections::HashMap}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DioxusConfig { @@ -34,6 +34,7 @@ impl Default for DioxusConfig { default_platform: "web".to_string(), out_dir: Some(PathBuf::from("dist")), asset_dir: Some(PathBuf::from("public")), + tools: None, sub_package: None, }, web: WebConfig { @@ -65,6 +66,7 @@ pub struct ApplicationConfig { pub default_platform: String, pub out_dir: Option, pub asset_dir: Option, + pub tools: Option>, pub sub_package: Option, } diff --git a/src/lib.rs b/src/lib.rs index 395cbc8dc..8cd4b1f2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub mod builder; pub mod server; +pub mod tools; + pub use builder::*; pub mod cargo; diff --git a/src/main.rs b/src/main.rs index 97d4a3bb4..099ff74c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,12 @@ async fn main() -> Result<()> { log::error!("config error: {}", e); } } + + Commands::Tool(opts) => { + if let Err(e) = opts.tool().await { + log::error!("tool error: {}", e); + } + } } Ok(()) diff --git a/src/server/mod.rs b/src/server/mod.rs index 69d9799d2..1df13ca6a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -35,23 +35,23 @@ pub async fn startup(config: CrateConfig) -> Result<()> { // file watcher: check file change let watcher_conf = config.clone(); let mut watcher = RecommendedWatcher::new(move |_: notify::Result| { - if chrono::Local::now().timestamp() > last_update_time - && builder::build(&watcher_conf).is_ok() - { - // change the websocket reload state to true; - // the page will auto-reload. - if watcher_conf - .dioxus_config - .web - .watcher - .reload_html - .unwrap_or(false) - { - let _ = Serve::regen_dev_page(&watcher_conf); + if chrono::Local::now().timestamp() > last_update_time { + log::info!("Start to rebuild project..."); + if builder::build(&watcher_conf).is_ok() { + // change the websocket reload state to true; + // the page will auto-reload. + if watcher_conf + .dioxus_config + .web + .watcher + .reload_html + .unwrap_or(false) + { + let _ = Serve::regen_dev_page(&watcher_conf); + } + let _ = reload_tx.send("reload".into()); + last_update_time = chrono::Local::now().timestamp(); } - println!("watcher send reload"); - reload_tx.send("reload".into()).unwrap(); - last_update_time = chrono::Local::now().timestamp(); } }) .unwrap(); diff --git a/src/tools.rs b/src/tools.rs new file mode 100644 index 000000000..937063be0 --- /dev/null +++ b/src/tools.rs @@ -0,0 +1,187 @@ +use std::{ + fs::{create_dir_all, File}, + path::PathBuf, + process::Command, +}; + +use anyhow::Context; +use flate2::read::GzDecoder; +use futures::StreamExt; +use tar::Archive; +use tokio::io::AsyncWriteExt; + +#[derive(Debug, PartialEq, Eq)] +pub enum Tool { + Binaryen, +} + +pub fn tool_list() -> Vec<&'static str> { + vec!["binaryen"] +} + +pub fn app_path() -> PathBuf { + let data_local = dirs::data_local_dir().unwrap(); + let dioxus_dir = data_local.join("dioxus"); + if !dioxus_dir.is_dir() { + create_dir_all(&dioxus_dir).unwrap(); + } + dioxus_dir +} + +pub fn temp_path() -> PathBuf { + let app_path = app_path(); + let temp_path = app_path.join("temp"); + if !temp_path.is_dir() { + create_dir_all(&temp_path).unwrap(); + } + temp_path +} + +pub fn tools_path() -> PathBuf { + let app_path = app_path(); + let temp_path = app_path.join("tools"); + if !temp_path.is_dir() { + create_dir_all(&temp_path).unwrap(); + } + temp_path +} + +#[allow(clippy::should_implement_trait)] +impl Tool { + /// from str to tool enum + pub fn from_str(name: &str) -> Option { + match name { + "binaryen" => Some(Self::Binaryen), + _ => None, + } + } + + /// get current tool name str + pub fn name(&self) -> &str { + match self { + Self::Binaryen => "binaryen", + } + } + + /// get tool bin dir path + pub fn bin_path(&self) -> &str { + match self { + Self::Binaryen => "bin", + } + } + + /// get target platform + pub fn target_platform(&self) -> &str { + match self { + Self::Binaryen => { + if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_os = "linux") { + "linux" + } else { + panic!("unsupported platformm"); + } + } + } + } + + /// get tool package download url + pub fn download_url(&self) -> String { + match self { + Self::Binaryen => { + format!( + "https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-{target}.tar.gz", + target = self.target_platform() + ) + } + } + } + + /// get package extension name + pub fn extension(&self) -> &str { + match self { + Self::Binaryen => "tar.gz", + } + } + + /// check tool state + pub fn is_installed(&self) -> bool { + tools_path().join(self.name()).is_dir() + } + + /// get download temp path + pub fn temp_out_path(&self) -> PathBuf { + temp_path().join(format!("{}-tool.tmp", self.name())) + } + + /// start to download package + pub async fn download_package(&self) -> anyhow::Result { + let download_url = self.download_url(); + let temp_out = self.temp_out_path(); + let mut file = tokio::fs::File::create(&temp_out) + .await + .context("failed creating temporary output file")?; + + let resp = reqwest::get(download_url).await.unwrap(); + + let mut res_bytes = resp.bytes_stream(); + while let Some(chunk_res) = res_bytes.next().await { + let chunk = chunk_res.context("error reading chunk from download")?; + let _ = file.write(chunk.as_ref()).await; + } + + Ok(temp_out) + } + + /// start to install package + pub async fn install_package(&self) -> anyhow::Result<()> { + let temp_path = self.temp_out_path(); + let tool_path = tools_path(); + + let dir_name = if self == &Tool::Binaryen { + "binaryen-version_105" + } else { + "" + }; + + if self.extension() == "tar.gz" { + let tar_gz = File::open(temp_path)?; + let tar = GzDecoder::new(tar_gz); + let mut archive = Archive::new(tar); + archive.unpack(&tool_path)?; + // println!("{:?} -> {:?}", tool_path.join(dir_name), tool_path.join(self.name())); + std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?; + } + + Ok(()) + } + + pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result> { + let bin_path = tools_path().join(self.name()).join(self.bin_path()); + + let command_file = match self { + Tool::Binaryen => { + if cfg!(target_os = "windows") { + format!("{}.exe", command) + } else { + command.to_string() + } + } + }; + + if !bin_path.join(&command_file).is_file() { + return Err(anyhow::anyhow!("Command file not found.")); + } + + let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap()); + + let output = command + .args(&args[..]) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .output()?; + Ok(output.stdout) + } +}