vite_actix/lib.rs
1#![doc = include_str!("../README.md")]
2
3pub mod proxy_vite_options;
4pub mod vite_app_factory;
5
6use std::time::Duration;
7use crate::proxy_vite_options::ProxyViteOptions;
8use actix_web::error::ErrorInternalServerError;
9use actix_web::{web, Error, HttpRequest, HttpResponse};
10use awc::Client;
11use futures_util::StreamExt;
12use log::{debug, error, info, trace, warn};
13use regex::Regex;
14
15// The maximum payload size allowed for forwarding requests and responses.
16//
17// This constant defines the maximum size (in bytes) for the request and response payloads
18// when proxying. Any payload exceeding this size will result in an error.
19//
20// Currently, it is set to 1 GB.
21const MAX_PAYLOAD_SIZE: usize = 1024 * 1024 * 1024; // 1 GB
22
23// Proxy requests to the Vite development server.
24//
25// This function forwards incoming requests to a local Vite server running on port 3000.
26// It buffers the entire request payload and response payload to avoid partial transfers.
27// Requests and responses larger than the maximum payload size will result in an error.
28//
29// # Arguments
30//
31// * `req` - The HTTP request object.
32// * `payload` - The request payload.
33//
34// # Returns
35//
36// An `HttpResponse` which contains the response from the Vite server,
37// or an error response in case of failure.
38async fn proxy_to_vite(
39 req: HttpRequest,
40 mut payload: web::Payload,
41) -> anyhow::Result<HttpResponse, Error> {
42 // Create a new HTTP client instance for making requests to the Vite server.
43 let client = Client::builder().timeout(Duration::from_secs(60)).finish();
44
45 // Get a copy of the current global options
46 let options = ProxyViteOptions::global();
47
48 let port = if let Some(port) = options.port {
49 port
50 } else {
51 return Err(ErrorInternalServerError(
52 "Unable to get port, you may have to set the port manually",
53 ));
54 };
55
56 // Construct the URL of the Vite server by reading the VITE_PORT environment variable,
57 // defaulting to 5173 if the variable is not set.
58 // The constructed URL uses the same URI as the incoming request.
59 let forward_url = format!("http://localhost:{}{}", port, req.uri());
60
61 // Buffer the entire payload from the incoming request into body_bytes.
62 // This accumulates all chunks of the request body until no more are received or
63 // until the maximum allowed payload size is exceeded.
64 let mut body_bytes = web::BytesMut::new();
65 while let Some(chunk) = payload.next().await {
66 let chunk = chunk?;
67 // Check if the payload exceeds the maximum size defined by MAX_PAYLOAD_SIZE.
68 if (body_bytes.len() + chunk.len()) > MAX_PAYLOAD_SIZE {
69 return Err(actix_web::error::ErrorPayloadTooLarge("Payload overflow"));
70 }
71 // Append the current chunk to the body buffer.
72 body_bytes.extend_from_slice(&chunk);
73 }
74
75 // Forward the request to the Vite server along with the buffered request body.
76 let mut forwarded_resp = client
77 .request_from(forward_url.as_str(), req.head()) // Clone headers and method from the original request.
78 .no_decompress() // Disable automatic decompression of the response.
79 .send_body(body_bytes) // Send the accumulated request payload to the Vite server.
80 .await
81 .map_err(|err| ErrorInternalServerError(format!("Failed to forward request: {}", err)))?;
82
83 // Buffer the entire response body from the Vite server into resp_body_bytes.
84 // This accumulates all chunks of the response body until no more are received or
85 // until the maximum allowed payload size is exceeded.
86 let mut resp_body_bytes = web::BytesMut::new();
87 while let Some(chunk) = forwarded_resp.next().await {
88 let chunk = chunk?;
89 // Check if the response payload exceeds the maximum size defined by MAX_PAYLOAD_SIZE.
90 if (resp_body_bytes.len() + chunk.len()) > MAX_PAYLOAD_SIZE {
91 return Err(actix_web::error::ErrorPayloadTooLarge(
92 "Response payload overflow",
93 ));
94 }
95 // Append the current chunk to the response buffer.
96 resp_body_bytes.extend_from_slice(&chunk);
97 }
98
99 // Build the HTTP response to send back to the client.
100 let mut res = HttpResponse::build(forwarded_resp.status());
101
102 // Copy all headers from the response received from the Vite server
103 // and include them in the response to the client.
104 for (header_name, header_value) in forwarded_resp.headers().iter() {
105 res.insert_header((header_name.clone(), header_value.clone()));
106 }
107
108 // Return the response with the buffered body to the client.
109 Ok(res.body(resp_body_bytes))
110}
111
112/// Starts a Vite server by locating the installation of the Vite command using the system's
113/// `where` or `which` command (based on OS) and spawning the server in the configured working
114/// directory.
115///
116/// # Returns
117///
118/// Returns a result containing the spawned process's [`std::process::Child`] handle if successful,
119/// or an [`anyhow::Error`] if an error occurs.
120///
121/// # Errors
122///
123/// - Returns an error if the `vite` command cannot be found (`NotFound` error).
124/// - Returns an error if the `vite` command fails to execute or produce valid output.
125/// - Returns an error if the working directory environment variable or directory retrieval fails.
126///
127/// # Notes
128///
129/// - The working directory for Vite is set with the `VITE_WORKING_DIR` environment variable,
130/// falling back to the result of `try_find_vite_dir` or the current directory (".").
131///
132/// # Example
133/// ```no-rust
134/// let server = start_vite_server().expect("Failed to start Vite server");
135/// println!("Vite server started with PID: {}", server.id());
136/// ```
137///
138/// # Platform-Specific
139/// - On Windows, it uses `where` to find the `vite` executable.
140/// - On other platforms, it uses `which`.
141///
142/// # Clippy:
143/// You may want to allow zombie processes in your code.
144/// `#[allow(clippy::zombie_processes)]`
145pub fn start_vite_server() -> anyhow::Result<std::process::Child> {
146 #[cfg(target_os = "windows")]
147 let find_cmd = "where"; // Use `where` on Windows to find the executable location.
148 #[cfg(not(target_os = "windows"))]
149 let find_cmd = "which"; // Use `which` on Unix-based systems to find the executable location.
150
151 // Locate the `vite` executable by invoking the system command and checking its output.
152 let vite = std::process::Command::new(find_cmd)
153 .arg("vite")
154 .stdout(std::process::Stdio::piped()) // Capture the command's stdout.
155 .output()? // Execute the command and handle potential IO errors.
156 .stdout;
157
158 // Convert the command output from bytes to a UTF-8 string.
159 let vite = String::from_utf8(vite)?;
160 let vite = vite.as_str().trim(); // Trim whitespace around the command output.
161
162 // If the `vite` command output is empty, the executable was not found.
163 if vite.is_empty() {
164 // Log an error message and return a `NotFound` error.
165 error!("vite not found, make sure it's installed with npm install -g vite");
166 Err(std::io::Error::new(
167 std::io::ErrorKind::NotFound,
168 "vite not found",
169 ))?;
170 }
171
172 // Vite installation could have multiple paths; using the last occurrence is a safeguard.
173 let vite = vite
174 .split("\n") // Split the result line by line.
175 .collect::<Vec<_>>() // Collect lines into a vector of strings.
176 .last() // Take the last entry in the result list.
177 .expect("Failed to get vite executable") // Panic if the vector for some reason is empty.
178 .trim(); // Trim any extra whitespace around the final path.
179
180 debug!("found vite at: {:?}", vite); // Log the found Vite path for debugging.
181
182 let options = ProxyViteOptions::global();
183
184 let mut vite_process = std::process::Command::new(vite);
185 vite_process.current_dir(&options.working_directory);
186 vite_process.stdout(std::process::Stdio::piped());
187
188 if let Some(port) = options.port {
189 vite_process.arg("--port").arg(port.to_string());
190 // vite_process.arg("--strictPort");
191 }
192
193 let mut vite_process = vite_process.spawn()?;
194
195 // Create a buffered reader to capture the output from the Vite process.
196 let vite_stdout = vite_process
197 .stdout
198 .take()
199 .ok_or_else(|| anyhow::Error::msg("Failed to capture Vite process stdout"))?;
200
201 // Clone options for the thread
202 let options_clone = options.clone();
203
204 // Create a channel to signal when Vite is ready
205 let (tx, rx) = tokio::sync::mpsc::channel::<String>(100);
206
207 // Spawn a thread to handle stdout reading
208 std::thread::spawn(move || {
209 use std::io::BufRead;
210 let mut reader = std::io::BufReader::new(vite_stdout);
211 let mut line = String::new();
212
213 // Create a Tokio runtime for this thread to handle async operations
214 let rt = tokio::runtime::Builder::new_current_thread()
215 .enable_all()
216 .build()
217 .expect("Failed to create Tokio runtime");
218
219 let regex = Regex::new(r"(?P<url>http://localhost:\d+).*").unwrap();
220 loop {
221 line.clear();
222 match reader.read_line(&mut line) {
223 Ok(0) => {
224 // End of file reached, the process has likely terminated
225 debug!("End of output stream from Vite process, exiting reader loop");
226 break;
227 }
228 Ok(_) => {
229 let trimmed_line = line.trim().to_string();
230
231 // Send the line through the channel
232 // This will block until the message is sent,
233 // but that's okay because we're in a dedicated thread
234 if rt.block_on(tx.send(trimmed_line.clone())).is_err() {
235 debug!("Failed to send log line, receiver was dropped");
236 break;
237 }
238 let decolored_text =
239 String::from_utf8(strip_ansi_escapes::strip(trimmed_line.as_str()))
240 .unwrap();
241 if decolored_text.contains("Local")
242 && decolored_text.contains("http://localhost:")
243 {
244 let caps = regex.captures(&decolored_text).unwrap();
245 let url = caps.name("url").unwrap().as_str();
246 let port = url.split(":").last().unwrap();
247 let port: u16 = port.parse().unwrap();
248
249 if let Err(e) = ProxyViteOptions::update_port(port) {
250 debug!("Failed to update Vite port to {}: {}", port, e);
251 } else {
252 debug!("Successfully updated Vite port to {}", port);
253 }
254 }
255 }
256 Err(err) => {
257 error!("Failed to read line from Vite process: {}", err);
258 break;
259 }
260 }
261 }
262 debug!("Exiting Vite stdout reader thread");
263 });
264
265 // Spawn a task to receive messages and log them
266 // This will work if we're in an async context with a Tokio runtime
267 if let Ok(handle) = tokio::runtime::Handle::try_current() {
268 let options = options_clone.clone();
269 handle.spawn(async move {
270 let mut rx = rx;
271 while let Some(line) = rx.recv().await {
272 match options.log_level {
273 None => {}
274 Some(log::Level::Trace) => trace!("{}", line),
275 Some(log::Level::Debug) => debug!("{}", line),
276 Some(log::Level::Info) => info!("{}", line),
277 Some(log::Level::Warn) => warn!("{}", line),
278 Some(log::Level::Error) => error!("{}", line),
279 }
280 }
281 });
282 } else {
283 // If we're not in a Tokio runtime context, we can create a thread to handle it
284 std::thread::spawn(move || {
285 // Create a runtime for this thread
286 let rt = tokio::runtime::Builder::new_current_thread()
287 .enable_all()
288 .build()
289 .expect("Failed to create Tokio runtime");
290
291 rt.block_on(async move {
292 let mut rx = rx;
293 while let Some(line) = rx.recv().await {
294 match options_clone.log_level {
295 None => {}
296 Some(log::Level::Trace) => trace!("{}", line),
297 Some(log::Level::Debug) => debug!("{}", line),
298 Some(log::Level::Info) => info!("{}", line),
299 Some(log::Level::Warn) => warn!("{}", line),
300 Some(log::Level::Error) => error!("{}", line),
301 }
302 }
303 });
304 });
305 }
306
307 // Return the process, which will continue running and logging output
308 Ok(vite_process)
309}