diff --git a/Cargo.toml b/Cargo.toml index 455048c36..5bbfd7ac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-cli" -version = "0.1.1" +version = "0.1.2" authors = ["Jonathan Kelley"] edition = "2018" description = "CLI tool for developing, testing, and publishing Dioxus apps" @@ -12,23 +12,29 @@ license = "MIT/Apache-2.0" thiserror = "1.0.23" log = "0.4.13" fern = { version = "0.6.0", features = ["colored"] } -wasm-bindgen-cli-support = "0.2.78" +wasm-bindgen-cli-support = "0.2.79" anyhow = "1.0.38" serde = { version = "1.0.133", features = ["derive"] } serde_json = "1" +toml = "0.5.8" fs_extra = "1.2.0" cargo_toml = "0.10.0" futures = "0.3.12" -notify = "4.0.17" +notify = { version = "5.0.0-pre.13", features = ["serde"] } html_parser = "0.6.2" -tui = { version = "0.16.0", default-features = false, features = ["crossterm"] } -crossterm = "0.22.1" binary-install = "0.0.2" convert_case = "0.5.0" structopt = "0.3.25" cargo_metadata = "0.14.1" tokio = { version = "1.15.0", features = ["full"] } atty = "0.2.14" +regex = "1.5.4" +chrono = "0.4.19" + +axum = { version = "0.4.4", features = ["ws", "headers"] } +tower-http = { version = "0.2.0", features = ["fs", "trace"] } +headers = "0.3" +# hyper = { version = "0.14.11", features = ["full"] } [[bin]] path = "src/main.rs" diff --git a/README.md b/README.md index 34be58eda..75e05b3cf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # About -dioxus-studio (inspired by wasm-pack and webpack) is a tool to help get dioxus projects off the ground. It handles all the build, development, bundling, and publishing to make web development simple. +dioxus-cli (inspired by wasm-pack and webpack) is a tool to help get dioxus projects off the ground. It handles all the build, development, bundling, and publishing to make web development simple. ## Installation @@ -19,5 +19,4 @@ dioxus-studio (inspired by wasm-pack and webpack) is a tool to help get dioxus p $ cargo install dioxus-cli ``` -Now, `dioxus` is in your path. - +Now, `dioxus` is in your path. \ No newline at end of file diff --git a/packages/rsx-translate/Cargo.toml b/packages/rsx-translate/Cargo.toml deleted file mode 100644 index 2af83c634..000000000 --- a/packages/rsx-translate/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "rsx-translate" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/packages/rsx-translate/src/lib.rs b/packages/rsx-translate/src/lib.rs deleted file mode 100644 index 1b4a90c93..000000000 --- a/packages/rsx-translate/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} diff --git a/src/assets/autoreload.js b/src/assets/autoreload.js new file mode 100644 index 000000000..da62e5eae --- /dev/null +++ b/src/assets/autoreload.js @@ -0,0 +1,23 @@ +(function () { + var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + var url = protocol + '//' + window.location.host + '/_dioxus/ws'; + var poll_interval = 8080; + var reload_upon_connect = () => { + window.setTimeout( + () => { + var ws = new WebSocket(url); + ws.onopen = () => window.location.reload(); + ws.onclose = reload_upon_connect; + }, + poll_interval); + }; + + var ws = new WebSocket(url); + ws.onmessage = (ev) => { + if (ev.data == "reload") { + // alert("reload!!!"); + window.location.reload(); + } + }; + ws.onclose = reload_upon_connect; +})() \ No newline at end of file diff --git a/src/assets/dioxus.toml b/src/assets/dioxus.toml new file mode 100644 index 000000000..717b070e2 --- /dev/null +++ b/src/assets/dioxus.toml @@ -0,0 +1,38 @@ +[application] + +# App (Project) Name +name = "{{project-name}}" + +# Dioxus App Support Platform +# desktop, web, mobile, ssr +platforms = ["web"] + +# Web `build` & `serve` dist path +out_dir = "dist" + +[web.app] + +# HTML title tag content +title = "dioxus | β›Ί" + +# resource (static) file folder +public_dir = "public" + +[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 = [] \ No newline at end of file diff --git a/src/assets/index.html b/src/assets/index.html new file mode 100644 index 000000000..9934e490d --- /dev/null +++ b/src/assets/index.html @@ -0,0 +1,18 @@ + + + {app_title} + + + {style_include} + + + +
+ + + {script_include} + + diff --git a/src/builder.rs b/src/builder.rs index b80c3dd36..257072a6a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,9 +1,11 @@ use crate::{ config::{CrateConfig, ExecutableType}, error::{Error, Result}, + DioxusConfig, }; use std::{ - io::{Read, Write}, + fs::{copy, create_dir_all, remove_dir_all}, + path::PathBuf, process::Command, }; use wasm_bindgen_cli_support::Bindgen; @@ -21,22 +23,23 @@ pub fn build(config: &CrateConfig) -> Result<()> { out_dir, crate_dir, target_dir, - static_dir, + public_dir, executable, + dioxus_config, .. } = config; let t_start = std::time::Instant::now(); // [1] Build the .wasm module - log::info!("Running build commands..."); + log::info!("πŸš… Running build command..."); let mut cmd = Command::new("cargo"); cmd.current_dir(&crate_dir) .arg("build") .arg("--target") .arg("wasm32-unknown-unknown") - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()); if config.release { cmd.arg("--release"); @@ -48,20 +51,16 @@ pub fn build(config: &CrateConfig) -> Result<()> { ExecutableType::Example(name) => cmd.arg("--example").arg(name), }; - let mut child = cmd.spawn()?; - let output = child.wait()?; + let output = cmd.output()?; - if output.success() { - log::info!("Build complete!"); - } else { + if !output.status.success() { log::error!("Build failed!"); - let mut reason = String::new(); - child.stderr.unwrap().read_to_string(&mut reason)?; + let reason = String::from_utf8_lossy(&output.stderr).to_string(); return Err(Error::BuildFailed(reason)); } // [2] Establish the output directory structure - let bindgen_outdir = out_dir.join("wasm"); + let bindgen_outdir = out_dir.join("assets").join("dioxus"); // [3] Bindgen the final binary for use easy linking let mut bindgen_builder = Bindgen::new(); @@ -89,53 +88,168 @@ pub fn build(config: &CrateConfig) -> Result<()> { .keep_debug(true) .remove_name_section(false) .remove_producers_section(false) - .out_name("module") + .out_name(&dioxus_config.application.name) .generate(&bindgen_outdir)?; - // [4] - // TODO: wasm-opt - - // [5] Generate the html file with the module name - // TODO: support names via options - log::info!("Writing to '{:#?}' directory...", out_dir); - let mut file = std::fs::File::create(out_dir.join("index.html"))?; - file.write_all(gen_page("./wasm/module.js").as_str().as_bytes())?; - - let copy_options = fs_extra::dir::CopyOptions::new(); - match fs_extra::dir::copy(static_dir, out_dir, ©_options) { - Ok(_) => {} - Err(_e) => { - log::warn!("Error copying dir"); + let copy_options = fs_extra::dir::CopyOptions { + overwrite: true, + skip_exist: false, + buffer_size: 64000, + copy_inside: false, + content_only: false, + depth: 0, + }; + if public_dir.is_dir() { + for entry in std::fs::read_dir(&public_dir)? { + let path = entry?.path(); + if path.is_file() { + std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?; + } else { + match fs_extra::dir::copy(&path, out_dir, ©_options) { + Ok(_) => {} + Err(_e) => { + log::warn!("Error copying dir: {}", _e); + } + } + } } } let t_end = std::time::Instant::now(); - log::info!("Done in {}ms! πŸŽ‰", (t_end - t_start).as_millis()); + log::info!("🏁 Done in {}ms!", (t_end - t_start).as_millis()); Ok(()) } -fn gen_page(module: &str) -> String { - format!( - r#" - - - - - - -
-
- - -
- - -"#, - module - ) +pub fn build_desktop(config: &CrateConfig) -> Result<()> { + log::info!("πŸš… Running build [Desktop] command..."); + + let mut cmd = Command::new("cargo"); + cmd.current_dir(&config.crate_dir) + .arg("build") + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()); + + if config.release { + cmd.arg("--release"); + } + + match &config.executable { + crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), + crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), + crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name), + }; + + let output = cmd.output()?; + + if !output.status.success() { + return Err(Error::BuildFailed("Program build failed.".into())); + } + + if output.status.success() { + if config.out_dir.is_dir() { + remove_dir_all(&config.out_dir)?; + } + + let release_type = match config.release { + true => "release", + false => "debug", + }; + + let file_name: String; + let mut res_path = match &config.executable { + crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => { + file_name = name.clone(); + config + .target_dir + .join(release_type.to_string()) + .join(name.to_string()) + } + crate::ExecutableType::Example(name) => { + file_name = name.clone(); + config + .target_dir + .join(release_type.to_string()) + .join("examples") + .join(name.to_string()) + } + }; + + let target_file = if cfg!(windows) { + res_path.set_extension("exe"); + format!("{}.exe", &file_name) + } else { + file_name + }; + + create_dir_all(&config.out_dir)?; + copy(res_path, &config.out_dir.join(target_file))?; + + log::info!( + "🚩 Build completed: [./{}]", + config + .dioxus_config + .application + .out_dir + .clone() + .unwrap_or_else(|| PathBuf::from("dist")) + .display() + ); + } + + Ok(()) +} + +pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { + let mut html = String::from(include_str!("./assets/index.html")); + + let resouces = config.web.resource.clone(); + + let mut style_list = resouces.style.unwrap_or_default(); + let mut script_list = resouces.script.unwrap_or_default(); + + if serve { + let mut dev_style = resouces.dev.style.clone().unwrap_or_default(); + let mut dev_script = resouces.dev.script.unwrap_or_default(); + style_list.append(&mut dev_style); + script_list.append(&mut dev_script); + } + + let mut style_str = String::new(); + for style in style_list { + style_str.push_str(&format!( + "\n", + &style.to_str().unwrap(), + )) + } + html = html.replace("{style_include}", &style_str); + + let mut script_str = String::new(); + for script in script_list { + script_str.push_str(&format!( + "\n", + &script.to_str().unwrap(), + )) + } + + html = html.replace("{script_include}", &script_str); + + if serve { + html += &format!( + "", + include_str!("./assets/autoreload.js") + ); + } + + html = html.replace("{app_name}", &config.application.name); + + let title = config + .web + .app + .title + .clone() + .unwrap_or_else(|| "dioxus | β›Ί".into()); + + html.replace("{app_title}", &title) } // use binary_install::{Cache, Download}; diff --git a/src/buildsystem/assets.rs b/src/buildsystem/assets.rs deleted file mode 100644 index e35ac1005..000000000 --- a/src/buildsystem/assets.rs +++ /dev/null @@ -1,4 +0,0 @@ -/* - -todo: wasm splitting -*/ diff --git a/src/buildsystem/wasm.rs b/src/buildsystem/wasm.rs deleted file mode 100644 index 8b1378917..000000000 --- a/src/buildsystem/wasm.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/cargo.rs b/src/cargo.rs index 3a911a81e..67a3e48f1 100644 --- a/src/cargo.rs +++ b/src/cargo.rs @@ -1,6 +1,11 @@ //! Utilities for working with cargo and rust files use crate::error::{Error, Result}; -use std::{env, fs, path::PathBuf, process::Command, str}; +use std::{ + env, fs, + path::{Path, PathBuf}, + process::Command, + str, +}; /// How many parent folders are searched for a `Cargo.toml` const MAX_ANCESTORS: u32 = 10; @@ -44,7 +49,7 @@ pub fn workspace_root() -> Result { } let stdout = str::from_utf8(&output.stdout).unwrap(); - for line in stdout.lines() { + if let Some(line) = stdout.lines().next() { let meta: serde_json::Value = serde_json::from_str(line) .map_err(|_| Error::CargoError("InvalidOutput".to_string()))?; @@ -58,7 +63,7 @@ pub fn workspace_root() -> Result { } /// Checks if the directory contains `Cargo.toml` -fn contains_manifest(path: &PathBuf) -> bool { +fn contains_manifest(path: &Path) -> bool { fs::read_dir(path) .map(|entries| { entries diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index 33a81a540..684cec58c 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -1,6 +1,6 @@ -use crate::cfg::ConfigOptsBuild; -use anyhow::Result; -use std::path::PathBuf; +use std::{io::Write, path::PathBuf}; + +use crate::{cfg::ConfigOptsBuild, gen_page}; use structopt::StructOpt; /// Build the Rust WASM app and all of its assets. @@ -11,4 +11,46 @@ pub struct Build { pub build: ConfigOptsBuild, } -pub async fn perform_build() {} +impl Build { + pub fn build(self) -> anyhow::Result<()> { + let mut crate_config = crate::CrateConfig::new()?; + + // change the relase state. + crate_config.with_release(self.build.release); + + if self.build.example.is_some() { + crate_config.as_example(self.build.example.unwrap()); + } + + match self.build.platform.as_str() { + "web" => { + crate::builder::build(&crate_config)?; + } + "desktop" => { + crate::builder::build_desktop(&crate_config)?; + } + _ => { + return Err(anyhow::anyhow!("Unsoppurt platform target.")); + } + } + + let temp = gen_page(&crate_config.dioxus_config, false); + + let mut file = std::fs::File::create( + crate_config + .crate_dir + .join( + crate_config + .dioxus_config + .application + .out_dir + .clone() + .unwrap_or_else(|| PathBuf::from("dist")), + ) + .join("index.html"), + )?; + file.write_all(temp.as_bytes())?; + + Ok(()) + } +} diff --git a/src/cli/cfg.rs b/src/cli/cfg.rs index feab0e61f..ece00e07e 100644 --- a/src/cli/cfg.rs +++ b/src/cli/cfg.rs @@ -1,11 +1,7 @@ -use anyhow::Result; use std::path::PathBuf; use structopt::StructOpt; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::str::FromStr; -use std::sync::Arc; +use serde::Deserialize; /// Config options for the build system. #[derive(Clone, Debug, Default, Deserialize, StructOpt)] @@ -19,52 +15,33 @@ pub struct ConfigOptsBuild { #[serde(default)] pub release: bool, - /// The output dir for all final assets [default: dist] - #[structopt(short, long, parse(from_os_str))] - pub dist: Option, + /// Build a example [default: ""] + #[structopt(long)] + pub example: Option, - /// The public URL from which assets are to be served [default: /] - #[structopt(long, parse(from_str=parse_public_url))] - pub public_url: Option, + /// Build platform: support Web & Desktop [default: "web"] + #[structopt(long, default_value = "web")] + pub platform: String, +} - /// Optional pattern for the app loader script [default: None] - /// - /// Patterns should include the sequences `{base}`, `{wasm}`, and `{js}` in order to - /// properly load the application. Other sequences may be included corresponding - /// to key/value pairs provided in `pattern_params`. - /// - /// These values can only be provided via config file. - #[structopt(skip)] +#[derive(Clone, Debug, Default, Deserialize, StructOpt)] +pub struct ConfigOptsServe { + /// The index HTML file to drive the bundling process [default: index.html] + #[structopt(parse(from_os_str))] + pub target: Option, + + /// Build a example [default: ""] + #[structopt(long)] + pub example: Option, + + /// Build in release mode [default: false] + #[structopt(long)] #[serde(default)] - pub pattern_script: Option, + pub release: bool, - /// Optional pattern for the app preload element [default: None] - /// - /// Patterns should include the sequences `{base}`, `{wasm}`, and `{js}` in order to - /// properly preload the application. Other sequences may be included corresponding - /// to key/value pairs provided in `pattern_params`. - /// - /// These values can only be provided via config file. - #[structopt(skip)] - #[serde(default)] - pub pattern_preload: Option, - - #[structopt(skip)] - #[serde(default)] - /// Optional replacement parameters corresponding to the patterns provided in - /// `pattern_script` and `pattern_preload`. - /// - /// When a pattern is being replaced with its corresponding value from this map, if the value is - /// prefixed with the symbol `@`, then the value is expected to be a file path, and the pattern - /// will be replaced with the contents of the target file. This allows insertion of some big JSON - /// state or even HTML files as a part of the `index.html` build. - /// - /// Trunk will automatically insert the `base`, `wasm` and `js` key/values into this map. In order - //// for the app to be loaded properly, the patterns `{base}`, `{wasm}` and `{js}` should be used - /// in `pattern_script` and `pattern_preload`. - /// - /// These values can only be provided via config file. - pub pattern_params: Option>, + /// Build platform: support Web & Desktop [default: "web"] + #[structopt(long, default_value = "web")] + pub platform: String, } /// Ensure the given value for `--public-url` is formatted correctly. diff --git a/src/cli/clean/mod.rs b/src/cli/clean/mod.rs index aeeedf523..62593b1ee 100644 --- a/src/cli/clean/mod.rs +++ b/src/cli/clean/mod.rs @@ -1,9 +1,40 @@ -use crate::cfg::ConfigOptsBuild; -use anyhow::Result; -use std::path::PathBuf; +use std::{ + fs::remove_dir_all, + path::PathBuf, + process::{Command, Stdio}, +}; + use structopt::StructOpt; /// Build the Rust WASM app and all of its assets. #[derive(Clone, Debug, StructOpt)] #[structopt(name = "clean")] pub struct Clean {} + +impl Clean { + pub fn clean(self) -> anyhow::Result<()> { + let crate_config = crate::CrateConfig::new()?; + + let output = Command::new("cargo") + .arg("clean") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output()?; + + if !output.status.success() { + log::error!("Cargo clean failed."); + return Ok(()); + } + + let out_dir = crate_config + .dioxus_config + .application + .out_dir + .unwrap_or_else(|| PathBuf::from("dist")); + if crate_config.crate_dir.join(&out_dir).is_dir() { + remove_dir_all(crate_config.crate_dir.join(&out_dir))?; + } + + Ok(()) + } +} diff --git a/src/cli/config/mod.rs b/src/cli/config/mod.rs index 616c9bc78..d5291a7d6 100644 --- a/src/cli/config/mod.rs +++ b/src/cli/config/mod.rs @@ -1,9 +1,43 @@ -use crate::cfg::ConfigOptsBuild; -use anyhow::Result; -use std::path::PathBuf; +use std::{fs::File, io::Write}; + +use serde::Deserialize; use structopt::StructOpt; /// Build the Rust WASM app and all of its assets. -#[derive(Clone, Debug, StructOpt)] +#[derive(Clone, Debug, Deserialize, StructOpt)] #[structopt(name = "config")] -pub struct Config {} +pub enum Config { + /// Init `Dioxus.toml` for project/folder. + Init { + /// Init project name + name: String, + + /// Cover old config + #[structopt(long)] + #[serde(default)] + force: bool, + }, +} + +impl Config { + pub fn config(self) -> anyhow::Result<()> { + let crate_root = crate::cargo::crate_root()?; + match self { + Config::Init { name, force } => { + let conf_path = crate_root.join("Dioxus.toml"); + if conf_path.is_file() && !force { + log::warn!( + "config file `Dioxus.toml` already exist, use `--force` to overwrite it." + ); + return Ok(()); + } + let mut file = File::create(conf_path)?; + let content = String::from(include_str!("../../assets/dioxus.toml")) + .replace("{{project-name}}", &name); + file.write_all(content.as_bytes())?; + log::info!("🚩 Init config file completed."); + } + } + Ok(()) + } +} diff --git a/src/cli/create/mod.rs b/src/cli/create/mod.rs new file mode 100644 index 000000000..fa553c55a --- /dev/null +++ b/src/cli/create/mod.rs @@ -0,0 +1,98 @@ +use std::{ + fs::File, + io::{Read, Write}, + path::PathBuf, + process::{Command, Stdio}, +}; + +use regex::Regex; +use serde::Deserialize; +use structopt::StructOpt; + +use crate::{error::Result, Error}; + +/// Build the Rust WASM app and all of its assets. +#[derive(Clone, Debug, Default, Deserialize, StructOpt)] +#[structopt(name = "create")] +pub struct Create { + /// Init project name + #[structopt(default_value = ".")] + name: String, + + /// Template path + #[structopt(default_value = "gh:dioxuslabs/dioxus-template", long)] + template: String, +} + +impl Create { + pub fn create(self) -> Result<()> { + if Self::name_vaild_check(self.name.clone()) { + log::error!("❗Unsupported project name."); + return Ok(()); + } + + let project_path = PathBuf::from(&self.name); + + if project_path.join("Dioxus.toml").is_file() || project_path.join("Cargo.toml").is_file() { + return Err(Error::Other(anyhow::anyhow!( + "🧨 Folder '{}' is initialized.", + &self.name + ))); + } + + log::info!("πŸ”§ Start to create a new project '{}'.", self.name); + + let output = Command::new("cargo") + .arg("generate") + .arg("--help") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output()?; + + if !output.status.success() { + log::warn!("Tool is not installed: cargo-generate, try to install it."); + let install_output = Command::new("cargo") + .arg("install") + .arg("cargo-generate") + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output()?; + if !install_output.status.success() { + return Err(Error::Other(anyhow::anyhow!( + "Try to install cargo-generate failed." + ))); + } + } + + let generate_output = Command::new("cargo") + .arg("generate") + .arg(&self.template) + .arg("--name") + .arg(&self.name) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output()?; + + if !generate_output.status.success() { + return Err(Error::Other(anyhow::anyhow!("Generate project failed."))); + } + + let mut dioxus_file = File::open(project_path.join("Dioxus.toml"))?; + let mut meta_file = String::new(); + dioxus_file.read_to_string(&mut meta_file)?; + meta_file = meta_file.replace("{{project-name}}", &self.name); + File::create(project_path.join("Dioxus.toml"))?.write_all(meta_file.as_bytes())?; + + println!(); + log::info!("πŸ’‘ Project initialized:"); + log::info!("🎯> cd ./{}", self.name); + log::info!("🎯> dioxus serve"); + + Ok(()) + } + + fn name_vaild_check(name: String) -> bool { + let r = Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-_]$").unwrap(); + r.is_match(&name) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fe118cbb7..4a72e00ac 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,14 +1,12 @@ -use anyhow::{Context, Result}; -use std::path::PathBuf; use structopt::StructOpt; pub mod build; pub mod cfg; pub mod clean; pub mod config; +pub mod create; pub mod serve; pub mod translate; -pub mod watch; /// Build, bundle, & ship your Dioxus app. /// @@ -31,16 +29,16 @@ pub struct Cli { #[derive(StructOpt)] pub enum Commands { - // /// Build the Rust WASM app and all of its assets. - // Build(build::Build), + /// Build the Rust WASM app and all of its assets. + Build(build::Build), /// Translate some source file into Dioxus code. Translate(translate::Translate), - // /// Build, watch & serve the Rust WASM app and all of its assets. - // Serve(serve::Serve), - - // /// Clean output artifacts. - // Clean(clean::Clean), - - // /// Trunk config controls. - // Config(config::Config), + /// Build, watch & serve the Rust WASM app and all of its assets. + Serve(serve::Serve), + /// Init a new project for Dioxus. + Create(create::Create), + /// Clean output artifacts. + Clean(clean::Clean), + /// Dioxus config file controls. + Config(config::Config), } diff --git a/src/cli/serve/develop.rs b/src/cli/serve/develop.rs deleted file mode 100644 index dd386b14e..000000000 --- a/src/cli/serve/develop.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::{cli::DevelopOptions, config::CrateConfig, error::Result}; -use async_std::prelude::FutureExt; - -use log::info; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use tide::http::mime::HTML; -use tide::http::Mime; - -pub struct DevelopState { - // - reload_on_change: bool, -} - -pub async fn develop(options: DevelopOptions) -> Result<()> { - // - log::info!("Starting development server πŸš€"); - let mut cfg = CrateConfig::new()?; - cfg.with_develop_options(&options); - - let out_dir = cfg.out_dir.clone(); - - let is_err = Arc::new(AtomicBool::new(false)); - - // Spawn the server onto a seperate task - // This lets the task progress while we handle file updates - let server = async_std::task::spawn(launch_server(out_dir, is_err.clone())); - let watcher = async_std::task::spawn(watch_directory(cfg.clone(), is_err.clone())); - - match server.race(watcher).await { - Err(e) => log::warn!("Error running development server, {:?}", e), - _ => {} - } - - Ok(()) -} - -async fn watch_directory(config: CrateConfig, is_err: ErrStatus) -> Result<()> { - // Create a channel to receive the events. - let (watcher_tx, watcher_rx) = async_std::channel::bounded(100); - - // Automatically select the best implementation for your platform. - // You can also access each implementation directly e.g. INotifyWatcher. - let mut watcher: RecommendedWatcher = Watcher::new(move |res| { - async_std::task::block_on(watcher_tx.send(res)); - // send an event - let _ = async_std::task::block_on(watcher_tx.send(res)); - }) - .expect("failed to make watcher"); - - let src_dir = crate::cargo::crate_root()?; - - // Add a path to be watched. All files and directories at that path and - // below will be monitored for changes. - watcher - .watch(&src_dir.join("src"), RecursiveMode::Recursive) - .expect("Failed to watch dir"); - - match watcher.watch(&src_dir.join("examples"), RecursiveMode::Recursive) { - Ok(_) => {} - Err(e) => log::warn!("Failed to watch examples dir, {:?}", e), - } - - 'run: loop { - match crate::builder::build(&config) { - Ok(_) => { - is_err.store(false, std::sync::atomic::Ordering::Relaxed); - async_std::task::sleep(std::time::Duration::from_millis(500)).await; - } - Err(err) => is_err.store(true, std::sync::atomic::Ordering::Relaxed), - }; - - let mut msg = None; - loop { - let new_msg = watcher_rx.recv().await.unwrap().unwrap(); - if !watcher_rx.is_empty() { - msg = Some(new_msg); - break; - } - } - - info!("File updated, rebuilding app"); - } - Ok(()) -} - -async fn launch_server(outdir: PathBuf, is_err: ErrStatus) -> Result<()> { - let _crate_dir = crate::cargo::crate_root()?; - let _workspace_dir = crate::cargo::workspace_root()?; - - let mut app = tide::with_state(ServerState::new(outdir.to_owned(), is_err)); - - let file_path = format!("{}/index.html", outdir.display()); - log::info!("Serving {}", file_path); - let p = outdir.display().to_string(); - - app.at("/") - .get(|req: tide::Request| async move { - log::info!("Connected to development server"); - let state = req.state(); - - match state.is_err.load(std::sync::atomic::Ordering::Relaxed) { - true => { - // - let mut resp = - tide::Body::from_string(format!(include_str!("../err.html"), err = "_")); - resp.set_mime(HTML); - - Ok(resp) - } - false => { - Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?) - } - } - }) - .serve_dir(p)?; - // .serve_file(file_path) - // .unwrap(); - - let port = "8080"; - let serve_addr = format!("127.0.0.1:{}", port); - - info!("App available at http://{}/", serve_addr); - app.listen(serve_addr).await?; - Ok(()) -} - -/// https://github.com/http-rs/tide/blob/main/examples/state.rs -/// Tide seems to prefer using state instead of injecting into the app closure -/// The app closure needs to be static and -#[derive(Clone)] -struct ServerState { - serv_path: PathBuf, - is_err: ErrStatus, -} - -type ErrStatus = Arc; - -impl ServerState { - fn new(serv_path: PathBuf, is_err: ErrStatus) -> Self { - Self { serv_path, is_err } - } -} diff --git a/src/cli/serve/draw.rs b/src/cli/serve/draw.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/cli/serve/events.rs b/src/cli/serve/events.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/cli/serve/mod.rs b/src/cli/serve/mod.rs index 9695870c0..8f26e0f30 100644 --- a/src/cli/serve/mod.rs +++ b/src/cli/serve/mod.rs @@ -1,9 +1,87 @@ -use crate::cfg::ConfigOptsBuild; -use anyhow::Result; -use std::path::PathBuf; +use crate::{cfg::ConfigOptsServe, gen_page, server, CrateConfig}; +use std::{io::Write, path::PathBuf, process::{Command, Stdio}}; use structopt::StructOpt; -/// Build the Rust WASM app and all of its assets. +/// Run the WASM project on dev-server #[derive(Clone, Debug, StructOpt)] #[structopt(name = "serve")] -pub struct Serve {} +pub struct Serve { + #[structopt(flatten)] + pub serve: ConfigOptsServe, +} + +impl Serve { + pub async fn serve(self) -> anyhow::Result<()> { + let mut crate_config = crate::CrateConfig::new()?; + + // change the relase state. + crate_config.with_release(self.serve.release); + + if self.serve.example.is_some() { + crate_config.as_example(self.serve.example.unwrap()); + } + + match self.serve.platform.as_str() { + "web" => { + crate::builder::build(&crate_config)?; + } + "desktop" => { + crate::builder::build_desktop(&crate_config)?; + + match &crate_config.executable { + crate::ExecutableType::Binary(name) + | crate::ExecutableType::Lib(name) + | crate::ExecutableType::Example(name) => { + let mut file = crate_config.out_dir.join(name); + if cfg!(windows) { + file.set_extension("exe"); + } + Command::new( + crate_config + .out_dir + .join(file) + .to_str() + .unwrap() + .to_string(), + ) + .stdout(Stdio::inherit()) + .output()?; + } + } + return Ok(()); + } + _ => { + return Err(anyhow::anyhow!("Unsoppurt platform target.")); + } + } + + // generate dev-index page + Serve::regen_dev_page(&crate_config)?; + + // start the develop server + server::startup(crate_config.clone()).await?; + + Ok(()) + } + + pub fn regen_dev_page(crate_config: &CrateConfig) -> anyhow::Result<()> { + let serve_html = gen_page(&crate_config.dioxus_config, true); + + let mut file = std::fs::File::create( + crate_config + .crate_dir + .join( + crate_config + .dioxus_config + .application + .out_dir + .clone() + .unwrap_or_else(|| PathBuf::from("dist")), + ) + .join("index.html"), + )?; + file.write_all(serve_html.as_bytes())?; + + Ok(()) + } +} diff --git a/src/cli/serve/studio.rs b/src/cli/serve/studio.rs deleted file mode 100644 index 60c330beb..000000000 --- a/src/cli/serve/studio.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! It's better to store all the configuration in one spot - -use tui::{ - backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - symbols, - text::{Span, Spans}, - widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle}, - widgets::{ - Axis, BarChart, Block, BorderType, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, - ListItem, Paragraph, Row, Sparkline, Table, Tabs, Wrap, - }, - Frame, Terminal, -}; - -use crate::*; -use std::{any::Any, io::Write, path::PathBuf, process::Command}; - -pub struct Cfg { - command: LaunchOptions, - headless: bool, - example: Option, - outdir: Option, - release: bool, - hydrate: Option, - template: Option, - translate_file: Option, - crate_config: Option, - should_quit: bool, -} - -pub async fn start(options: DevelopOptions) -> Result<()> { - let mut state = Cfg { - command: todo!(), - headless: todo!(), - example: todo!(), - outdir: todo!(), - release: todo!(), - hydrate: todo!(), - template: todo!(), - translate_file: todo!(), - crate_config: todo!(), - should_quit: false, - }; - - crossterm::terminal::enable_raw_mode()?; - - let backend = CrosstermBackend::new(std::io::stdout()); - let mut terminal = Terminal::new(backend).unwrap(); - - // Setup input handling - // let (tx, rx) = futures::channel::mpsc::unbounded(); - let tick_rate = std::time::Duration::from_millis(100); - - let mut prev_time = std::time::Instant::now(); - while !state.should_quit { - let next_time = prev_time + tick_rate; - let now = std::time::Instant::now(); - - let diff = next_time - std::time::Instant::now(); - } - - Ok(()) -} - -struct TuiStudio { - cfg: Cfg, - hook_idx: usize, - hooks: Vec>, -} -impl TuiStudio { - fn use_hook(&mut self, f: impl FnOnce() -> F) -> &mut F { - if self.hook_idx == self.hooks.len() { - self.hooks.push(Box::new(f())); - } - let idx = self.hook_idx; - self.hook_idx += 1; - let hook = self.hooks.get_mut(idx).unwrap(); - let r = hook.downcast_mut::().unwrap(); - r - } -} - -impl TuiStudio { - fn event_handler(&self, action: crossterm::event::Event) -> anyhow::Result<()> { - match action { - crossterm::event::Event::Key(_) => {} - crossterm::event::Event::Mouse(_) => {} - crossterm::event::Event::Resize(_, _) => {} - } - Ok(()) - } - - fn handle_key(&mut self, key: crossterm::event::KeyEvent) {} - - fn tick(&mut self) {} - - fn should_quit(&self) -> bool { - false - } - - fn render(&mut self, f: &mut tui::Frame) { - self.hook_idx = 0; - - // Wrapping block for a group - // Just draw the block and the group on the same area and build the group - // with at least a margin of 1 - let size = f.size(); - let block = Block::default() - .borders(Borders::ALL) - .title("Main block with round corners") - .border_type(BorderType::Rounded); - f.render_widget(block, size); - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(4) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(f.size()); - - let top_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[0]); - let block = Block::default() - .title(vec![ - Span::styled("With", Style::default().fg(Color::Yellow)), - Span::from(" background"), - ]) - .style(Style::default().bg(Color::Green)); - f.render_widget(block, top_chunks[0]); - - let block = Block::default().title(Span::styled( - "Styled title", - Style::default() - .fg(Color::White) - .bg(Color::Red) - .add_modifier(Modifier::BOLD), - )); - f.render_widget(block, top_chunks[1]); - - let bottom_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[1]); - let block = Block::default().title("With borders").borders(Borders::ALL); - f.render_widget(block, bottom_chunks[0]); - let block = Block::default() - .title("With styled borders and doubled borders") - .border_style(Style::default().fg(Color::Cyan)) - .borders(Borders::LEFT | Borders::RIGHT) - .border_type(BorderType::Double); - f.render_widget(block, bottom_chunks[1]); - } -} - -impl TuiStudio { - fn render_list(&mut self, f: &mut tui::Frame) { - let options = [ - "Bundle", "Develop", - // - ]; - } -} diff --git a/src/cli/translate/mod.rs b/src/cli/translate/mod.rs index 5799cad02..81b5092ed 100644 --- a/src/cli/translate/mod.rs +++ b/src/cli/translate/mod.rs @@ -48,7 +48,7 @@ impl Translate { exit(0); }) }); - if let None = temp { + if let Some(..) = temp { if let Some(s) = source { contents = s; } else { @@ -112,7 +112,7 @@ fn render_child(f: &mut impl Write, child: &Node, il: u32) -> std::fmt::Result { } write!(f, "\",")?; } - write!(f, "\n")?; + writeln!(f)?; // write the attributes if let Some(id) = &el.id { diff --git a/src/cli/translate/to_component.rs b/src/cli/translate/to_component.rs index b08161e9c..c83777ccb 100644 --- a/src/cli/translate/to_component.rs +++ b/src/cli/translate/to_component.rs @@ -4,6 +4,7 @@ //! - [ ] Extracts svgs //! - [ ] Attempts to extract lists +#[allow(unused_imports)] use std::{ fmt::{Display, Formatter}, io::Write, @@ -20,6 +21,7 @@ pub fn convert_html_to_component(html: &str) -> Result { }) } +#[allow(dead_code)] pub struct ComponentRenderer { dom: Dom, icon_index: usize, @@ -118,7 +120,7 @@ fn render_element<'a>( } write!(f, "\",")?; } - write!(f, "\n")?; + writeln!(f)?; // write the attributes if let Some(id) = &el.id { diff --git a/src/cli/watch/mod.rs b/src/cli/watch/mod.rs deleted file mode 100644 index 8b1378917..000000000 --- a/src/cli/watch/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/config.rs b/src/config.rs index 47920c583..1151445ef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,99 @@ use crate::error::Result; -use std::{io::Write, path::PathBuf, process::Command}; +use serde::{Deserialize, Serialize}; +use std::{fs::File, io::Read, path::PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DioxusConfig { + pub application: ApplicationConfig, + pub web: WebConfig, +} + +impl DioxusConfig { + pub fn load() -> crate::error::Result { + let crate_dir = crate::cargo::crate_root()?; + + if !crate_dir.join("Dioxus.toml").is_file() { + log::warn!("Config file: `Dioxus.toml` not found; using default config."); + return Ok(DioxusConfig::default()); + } + + let mut dioxus_conf_file = File::open(crate_dir.join("Dioxus.toml"))?; + let mut meta_str = String::new(); + dioxus_conf_file.read_to_string(&mut meta_str)?; + + toml::from_str::(&meta_str) + .map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into())) + } +} + +impl Default for DioxusConfig { + fn default() -> Self { + Self { + application: ApplicationConfig { + name: "dioxus".into(), + platforms: vec![String::from("web")], + out_dir: Some(PathBuf::from("dist")), + }, + web: WebConfig { + app: WebAppConfing { + title: Some("dioxus | β›Ί".into()), + public_dir: Some(PathBuf::from("public")), + }, + watcher: WebWatcherConfing { + watch_path: Some(vec![PathBuf::from("src")]), + reload_html: Some(false), + }, + resource: WebResourceConfing { + dev: WebDevResourceConfing { + style: Some(vec![]), + script: Some(vec![]), + }, + style: Some(vec![]), + script: Some(vec![]), + }, + }, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApplicationConfig { + pub name: String, + pub platforms: Vec, + pub out_dir: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebConfig { + pub app: WebAppConfing, + pub watcher: WebWatcherConfing, + pub resource: WebResourceConfing, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebAppConfing { + pub title: Option, + pub public_dir: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebWatcherConfing { + pub watch_path: Option>, + pub reload_html: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebResourceConfing { + pub dev: WebDevResourceConfing, + pub style: Option>, + pub script: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WebDevResourceConfing { + pub style: Option>, + pub script: Option>, +} #[derive(Debug, Clone)] pub struct CrateConfig { @@ -7,9 +101,10 @@ pub struct CrateConfig { pub crate_dir: PathBuf, pub workspace_dir: PathBuf, pub target_dir: PathBuf, - pub static_dir: PathBuf, + pub public_dir: PathBuf, pub manifest: cargo_toml::Manifest, pub executable: ExecutableType, + pub dioxus_config: DioxusConfig, pub release: bool, } @@ -22,28 +117,33 @@ pub enum ExecutableType { impl CrateConfig { pub fn new() -> Result { + let dioxus_config = DioxusConfig::load()?; + let crate_dir = crate::cargo::crate_root()?; let workspace_dir = crate::cargo::workspace_root()?; let target_dir = workspace_dir.join("target"); - let out_dir = crate_dir.join("public"); + + let out_dir = match dioxus_config.application.out_dir { + Some(ref v) => crate_dir.join(v), + None => crate_dir.join("dist"), + }; + let cargo_def = &crate_dir.join("Cargo.toml"); - let static_dir = crate_dir.join("static"); + + let public_dir = match dioxus_config.web.app.public_dir { + Some(ref v) => crate_dir.join(v), + None => crate_dir.join("public"), + }; let manifest = cargo_toml::Manifest::from_path(&cargo_def).unwrap(); // We just assume they're using a 'main.rs' // Anyway, we've already parsed the manifest, so it should be easy to change the type - let output_filename = (&manifest) + let output_filename = manifest .lib .as_ref() .and_then(|lib| lib.name.clone()) - .or_else(|| { - (&manifest) - .package - .as_ref() - .and_then(|pkg| Some(pkg.name.replace("-", "_"))) - .clone() - }) + .or_else(|| manifest.package.as_ref().map(|pkg| pkg.name.clone())) .expect("No lib found from cargo metadata"); let executable = ExecutableType::Binary(output_filename); @@ -54,10 +154,11 @@ impl CrateConfig { crate_dir, workspace_dir, target_dir, - static_dir, + public_dir, manifest, executable, release, + dioxus_config, }) } diff --git a/src/err.html b/src/err.html deleted file mode 100644 index 8fdbf9bae..000000000 --- a/src/err.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - -
-

