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}