feat(cli): hotreload support for frontend static files, closes #2173 (#5256)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
Amr Bashir
2022-09-28 18:16:58 +02:00
committed by GitHub
parent e8e2853830
commit 54c337e06f
13 changed files with 471 additions and 30 deletions

View File

@@ -0,0 +1,5 @@
---
"cli.rs": "minor"
---
Hot-reload the frontend when `tauri.conf.json > build > devPath` points to a directory.

View File

@@ -0,0 +1,5 @@
---
"tauri-utils": "patch"
---
Add `mime_type` module.

View File

@@ -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"

View File

@@ -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};

View File

@@ -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"

View File

@@ -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")]

View File

@@ -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,

245
tooling/cli/Cargo.lock generated
View File

@@ -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"

View File

@@ -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" ] }

View File

@@ -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"));

View File

@@ -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;
})()

View File

@@ -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,

View File

@@ -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<P: AsRef<Path>>(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<T>(req: Request<T>, state: Arc<State>) -> 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<F: FnOnce(&NodeRef)>(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<State>) {
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;
}
}
}