From 54c337e06f3bc624c4780cf002bc54790f446c90 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Wed, 28 Sep 2022 18:16:58 +0200 Subject: [PATCH] feat(cli): hotreload support for frontend static files, closes #2173 (#5256) Co-authored-by: Lucas Nogueira --- .changes/cli-static-files-hot-reload.md | 5 + .changes/utils-mimetype.md | 5 + core/tauri-runtime/Cargo.toml | 1 - core/tauri-runtime/src/http/mod.rs | 4 +- core/tauri-utils/Cargo.toml | 1 + core/tauri-utils/src/lib.rs | 1 + .../src/http => tauri-utils/src}/mime_type.rs | 3 + tooling/cli/Cargo.lock | 245 ++++++++++++++++-- tooling/cli/Cargo.toml | 6 + tooling/cli/src/dev.rs | 46 +++- tooling/cli/src/helpers/auto-reload.js | 29 +++ tooling/cli/src/helpers/mod.rs | 1 + tooling/cli/src/helpers/web_dev_server.rs | 154 +++++++++++ 13 files changed, 471 insertions(+), 30 deletions(-) create mode 100644 .changes/cli-static-files-hot-reload.md create mode 100644 .changes/utils-mimetype.md rename core/{tauri-runtime/src/http => tauri-utils/src}/mime_type.rs (98%) create mode 100644 tooling/cli/src/helpers/auto-reload.js create mode 100644 tooling/cli/src/helpers/web_dev_server.rs diff --git a/.changes/cli-static-files-hot-reload.md b/.changes/cli-static-files-hot-reload.md new file mode 100644 index 000000000..008f6817c --- /dev/null +++ b/.changes/cli-static-files-hot-reload.md @@ -0,0 +1,5 @@ +--- +"cli.rs": "minor" +--- + +Hot-reload the frontend when `tauri.conf.json > build > devPath` points to a directory. diff --git a/.changes/utils-mimetype.md b/.changes/utils-mimetype.md new file mode 100644 index 000000000..77185aa8b --- /dev/null +++ b/.changes/utils-mimetype.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": "patch" +--- + +Add `mime_type` module. diff --git a/core/tauri-runtime/Cargo.toml b/core/tauri-runtime/Cargo.toml index 7663a1157..ada42b23f 100644 --- a/core/tauri-runtime/Cargo.toml +++ b/core/tauri-runtime/Cargo.toml @@ -30,7 +30,6 @@ tauri-utils = { version = "1.1.1", path = "../tauri-utils" } uuid = { version = "1", features = [ "v4" ] } http = "0.2.4" http-range = "0.1.4" -infer = "0.7" raw-window-handle = "0.5" rand = "0.8" diff --git a/core/tauri-runtime/src/http/mod.rs b/core/tauri-runtime/src/http/mod.rs index d20e27e40..359f8cf53 100644 --- a/core/tauri-runtime/src/http/mod.rs +++ b/core/tauri-runtime/src/http/mod.rs @@ -3,16 +3,16 @@ // SPDX-License-Identifier: MIT // custom wry types -mod mime_type; mod request; mod response; pub use self::{ - mime_type::MimeType, request::{Request, RequestParts}, response::{Builder as ResponseBuilder, Response, ResponseParts}, }; +pub use tauri_utils::mime_type::MimeType; + // re-expose default http types pub use http::{header, method, status, uri::InvalidUri, version, Uri}; diff --git a/core/tauri-utils/Cargo.toml b/core/tauri-utils/Cargo.toml index 540b98422..0d4b5d610 100644 --- a/core/tauri-utils/Cargo.toml +++ b/core/tauri-utils/Cargo.toml @@ -35,6 +35,7 @@ glob = { version = "0.3.0", optional = true } walkdir = { version = "2", optional = true } memchr = "2.4" semver = "1" +infer = "0.7" [target."cfg(target_os = \"linux\")".dependencies] heck = "0.4" diff --git a/core/tauri-utils/src/lib.rs b/core/tauri-utils/src/lib.rs index 1f17a315f..976101bb5 100644 --- a/core/tauri-utils/src/lib.rs +++ b/core/tauri-utils/src/lib.rs @@ -14,6 +14,7 @@ pub mod assets; pub mod config; pub mod html; pub mod io; +pub mod mime_type; pub mod platform; /// Prepare application resources and sidecars. #[cfg(feature = "resources")] diff --git a/core/tauri-runtime/src/http/mime_type.rs b/core/tauri-utils/src/mime_type.rs similarity index 98% rename from core/tauri-runtime/src/http/mime_type.rs rename to core/tauri-utils/src/mime_type.rs index 2f9bb87e6..be2471ea9 100644 --- a/core/tauri-runtime/src/http/mime_type.rs +++ b/core/tauri-utils/src/mime_type.rs @@ -2,11 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! Determine a mime type from a URI or file contents. + use std::fmt; const MIMETYPE_PLAIN: &str = "text/plain"; /// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers) +#[allow(missing_docs)] pub enum MimeType { Css, Csv, diff --git a/tooling/cli/Cargo.lock b/tooling/cli/Cargo.lock index f7bfdc02e..bb53e2a32 100644 --- a/tooling/cli/Cargo.lock +++ b/tooling/cli/Cargo.lock @@ -91,6 +91,17 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69" +[[package]] +name = "async-trait" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "attohttpc" version = "0.22.0" @@ -121,6 +132,56 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +dependencies = [ + "async-trait", + "axum-core", + "base64", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa 1.0.2", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sha-1 0.10.0", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "tower-layer", + "tower-service", +] + [[package]] name = "base64" version = "0.13.0" @@ -277,6 +338,27 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cfb" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f89d248799e3f15f91b70917f65381062a01bb8e222700ea0e5a7ff9785f9c" +dependencies = [ + "byteorder", + "uuid 0.8.2", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid 1.1.2", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -729,16 +811,15 @@ dependencies = [ [[package]] name = "exr" -version = "1.4.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cc0e06fb5f67e5d6beadf3a382fec9baca1aa751c6d5368fdeee7e5932c215" +checksum = "c9a7880199e74c6d3fe45579df2f436c5913a71405494cb89d59234d86b47dc5" dependencies = [ "bit_field", - "deflate", "flume", "half", - "inflate", "lebe", + "miniz_oxide", "smallvec", "threadpool", ] @@ -912,9 +993,9 @@ checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" @@ -930,6 +1011,7 @@ checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-core", "futures-io", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1148,6 +1230,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + [[package]] name = "httparse" version = "1.8.0" @@ -1227,9 +1315,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c" dependencies = [ "bytemuck", "byteorder", @@ -1275,12 +1363,21 @@ dependencies = [ ] [[package]] -name = "inflate" -version = "0.4.5" +name = "infer" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +checksum = "20b2b533137b9cad970793453d4f921c2e91312a6d88b1085c07bc15fc51bb3b" dependencies = [ - "adler32", + "cfb 0.6.1", +] + +[[package]] +name = "infer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a" +dependencies = [ + "cfb 0.7.3", ] [[package]] @@ -1471,9 +1568,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lebe" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" @@ -1572,6 +1669,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + [[package]] name = "md5" version = "0.7.0" @@ -1618,9 +1721,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -2119,7 +2222,7 @@ checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" dependencies = [ "maplit", "pest", - "sha-1", + "sha-1 0.8.2", ] [[package]] @@ -2862,6 +2965,17 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha1" version = "0.10.1" @@ -3034,6 +3148,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "sysctl" version = "0.4.4" @@ -3100,6 +3220,7 @@ name = "tauri-cli" version = "1.1.1" dependencies = [ "anyhow", + "axum", "base64", "clap 3.2.7", "colored", @@ -3109,11 +3230,14 @@ dependencies = [ "glob", "handlebars", "heck 0.4.0", + "html5ever", "ignore", "image", "include_dir", + "infer 0.9.0", "json-patch", "jsonschema", + "kuchiki", "lazy_static", "libc", "log", @@ -3132,6 +3256,7 @@ dependencies = [ "tauri-icns", "tauri-utils", "tempfile", + "tokio", "toml", "toml_edit", "unicode-width", @@ -3172,6 +3297,7 @@ dependencies = [ "glob", "heck 0.4.0", "html5ever", + "infer 0.7.0", "json-patch", "json5", "kuchiki", @@ -3352,9 +3478,33 @@ dependencies = [ "once_cell", "pin-project-lite", "socket2", + "tokio-macros", "winapi 0.3.9", ] +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.4" @@ -3389,6 +3539,47 @@ dependencies = [ "itertools", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.2" @@ -3402,6 +3593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if 1.0.0", + "log", "pin-project-lite", "tracing-core", ] @@ -3430,6 +3622,25 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tungstenite" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha-1 0.10.0", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.15.0" diff --git a/tooling/cli/Cargo.toml b/tooling/cli/Cargo.toml index 8e52ff2fd..3887f6f87 100644 --- a/tooling/cli/Cargo.toml +++ b/tooling/cli/Cargo.toml @@ -76,6 +76,12 @@ log = { version = "0.4.17", features = [ "kv_unstable", "kv_unstable_std" ] } env_logger = "0.9.0" icns = { package = "tauri-icns", version = "0.1" } image = { version = "0.24", default-features = false, features = [ "ico" ] } +axum = { version = "0.5.16", features = ["ws"] } +html5ever = "0.25" +infer = "0.9" +kuchiki = "0.8" +tokio = { version = "1.21.1", features = ["macros", "sync"] } + [target."cfg(windows)".dependencies] winapi = { version = "0.3", features = [ "handleapi", "processenv", "winbase", "wincon", "winnt" ] } diff --git a/tooling/cli/src/dev.rs b/tooling/cli/src/dev.rs index 5acff16fe..25acac83c 100644 --- a/tooling/cli/src/dev.rs +++ b/tooling/cli/src/dev.rs @@ -6,7 +6,7 @@ use crate::{ helpers::{ app_paths::{app_dir, tauri_dir}, command_env, - config::{get as get_config, AppUrl, BeforeDevCommand, WindowUrl}, + config::{get as get_config, reload as reload_config, AppUrl, BeforeDevCommand, WindowUrl}, }, interface::{AppInterface, ExitReason, Interface}, CommandExt, Result, @@ -202,16 +202,42 @@ fn command_internal(mut options: Options) -> Result<()> { cargo_features.extend(features.clone()); } + let mut dev_path = config + .lock() + .unwrap() + .as_ref() + .unwrap() + .build + .dev_path + .clone(); + if let AppUrl::Url(WindowUrl::App(path)) = &dev_path { + use crate::helpers::web_dev_server::{start_dev_server, SERVER_URL}; + if path.exists() { + let path = path.canonicalize()?; + start_dev_server(path); + dev_path = AppUrl::Url(WindowUrl::External(SERVER_URL.parse().unwrap())); + + // TODO: in v2, use an env var to pass the url to the app context + // or better separate the config passed from the cli internally and + // config passed by the user in `--config` into to separate env vars + // and the context merges, the user first, then the internal cli config + if let Some(c) = options.config { + let mut c: tauri_utils::config::Config = serde_json::from_str(&c)?; + c.build.dev_path = dev_path.clone(); + options.config = Some(serde_json::to_string(&c).unwrap()); + } else { + options.config = Some(format!( + r#"{{ "build": {{ "devPath": "{}" }} }}"#, + SERVER_URL + )) + } + } + } + + let config = reload_config(options.config.as_deref())?; + if std::env::var_os("TAURI_SKIP_DEVSERVER_CHECK") != Some("true".into()) { - if let AppUrl::Url(WindowUrl::External(dev_server_url)) = config - .lock() - .unwrap() - .as_ref() - .unwrap() - .build - .dev_path - .clone() - { + if let AppUrl::Url(WindowUrl::External(dev_server_url)) = dev_path { let host = dev_server_url .host() .unwrap_or_else(|| panic!("No host name in the URL")); diff --git a/tooling/cli/src/helpers/auto-reload.js b/tooling/cli/src/helpers/auto-reload.js new file mode 100644 index 000000000..8e3840d07 --- /dev/null +++ b/tooling/cli/src/helpers/auto-reload.js @@ -0,0 +1,29 @@ + +// taken from https://github.com/thedodd/trunk/blob/5c799dc35f1f1d8f8d3d30c8723cbb761a9b6a08/src/autoreload.js + +(function () { + var url = 'ws:' + '//' + window.location.host + '/_tauri-cli/ws'; + var poll_interval = 5000; + var reload_upon_connect = () => { + window.setTimeout( + () => { + // when we successfully reconnect, we'll force a + // reload (since we presumably lost connection to + // trunk due to it being killed, so it will have + // rebuilt on restart) + 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) => { + const msg = JSON.parse(ev.data); + if (msg.reload) { + window.location.reload(); + } + }; + ws.onclose = reload_upon_connect; +})() diff --git a/tooling/cli/src/helpers/mod.rs b/tooling/cli/src/helpers/mod.rs index 8daa36301..4c38fb3b3 100644 --- a/tooling/cli/src/helpers/mod.rs +++ b/tooling/cli/src/helpers/mod.rs @@ -7,6 +7,7 @@ pub mod config; pub mod framework; pub mod template; pub mod updater_signature; +pub mod web_dev_server; use std::{ collections::HashMap, diff --git a/tooling/cli/src/helpers/web_dev_server.rs b/tooling/cli/src/helpers/web_dev_server.rs new file mode 100644 index 000000000..6a37eb807 --- /dev/null +++ b/tooling/cli/src/helpers/web_dev_server.rs @@ -0,0 +1,154 @@ +use axum::{ + extract::{ws::WebSocket, WebSocketUpgrade}, + http::{header::CONTENT_TYPE, Request, StatusCode}, + response::IntoResponse, + routing::get, + Router, Server, +}; +use html5ever::{namespace_url, ns, LocalName, QualName}; +use kuchiki::{traits::TendrilSink, NodeRef}; +use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, + thread, + time::Duration, +}; +use tauri_utils::mime_type::MimeType; +use tokio::sync::broadcast::{channel, Sender}; + +const AUTO_RELOAD_SCRIPT: &str = include_str!("./auto-reload.js"); +pub const SERVER_URL: &str = "http://127.0.0.1:1430"; + +struct State { + serve_dir: PathBuf, + tx: Sender<()>, +} + +pub fn start_dev_server>(path: P) { + let serve_dir = path.as_ref().to_path_buf(); + + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap() + .block_on(async move { + let (tx, _) = channel(1); + + let tokio_tx = tx.clone(); + let serve_dir_ = serve_dir.clone(); + thread::spawn(move || { + let (tx, rx) = std::sync::mpsc::channel(); + let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); + watcher.watch(serve_dir_, RecursiveMode::Recursive).unwrap(); + + loop { + if let Ok(e) = rx.recv() { + match e { + DebouncedEvent::Create(_) + | DebouncedEvent::Remove(_) + | DebouncedEvent::Rename(_, _) + | DebouncedEvent::Write(_) => { + let _ = tokio_tx.send(()); + } + _ => {} + } + } + } + }); + + let state = Arc::new(State { serve_dir, tx }); + let router = Router::new() + .fallback( + Router::new().nest( + "/", + get({ + let state = state.clone(); + move |req| handler(req, state) + }) + .handle_error(|_error| async move { StatusCode::INTERNAL_SERVER_ERROR }), + ), + ) + .route( + "/_tauri-cli/ws", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(|socket| async move { ws_handler(socket, state).await }) + }), + ); + Server::bind(&SocketAddr::from_str(SERVER_URL.split('/').nth(2).unwrap()).unwrap()) + .serve(router.into_make_service()) + .await + .unwrap(); + }) + }); +} + +async fn handler(req: Request, state: Arc) -> impl IntoResponse { + let uri = req.uri().to_string(); + let uri = if uri == "/" { + &uri + } else { + uri.strip_prefix('/').unwrap_or(&uri) + }; + + let file = std::fs::read(state.serve_dir.join(&uri)) + .or_else(|_| std::fs::read(state.serve_dir.join(format!("{}.html", &uri)))) + .or_else(|_| std::fs::read(state.serve_dir.join(format!("{}/index.html", &uri)))) + .or_else(|_| std::fs::read(state.serve_dir.join("index.html"))); + + file + .map(|mut f| { + let mime_type = MimeType::parse(&f, uri); + if mime_type == MimeType::Html.to_string() { + let mut document = kuchiki::parse_html().one(String::from_utf8_lossy(&f).into_owned()); + fn with_html_head(document: &mut NodeRef, f: F) { + if let Ok(ref node) = document.select_first("head") { + f(node.as_node()) + } else { + let node = NodeRef::new_element( + QualName::new(None, ns!(html), LocalName::from("head")), + None, + ); + f(&node); + document.prepend(node) + } + } + + with_html_head(&mut document, |head| { + let script_el = + NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None); + script_el.append(NodeRef::new_text(AUTO_RELOAD_SCRIPT)); + head.prepend(script_el); + }); + + f = document.to_string().as_bytes().to_vec(); + } + + (StatusCode::OK, [(CONTENT_TYPE, mime_type)], f) + }) + .unwrap_or_else(|_| { + ( + StatusCode::NOT_FOUND, + [(CONTENT_TYPE, "text/plain".into())], + vec![], + ) + }) +} + +async fn ws_handler(mut ws: WebSocket, state: Arc) { + let mut rx = state.tx.subscribe(); + while tokio::select! { + _ = ws.recv() => return, + fs_reload_event = rx.recv() => fs_reload_event.is_ok(), + } { + let ws_send = ws.send(axum::extract::ws::Message::Text( + r#"{"reload": true}"#.to_owned(), + )); + if ws_send.await.is_err() { + break; + } + } +}