tower_webdev/
webdev_service.rs1use std::{
2 path::PathBuf,
3 process::Stdio,
4 task::{Context, Poll},
5};
6
7use futures_util::future::BoxFuture;
8use http::{Request, Response};
9use http_body::Body as HttpBody;
10use http_body_util::Either;
11use hyper::body::Incoming;
12use insecure_reverse_proxy::{HttpReverseProxyService, InsecureReverseProxyService};
13use serde::{Deserialize, Serialize};
14use tokio::{
15 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
16 process::{ChildStdout, Command},
17};
18use tower::Service;
19use tower_http::services::{fs::ServeFileSystemResponseBody, ServeDir};
20
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct Config {
23 mode: Mode,
25 command: String,
27 install_command: String,
29 root: PathBuf,
31 target: PathBuf,
33 dev_server_port: u32,
35}
36
37impl Config {
38 pub fn new_pnpm(mode: Mode, root: impl Into<PathBuf>) -> Self {
39 let root = root.into();
40
41 Self {
42 mode,
43 command: "pnpm".into(),
44 install_command: "install".into(),
45 target: root.join("dist"),
46 root,
47 dev_server_port: 3000,
48 }
49 }
50
51 pub fn root(mut self, value: impl Into<PathBuf>) -> Self {
52 self.target = value.into();
53
54 self
55 }
56
57 pub fn target(mut self, value: impl Into<PathBuf>) -> Self {
58 self.target = value.into();
59
60 self
61 }
62
63 pub fn dev_server_port(mut self, value: u32) -> Self {
64 self.dev_server_port = value;
65
66 self
67 }
68
69 fn ensure_target_exists(&self) -> std::io::Result<()> {
70 std::fs::create_dir_all(&self.target)
71 }
72}
73
74#[derive(Debug, Clone, Deserialize, Serialize)]
75#[serde(rename_all = "lowercase")]
76pub enum Mode {
77 Production,
78 Development,
79}
80
81impl Mode {
82 pub fn assumed() -> Self {
83 #[cfg(debug_assertions)]
84 let mode = Self::Development;
85
86 #[cfg(not(debug_assertions))]
87 let mode = Self::Production;
88
89 mode
90 }
91}
92
93pub struct WebdevService<B> {
94 config: Config,
95 inner_service: InnerService<B>,
96}
97
98impl<B> Clone for WebdevService<B> {
99 fn clone(&self) -> Self {
100 WebdevService {
101 config: self.config.clone(),
102 inner_service: self.inner_service.clone(),
103 }
104 }
105}
106
107impl<B> WebdevService<B> {
108 pub async fn new(
109 config: Config,
110 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync + 'static>>
111 where
112 B: HttpBody + Send + Unpin + 'static,
113 B::Data: Send,
114 B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
115 {
116 config.ensure_target_exists()?;
117
118 let this = Self {
119 inner_service: InnerService::from_config(&config),
120 config,
121 };
122
123 this.execute_install().await?;
124
125 match &this.config.mode {
126 Mode::Development => {
127 this.execute_dev().await?;
128 }
129 Mode::Production => {
130 this.execute_dev().await?;
131 }
132 }
133
134 Ok(this)
135 }
136}
137
138pub type WebdevResponse = Either<ServeFileSystemResponseBody, Incoming>;
139
140impl<Body> Service<Request<Body>> for WebdevService<Body>
141where
142 Body: HttpBody + Send + Unpin + 'static,
143 Body::Data: Send,
144 Body::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
145{
146 type Response = Response<WebdevResponse>;
147 type Error = std::convert::Infallible;
148 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
149
150 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
151 match &mut self.inner_service {
152 InnerService::ServeDir(serve_dir) => {
153 <ServeDir as Service<Request<Body>>>::poll_ready(serve_dir, cx)
154 }
155 InnerService::ReverseProxy(proxy) => {
156 <HttpReverseProxyService<Body> as Service<Request<Body>>>::poll_ready(proxy, cx)
157 }
158 }
159 }
160
161 fn call(&mut self, request: Request<Body>) -> Self::Future {
162 match &self.inner_service {
163 InnerService::ServeDir(serve_dir) => {
164 let mut serve_dir = serve_dir.clone();
165
166 Box::pin(async move {
167 let res = serve_dir.call(request).await.unwrap();
168
169 Ok(res.map(Either::Left))
170 })
171 }
172 InnerService::ReverseProxy(proxy) => {
173 let mut proxy = proxy.clone();
174
175 Box::pin(async move {
176 let res = proxy.call(request).await.unwrap();
177
178 Ok(res.map(Either::Right))
179 })
180 }
181 }
182 }
183}
184
185enum InnerService<Body> {
186 ReverseProxy(HttpReverseProxyService<Body>),
187 ServeDir(ServeDir),
188}
189
190impl<B> Clone for InnerService<B> {
191 fn clone(&self) -> Self {
192 match self {
193 Self::ReverseProxy(p) => Self::ReverseProxy(p.clone()),
194 Self::ServeDir(s) => Self::ServeDir(s.clone()),
195 }
196 }
197}
198
199impl<Body> InnerService<Body> {
200 fn from_config(config: &Config) -> Self
201 where
202 Body: HttpBody + Send + Unpin + 'static,
203 Body::Data: Send,
204 {
205 match &config.mode {
206 Mode::Development => Self::ReverseProxy(InsecureReverseProxyService::new_http(
207 format!("http://localhost:{}", config.dev_server_port),
208 )),
209 _ => {
210 let serve_dir = ServeDir::new(&config.target);
211
212 Self::ServeDir(serve_dir)
213 }
214 }
215 }
216}
217
218#[allow(unused)]
219impl<B> WebdevService<B> {
220 async fn execute_install(
221 &self,
222 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
223 let mut command = Command::new(&self.config.command);
224 command.current_dir(&self.config.root.canonicalize()?);
225
226 command.args(["install"]);
227 command.stdout(Stdio::piped());
228
229 let mut build_process = command.spawn()?;
230
231 let stdout = build_process
232 .stdout
233 .take()
234 .expect("build_process did not have a handle to stdout");
235
236 write_stdout(stdout, "install");
237
238 match build_process.wait().await {
239 Ok(status) => {
240 if !status.success() {
241 tracing::error!("build process exited with error");
242
243 std::process::exit(status.code().unwrap_or(1));
244 }
245 }
246 Err(error) => {
247 tracing::error!("error waiting for build process: {}", error);
248 }
249 }
250
251 Ok(())
252 }
253
254 async fn execute_build(
255 &self,
256 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
257 let mut command = Command::new(&self.config.command);
258 command.current_dir(&self.config.root.canonicalize()?);
259
260 command.args(["build"]);
261 command.stdout(Stdio::piped());
262
263 let mut build_process = command.spawn()?;
264
265 let stdout = build_process
266 .stdout
267 .take()
268 .expect("build_process did not have a handle to stdout");
269
270 write_stdout(stdout, "build");
271
272 match build_process.wait().await {
273 Ok(status) => {
274 if !status.success() {
275 tracing::error!("build process exited with error");
276
277 std::process::exit(status.code().unwrap_or(1));
278 }
279 }
280 Err(error) => {
281 tracing::error!("error waiting for build process: {}", error);
282 }
283 }
284
285 Ok(())
286 }
287
288 async fn execute_dev(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
289 let mut command = Command::new(&self.config.command);
290 command.current_dir(&self.config.root.canonicalize()?);
291 command.args(["dev"]);
292 command.stdout(Stdio::piped());
293
294 let mut build_process = command.spawn()?;
295
296 let stdout = build_process
297 .stdout
298 .take()
299 .expect("dev process did not have a handle to stdout");
300
301 write_stdout(stdout, "dev");
302
303 tokio::spawn(async move {
304 match build_process.wait().await {
305 Ok(status) => {
306 if !status.success() {
307 tracing::error!("dev process exited with error");
308
309 std::process::exit(status.code().unwrap_or(1));
310 }
311 }
312 Err(error) => {
313 tracing::error!("error waiting for dev process: {}", error);
314 }
315 }
316 });
317
318 Ok(())
319 }
320}
321
322fn write_stdout(stdout: ChildStdout, prefix: &'static str) {
323 let mut reader = BufReader::new(stdout).lines();
324
325 tokio::spawn(async move {
326 let mut output = tokio::io::stdout();
327
328 while let Ok(Some(line)) = reader.next_line().await {
329 output
330 .write_all(format!("webdev {prefix}: ").as_bytes())
331 .await
332 .unwrap();
333 output.write_all(line.as_bytes()).await.unwrap();
334 output.write_all(b"\n").await.unwrap();
335 output.flush().await.unwrap();
336 }
337 });
338}