tower_livereload/
lib.rs

1//! A middleware for browser reloading, built on top of [`tower`].
2//!
3//! # Example
4//!
5//! Note that [`axum`] is only used as an example here, pretty much any Rust
6//! HTTP library or framework will be compatible!
7//!
8//! ```
9//! use axum::{response::Html, routing::get, Router};
10//! use tower_livereload::LiveReloadLayer;
11//!
12//! #[tokio::main]
13//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
14//!     let app = Router::new()
15//!         .route("/", get(|| async { Html("<h1>Wow, such webdev</h1>") }))
16//!         .layer(LiveReloadLayer::new());
17//!
18//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3030").await?;
19//!     axum::serve(listener, app).await?;
20//!
21//!     Ok(())
22//! }
23//! ```
24//!
25//! If you continuously rebuild and rerun this example e.g. using [`watchexec`],
26//! you should see your browser reload whenever the code is changed.
27//!
28//! More examples can be found on GitHub under [examples].
29//!
30//! [`axum`]: https://docs.rs/axum
31//! [`tower`]: https://docs.rs/tower
32//! [`watchexec`]: https://watchexec.github.io/
33//! [examples]: https://github.com/leotaku/tower-livereload/tree/master/examples
34//!
35//! # Manual reload
36//!
37//! With the [`Reloader`] utility, it is possible to reload your web browser
38//! entirely using hooks from Rust code. See this [example] on GitHub for
39//! pointers on how to implement a self-contained live-reloading static server.
40//!
41//! [example]: https://github.com/leotaku/tower-livereload/blob/master/examples/axum-file-watch/
42//!
43//! # Ecosystem compatibility
44//!
45//! `tower-livereload` has been built from the ground up to provide the highest
46//! amount of ecosystem compatibility.
47//!
48//! The provided middleware uses the [`http`] and [`http_body`] crates as its
49//! HTTP abstractions. That means it is compatible with any library or framework
50//! that also uses those crates, such as [`hyper`], [`axum`], [`tonic`], and
51//! [`warp`].
52//!
53//! [`http`]: https://docs.rs/http
54//! [`http_body`]: https://docs.rs/http_body
55//! [`hyper`]: https://docs.rs/hyper
56//! [`axum`]: https://docs.rs/axum
57//! [`tonic`]: https://docs.rs/tonic
58//! [`warp`]: https://docs.rs/warp
59//!
60//! # Heuristics
61//!
62//! To provide LiveReload functionality, we have to inject code into HTML web
63//! pages. To determine whether a page is injectable, some header-based
64//! heuristics are used. In particular, [`Content-Type`] has to start with
65//! `text/html` and [`Content-Encoding`] must not be set.
66//!
67//! If LiveReload is not working for some of your pages, ensure that these
68//! heuristics apply to your responses. In particular, if you use middleware to
69//! compress your HTML, ensure that the [`LiveReload`] middleware is
70//! applied before your compression middleware.
71//!
72//! [`Content-Type`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
73//! [`Content-Encoding`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
74
75#![forbid(unsafe_code, unused_unsafe)]
76#![warn(clippy::all, missing_docs, nonstandard_style, future_incompatible)]
77#![allow(clippy::type_complexity)]
78
79mod inject;
80mod overlay;
81pub mod predicate;
82mod sse;
83
84use std::{convert::Infallible, time::Duration};
85
86use http::{header, Request, Response, StatusCode};
87use tokio::sync::broadcast::Sender;
88use tower::{Layer, Service};
89
90use crate::{
91    inject::InjectService,
92    overlay::OverlayService,
93    predicate::{Always, ContentTypeStartsWith, Predicate},
94    sse::ReloadEventsBody,
95};
96
97const DEFAULT_PREFIX: &str = "/_tower-livereload";
98
99/// Utility to send reload requests to clients.
100#[derive(Clone, Debug)]
101pub struct Reloader {
102    sender: Sender<()>,
103}
104
105impl Reloader {
106    /// Create a new [`Reloader`].
107    ///
108    /// A standalone [`Reloader`] is not useful in most cases. Instead, the
109    /// [`LiveReloadLayer::reloader`] utility should be used to create a
110    /// [`Reloader`] that can send reload requests to connected clients.
111    pub fn new() -> Self {
112        let (sender, _) = tokio::sync::broadcast::channel(1);
113        Self { sender }
114    }
115
116    /// Send a reload request to all open clients.
117    pub fn reload(&self) {
118        self.sender.send(()).ok();
119    }
120}
121
122impl Default for Reloader {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128/// Layer to apply [`LiveReload`] middleware.
129#[derive(Clone, Debug)]
130pub struct LiveReloadLayer<ReqPred = Always, ResPred = ContentTypeStartsWith<&'static str>> {
131    custom_prefix: Option<String>,
132    reloader: Reloader,
133    req_predicate: ReqPred,
134    res_predicate: ResPred,
135    reload_interval: Duration,
136}
137
138impl LiveReloadLayer {
139    /// Create a new [`LiveReloadLayer`] with default settings.
140    pub fn new() -> Self {
141        Self {
142            custom_prefix: None,
143            reloader: Reloader::new(),
144            req_predicate: Always,
145            res_predicate: ContentTypeStartsWith::new("text/html"),
146            reload_interval: Duration::from_secs(1),
147        }
148    }
149}
150
151impl<ReqPred, ResPred> LiveReloadLayer<ReqPred, ResPred> {
152    /// Set a custom prefix for internal routes of the given
153    /// [`LiveReloadLayer`].
154    ///
155    /// Note that the provided prefix is not normalized before comparison. As
156    /// such, it has to include a leading slash to match URL paths correctly.
157    pub fn custom_prefix<P: Into<String>>(self, prefix: P) -> Self {
158        Self {
159            custom_prefix: Some(prefix.into()),
160            ..self
161        }
162    }
163
164    /// Set a custom predicate for requests that should have their response HTML
165    /// injected with live-reload logic.
166    ///
167    /// Note that this predicate is applied in addition to the default response
168    /// predicate, which makes sure that only HTML responses are injected.
169    ///
170    /// Also see [`predicate`] for pre-defined predicates and
171    /// [`predicate::Predicate`] for how to implement your own predicates.
172    pub fn request_predicate<Body, P: Predicate<Request<Body>>>(
173        self,
174        predicate: P,
175    ) -> LiveReloadLayer<P, ResPred> {
176        LiveReloadLayer {
177            custom_prefix: self.custom_prefix,
178            reloader: self.reloader,
179            req_predicate: predicate,
180            res_predicate: self.res_predicate,
181            reload_interval: self.reload_interval,
182        }
183    }
184
185    /// Set a custom predicate for responses that should be injected with
186    /// live-reload logic.
187    ///
188    /// Note that this predicate is applied instead of the default response
189    /// predicate, which would make sure that only HTML responses are injected.
190    /// However, even with a custom predicate only responses without a custom
191    /// encoding i.e. no [`Content-Encoding`] header can and will be injected.
192    ///
193    /// Also see [`predicate`] for pre-defined predicates and
194    /// [`predicate::Predicate`] for how to implement your own predicates.
195    ///
196    /// [`Content-Encoding`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
197    pub fn response_predicate<Body, P: Predicate<Response<Body>>>(
198        self,
199        predicate: P,
200    ) -> LiveReloadLayer<ReqPred, P> {
201        LiveReloadLayer {
202            custom_prefix: self.custom_prefix,
203            reloader: self.reloader,
204            req_predicate: self.req_predicate,
205            res_predicate: predicate,
206            reload_interval: self.reload_interval,
207        }
208    }
209
210    /// Set a custom retry interval for the live-reload logic.
211    pub fn reload_interval(self, interval: Duration) -> Self {
212        Self {
213            reload_interval: interval,
214            ..self
215        }
216    }
217
218    /// Return a manual [`Reloader`] trigger for the given [`LiveReloadLayer`].
219    pub fn reloader(&self) -> Reloader {
220        self.reloader.clone()
221    }
222}
223
224impl Default for LiveReloadLayer {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230impl<S, ReqPred: Copy, ResPred: Copy> Layer<S> for LiveReloadLayer<ReqPred, ResPred> {
231    type Service = LiveReload<S, ReqPred, ResPred>;
232
233    fn layer(&self, inner: S) -> Self::Service {
234        LiveReload::new(
235            inner,
236            self.reloader.clone(),
237            self.req_predicate,
238            self.res_predicate,
239            self.reload_interval,
240            self.custom_prefix
241                .as_ref()
242                .cloned()
243                .unwrap_or_else(|| DEFAULT_PREFIX.to_owned()),
244        )
245    }
246}
247
248type InnerService<S, ReqPred, ResPred> =
249    OverlayService<ReloadEventsBody, Infallible, InjectService<S, ReqPred, ResPred>>;
250
251/// Middleware to enable LiveReload functionality.
252#[derive(Clone, Debug)]
253pub struct LiveReload<S, ReqPred = Always, ResPred = ContentTypeStartsWith<&'static str>> {
254    service: InnerService<S, ReqPred, ResPred>,
255}
256
257impl<S, ReqPred, ResPred> LiveReload<S, ReqPred, ResPred> {
258    fn new<P: AsRef<str>>(
259        service: S,
260        reloader: Reloader,
261        req_predicate: ReqPred,
262        res_predicate: ResPred,
263        reload_interval: Duration,
264        prefix: P,
265    ) -> Self {
266        let event_stream_path = format!("{}/event-stream", prefix.as_ref());
267        let inject = InjectService::new(
268            service,
269            format!(
270                r#"<script data-event-stream="{path}">{code}</script>"#,
271                path = event_stream_path,
272                code = include_str!("../assets/sse_reload.js"),
273            )
274            .into(),
275            req_predicate,
276            res_predicate,
277        );
278        let overlay = OverlayService::new(inject, move |parts| {
279            if parts.uri.path() == event_stream_path {
280                return Some(
281                    Response::builder()
282                        .status(StatusCode::OK)
283                        .header(header::CONTENT_TYPE, "text/event-stream")
284                        .body(ReloadEventsBody::new(
285                            reloader.sender.subscribe(),
286                            reload_interval,
287                        ))
288                        .map_err(|_| unreachable!()),
289                );
290            }
291
292            None
293        });
294
295        LiveReload { service: overlay }
296    }
297}
298
299impl<ReqBody, ResBody, S, ReqPred, ResPred> Service<Request<ReqBody>>
300    for LiveReload<S, ReqPred, ResPred>
301where
302    S: Service<Request<ReqBody>, Response = Response<ResBody>>,
303    ResBody: http_body::Body,
304    ReqPred: Predicate<Request<ReqBody>>,
305    ResPred: Predicate<Response<ResBody>>,
306{
307    type Response = <InnerService<S, ReqPred, ResPred> as Service<Request<ReqBody>>>::Response;
308    type Error = <InnerService<S, ReqPred, ResPred> as Service<Request<ReqBody>>>::Error;
309    type Future = <InnerService<S, ReqPred, ResPred> as Service<Request<ReqBody>>>::Future;
310
311    fn poll_ready(
312        &mut self,
313        cx: &mut std::task::Context<'_>,
314    ) -> std::task::Poll<Result<(), Self::Error>> {
315        self.service.poll_ready(cx)
316    }
317
318    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
319        self.service.call(req)
320    }
321}