- Sorry, but building your application failed. -

-

- Here's the error: -

- - {err} - -
- - - diff --git a/src/lib.rs b/src/lib.rs index 4cc4f2d75..395cbc8dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod builder; +pub mod server; pub use builder::*; pub mod cargo; @@ -15,13 +16,3 @@ pub use error::*; pub mod logging; pub use logging::*; - -pub mod watch; - -pub mod buildsystem { - pub mod wasm; - pub use wasm::*; - - pub mod assets; - pub use assets::*; -} diff --git a/src/logging.rs b/src/logging.rs index d30eeee47..48c2a2788 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -14,7 +14,7 @@ pub fn set_up_logging() { // configure colors for the name of the level. // since almost all of them are the same as the color for the whole line, we // just clone `colors_line` and overwrite our changes - let colors_level = colors_line.clone().info(Color::Green); + let colors_level = colors_line.info(Color::Green); // here we set up our fern Dispatch fern::Dispatch::new() .format(move |out, message, record| { diff --git a/src/main.rs b/src/main.rs index 798c492dc..6b8c177db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,22 +8,40 @@ async fn main() -> Result<()> { match args.action { Commands::Translate(opts) => { - opts.translate(); - } // Commands::Build(_) => { - // // - // } + if let Err(e) = opts.translate() { + log::error!("translate error: {}", e); + } + } - // Commands::Clean(_) => { - // // - // } + Commands::Build(opts) => { + if let Err(e) = opts.build() { + log::error!("build error: {}", e); + } + } - // Commands::Config(_) => { - // // - // } + Commands::Clean(opts) => { + if let Err(e) = opts.clean() { + log::error!("clean error: {}", e); + } + } - // Commands::Serve(_) => { - // // - // } + Commands::Serve(opts) => { + if let Err(e) = opts.serve().await { + log::error!("serve error: {}", e); + } + } + + Commands::Create(opts) => { + if let Err(e) = opts.create() { + log::error!("create error: {}", e); + } + } + + Commands::Config(opts) => { + if let Err(e) = opts.config() { + log::error!("config error: {}", e); + } + } } Ok(()) diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 000000000..880f25ceb --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,124 @@ +use axum::{ + extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, + http::StatusCode, + response::IntoResponse, + routing::{get, get_service}, + AddExtensionLayer, Router, +}; +use notify::{RecommendedWatcher, Watcher}; + +use std::{path::PathBuf, sync::Arc}; +use tower_http::services::ServeDir; + +use crate::{builder, serve::Serve, CrateConfig}; +use tokio::sync::broadcast; + +struct WsRelodState { + update: broadcast::Sender, +} + +pub async fn startup(config: CrateConfig) -> anyhow::Result<()> { + log::info!("πŸš€ Starting development server..."); + + let dist_path = config.out_dir.clone(); + + let (reload_tx, _) = broadcast::channel(100); + + let ws_reload_state = Arc::new(WsRelodState { + update: reload_tx.clone(), + }); + + let mut last_update_time = chrono::Local::now().timestamp(); + + // 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); + } + println!("watcher send reload"); + reload_tx.send("reload".into()).unwrap(); + last_update_time = chrono::Local::now().timestamp(); + } + }) + .unwrap(); + let allow_watch_path = config + .dioxus_config + .web + .watcher + .watch_path + .clone() + .unwrap_or_else(|| vec![PathBuf::from("src")]); + + for sub_path in allow_watch_path { + watcher + .watch( + &config.crate_dir.join(sub_path), + notify::RecursiveMode::Recursive, + ) + .unwrap(); + } + + // start serve dev-server at 0.0.0.0:8080 + let port = "8080"; + log::info!("πŸ“‘ Dev-Server is started at: http://127.0.0.1:{}/", port); + + axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap()) + .serve( + Router::new() + .route("/_dioxus/ws", get(ws_handler)) + .fallback( + get_service(ServeDir::new(config.crate_dir.join(&dist_path))).handle_error( + |error: std::io::Error| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + ) + }, + ), + ) + .layer(AddExtensionLayer::new(ws_reload_state)) + .into_make_service(), + ) + .await?; + + Ok(()) +} + +async fn ws_handler( + ws: WebSocketUpgrade, + _: Option>, + Extension(state): Extension>, +) -> impl IntoResponse { + ws.on_upgrade(|mut socket| async move { + let mut rx = state.update.subscribe(); + let reload_watcher = tokio::spawn(async move { + loop { + let v = rx.recv().await.unwrap(); + if v == "reload" { + // ignore the error + if socket + .send(Message::Text(String::from("reload"))) + .await + .is_err() + { + break; + } + } + } + }); + + reload_watcher.await.unwrap(); + }) +} diff --git a/src/watch.rs b/src/watch.rs deleted file mode 100644 index 8b1378917..000000000 --- a/src/watch.rs +++ /dev/null @@ -1 +0,0 @@ -