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 long_poll;
81mod overlay;
82pub mod predicate;
83
84use std::{convert::Infallible, time::Duration};
85
86use http::{header, Request, Response, StatusCode};
87use inject::InjectService;
88use long_poll::LongPollBody;
89use overlay::OverlayService;
90use predicate::{Always, ContentTypeStartsWith, Predicate};
91use tokio::sync::broadcast::Sender;
92use tower::{Layer, Service};
93
94const DEFAULT_PREFIX: &str = "/tower-livereload/long-name-to-avoid-collisions";
95
96/// Utility to send reload requests to clients.
97#[derive(Clone, Debug)]
98pub struct Reloader {
99 sender: Sender<()>,
100}
101
102impl Reloader {
103 /// Create a new [`Reloader`].
104 ///
105 /// This can be manually passed to the [`LiveReload`] constructor, but in
106 /// most cases the [`LiveReloadLayer`] and [`LiveReloadLayer::reloader`]
107 /// utilities are preferred.
108 pub fn new() -> Self {
109 let (sender, _) = tokio::sync::broadcast::channel(1);
110 Self { sender }
111 }
112
113 /// Send a reload request to all open clients.
114 pub fn reload(&self) {
115 self.sender.send(()).ok();
116 }
117}
118
119impl Default for Reloader {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125/// Layer to apply [`LiveReload`] middleware.
126#[derive(Clone, Debug)]
127pub struct LiveReloadLayer<ReqPred = Always, ResPred = ContentTypeStartsWith<&'static str>> {
128 custom_prefix: Option<String>,
129 reloader: Reloader,
130 req_predicate: ReqPred,
131 res_predicate: ResPred,
132 reload_interval: Duration,
133}
134
135impl LiveReloadLayer {
136 /// Create a new [`LiveReloadLayer`] with the default prefix for internal
137 /// routes.
138 ///
139 /// The default prefix is deliberately long and specific to avoid any
140 /// accidental collisions with the wrapped service.
141 pub fn new() -> Self {
142 Self {
143 custom_prefix: None,
144 reloader: Reloader::new(),
145 req_predicate: Always,
146 res_predicate: ContentTypeStartsWith::new("text/html"),
147 reload_interval: Duration::from_secs(1),
148 }
149 }
150
151 /// Create a new [`LiveReloadLayer`] with a custom prefix.
152 #[deprecated(
153 since = "0.8.0",
154 note = "please use `LiveReloadLayer::new` and `custom_prefix` instead"
155 )]
156 pub fn with_custom_prefix<P: Into<String>>(prefix: P) -> Self {
157 Self::new().custom_prefix(prefix)
158 }
159}
160
161impl<ReqPred, ResPred> LiveReloadLayer<ReqPred, ResPred> {
162 /// Set a custom prefix for internal routes for the given
163 /// [`LiveReloadLayer`].
164 pub fn custom_prefix<P: Into<String>>(self, prefix: P) -> Self {
165 Self {
166 custom_prefix: Some(prefix.into()),
167 ..self
168 }
169 }
170
171 /// Set a custom predicate for requests that should have their response HTML
172 /// injected with live-reload logic.
173 ///
174 /// Note that this predicate is applied in addition to the default response
175 /// predicate, which makes sure that only HTML responses are injected.
176 ///
177 /// Also see [`predicate`] for pre-defined predicates and
178 /// [`predicate::Predicate`] for how to implement your own predicates.
179 pub fn request_predicate<Body, P: Predicate<Request<Body>>>(
180 self,
181 predicate: P,
182 ) -> LiveReloadLayer<P, ResPred> {
183 LiveReloadLayer {
184 custom_prefix: self.custom_prefix,
185 reloader: self.reloader,
186 req_predicate: predicate,
187 res_predicate: self.res_predicate,
188 reload_interval: self.reload_interval,
189 }
190 }
191
192 /// Set a custom predicate for responses that should be injected with
193 /// live-reload logic.
194 ///
195 /// Note that this predicate is applied instead of the default response
196 /// predicate, which would make sure that only HTML responses are injected.
197 /// However, even with a custom predicate only responses without a custom
198 /// encoding i.e. no [`Content-Encoding`] header can and will be injected.
199 ///
200 /// Also see [`predicate`] for pre-defined predicates and
201 /// [`predicate::Predicate`] for how to implement your own predicates.
202 ///
203 /// [`Content-Encoding`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
204 pub fn response_predicate<Body, P: Predicate<Response<Body>>>(
205 self,
206 predicate: P,
207 ) -> LiveReloadLayer<ReqPred, P> {
208 LiveReloadLayer {
209 custom_prefix: self.custom_prefix,
210 reloader: self.reloader,
211 req_predicate: self.req_predicate,
212 res_predicate: predicate,
213 reload_interval: self.reload_interval,
214 }
215 }
216
217 /// Set a custom retry interval for the live-reload logic.
218 pub fn reload_interval(self, interval: Duration) -> Self {
219 Self {
220 reload_interval: interval,
221 ..self
222 }
223 }
224
225 /// Return a manual [`Reloader`] trigger for the given [`LiveReloadLayer`].
226 pub fn reloader(&self) -> Reloader {
227 self.reloader.clone()
228 }
229}
230
231impl Default for LiveReloadLayer {
232 fn default() -> Self {
233 Self::new()
234 }
235}
236
237impl<S, ReqPred: Copy, ResPred: Copy> Layer<S> for LiveReloadLayer<ReqPred, ResPred> {
238 type Service = LiveReload<S, ReqPred, ResPred>;
239
240 fn layer(&self, inner: S) -> Self::Service {
241 #[allow(deprecated)]
242 LiveReload::new(
243 inner,
244 self.reloader.clone(),
245 self.req_predicate,
246 self.res_predicate,
247 self.reload_interval,
248 self.custom_prefix
249 .as_ref()
250 .cloned()
251 .unwrap_or_else(|| DEFAULT_PREFIX.to_owned()),
252 )
253 }
254}
255
256type InnerService<S, ReqPred, ResPred> = OverlayService<
257 String,
258 Infallible,
259 OverlayService<LongPollBody, Infallible, InjectService<S, ReqPred, ResPred>>,
260>;
261
262/// Middleware to enable LiveReload functionality.
263#[derive(Clone, Debug)]
264pub struct LiveReload<S, ReqPred = Always, ResPred = ContentTypeStartsWith<&'static str>> {
265 service: InnerService<S, ReqPred, ResPred>,
266}
267
268impl<S, ReqPred, ResPred> LiveReload<S, ReqPred, ResPred> {
269 #[deprecated(
270 since = "0.9.0",
271 note = "please use `LiveReloadLayer::new().layer(service)` instead"
272 )]
273 /// Create a new [`LiveReload`] middleware.
274 pub fn new<P: Into<String>>(
275 service: S,
276 reloader: Reloader,
277 req_predicate: ReqPred,
278 res_predicate: ResPred,
279 reload_interval: Duration,
280 prefix: P,
281 ) -> Self {
282 let prefix = prefix.into();
283 let long_poll_path = format!("{}/long-poll", prefix);
284 let back_up_path = format!("{}/back-up", prefix);
285 let inject = InjectService::new(
286 service,
287 format!(
288 r#"<script data-long-poll="{long_poll}" data-back-up="{back_up}" data-reload-interval="{reload_interval}">{code}</script>"#,
289 code = include_str!("../assets/polling.js"),
290 long_poll = long_poll_path,
291 back_up = back_up_path,
292 reload_interval = reload_interval.as_millis(),
293 )
294 .into(),
295 req_predicate,
296 res_predicate,
297 );
298 let overlay_poll = OverlayService::new(inject).path(long_poll_path, move || {
299 Response::builder()
300 .status(StatusCode::OK)
301 .header(header::CONTENT_TYPE, "text/event-stream")
302 .body(LongPollBody::new(reloader.sender.subscribe()))
303 .map_err(|_| unreachable!())
304 });
305 let overlay_up = OverlayService::new(overlay_poll).path(back_up_path, || {
306 Response::builder()
307 .status(StatusCode::OK)
308 .header(header::CONTENT_TYPE, "text/plain")
309 .body("Ok".to_owned())
310 .map_err(|_| unreachable!())
311 });
312
313 LiveReload {
314 service: overlay_up,
315 }
316 }
317}
318
319impl<ReqBody, ResBody, S, ReqPred, ResPred> Service<Request<ReqBody>>
320 for LiveReload<S, ReqPred, ResPred>
321where
322 S: Service<Request<ReqBody>, Response = Response<ResBody>>,
323 ResBody: http_body::Body,
324 ReqPred: Predicate<Request<ReqBody>>,
325 ResPred: Predicate<Response<ResBody>>,
326{
327 type Response = <InnerService<S, ReqPred, ResPred> as Service<Request<ReqBody>>>::Response;
328 type Error = <InnerService<S, ReqPred, ResPred> as Service<Request<ReqBody>>>::Error;
329 type Future = <InnerService<S, ReqPred, ResPred> as Service<Request<ReqBody>>>::Future;
330
331 fn poll_ready(
332 &mut self,
333 cx: &mut std::task::Context<'_>,
334 ) -> std::task::Poll<Result<(), Self::Error>> {
335 self.service.poll_ready(cx)
336 }
337
338 fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
339 self.service.call(req)
340 }
341}