vite_actix/lib.rs
1//!
2//! # Vite Actix
3//! 
4//!
5//! Vite Actix is a library designed to enable seamless integration of the **Vite development server** with the **Actix web framework**. It provides proxying functionality to forward HTTP requests to a local Vite server during development, enabling support for features like **hot module replacement (HMR)**, while maintaining a production-ready design for serving static files.
6//!
7//! ---
8//!
9//! ## Features
10//!
11//! - **Development Proxy**
12//! Forwards unmatched HTTP requests to the Vite development server during development.
13//!
14//! - **Hot Module Replacement**
15//! Enables fast reloads of assets and code during development, boosting productivity.
16//!
17//! - **Production-Ready**
18//! Automatically serves pre-bundled assets in production without proxy overhead.
19//!
20//! - **Customizable Configuration**
21//! Supports environment variables for customizing Vite integration (e.g., working directory and port).
22//!
23//! ---
24//!
25//! ## Getting Started
26//!
27//! ### Prerequisites
28//!
29//! Make sure you have the following tools installed:
30//!
31//! - **Rust** (version 1.65 or higher recommended)
32//! - **Node.js** (for Vite, version 18+ recommended)
33//! - **npm/yarn/pnpm** (for managing front-end dependencies)
34//!
35//! ### Installation
36//!
37//! Add the library to your Rust project by including it in your `Cargo.toml` file:
38//!
39//! ```toml
40//! [dependencies]
41//! vite-actix = "0.1.0"
42//! ```
43//!
44//! or using git
45//!
46//! ```toml
47//! [dependencies]
48//! vite-actix = {git = "https://github.com/Drew-Chase/vite-actix.git"}
49//! ```
50//!
51//! ---
52//!
53//! ## Usage
54//!
55//! ### Basic Configuration and Setup
56//!
57//! Follow these steps to integrate Vite with an Actix application:
58//!
59//! 1. **Set Environment Variables (Optional)**:
60//! - `VITE_WORKING_DIR`: Specifies the working directory containing `vite.config.[js|ts]`.
61//! - `VITE_PORT`: Vite server's port (default is `5173`).
62//!
63//! 2. **Example: Configuring Your Main Actix App**:
64//! Create a basic Actix application that includes Vite integration:
65//!
66//! ```rust
67//! use actix_web::{web, App, HttpResponse, HttpServer};
68//! use anyhow::Result;
69//! use vite_actix::{start_vite_server, ViteAppFactory};
70//!
71//! #[actix_web::main]
72//! async fn main() -> Result<()> {
73//! if cfg!(debug_assertions) {
74//! // Specify Vite's working directory and port in development mode
75//! std::env::set_var("VITE_WORKING_DIR", "./examples/wwwroot");
76//! std::env::set_var("VITE_PORT", "3000");
77//! }
78//!
79//! let server = HttpServer::new(move || {
80//! App::new()
81//! .route("/api/", web::get().to(HttpResponse::Ok))
82//! .configure_vite() // Enable Vite proxy during development
83//! })
84//! .bind("127.0.0.1:8080")?
85//! .run();
86//!
87//! if cfg!(debug_assertions) {
88//! start_vite_server()?;
89//! }
90//!
91//! println!("Server running at http://127.0.0.1:8080/");
92//! Ok(server.await?)
93//! }
94//! ```
95//!
96//! 3. **Run the Vite Dev Server**:
97//! - Use `vite-actix`'s `start_vite_server` function to automatically run the Vite server in development mode.
98//! - Static files and modules (such as `/assets/...`) are proxied to Vite when `cfg!(debug_assertions)` is true.
99//!
100//! ---
101//!
102//! ## Configuration
103//!
104//! ### Environment Variables
105//!
106//! | Variable | Default Value | Description |
107//! |--------------------|---------------|-----------------------------------------------------------------------------|
108//! | `VITE_WORKING_DIR` | `.` | Specifies the directory containing `vite.config.ts` (or similar config). |
109//! | `VITE_PORT` | `5173` | Configures the port that the Vite dev server listens on. |
110//!
111//! ### Proxy Rules
112//!
113//! The following routes are automatically proxied to the Vite dev server during development:
114//!
115//! - **Default Service**: Proxies all unmatched routes.
116//! - **Static Assets**: Requests for `/assets/...` are forwarded to the Vite server.
117//! - **Node Modules**: Resolves `/node_modules/...` through Vite.
118//!
119//! Ensure that your Vite configuration is consistent with the paths and routes used by your Actix web server.
120//!
121//! ---
122//!
123//! ## License
124//!
125//! This project is licensed under the GNU General Public License v3.0.
126//! See the [LICENSE](./LICENSE) file for details.
127//!
128//! ---
129//!
130//! ## Contributing
131//!
132//! Contributions are welcome! Please follow these steps:
133//!
134//! 1. Fork the repository.
135//! 2. Create a feature branch (`git checkout -b feature-name`).
136//! 3. Commit your changes (`git commit -m "Description of changes"`).
137//! 4. Push to the branch (`git push origin feature-name`).
138//! 5. Open a pull request.
139//!
140//! ---
141//!
142//! ## Repository & Support
143//!
144//! - **Repository**: [Vite Actix GitHub](https://github.com/Drew-Chase/vite-actix)
145//! - **Issues**: Use the GitHub issue tracker for bug reports and feature requests.
146//! - **Contact**: Reach out to the maintainer via the email listed in the repository.
147//!
148//! ---
149//!
150//! ## Examples
151//!
152//! See the [`/examples`](https://github.com/Drew-Chase/vite-actix/tree/master/examples) directory for sample implementations, including a fully functional integration of Vite with an Actix service.
153//!
154//! ---
155//!
156//! ## Acknowledgements
157//!
158//! - **Rust** for providing the ecosystem to build fast, secure web backends.
159//! - **Vite** for its cutting-edge tooling in front-end development.
160//!
161//! ---
162//!
163//! Enjoy using **Vite Actix** for your next project! If you encounter any issues, feel free to open a ticket on GitHub. 🛠️
164
165use actix_web::error::ErrorInternalServerError;
166use actix_web::{web, App, Error, HttpRequest, HttpResponse};
167use awc::Client;
168use futures_util::StreamExt;
169use log::{debug, error};
170// The maximum payload size allowed for forwarding requests and responses.
171//
172// This constant defines the maximum size (in bytes) for the request and response payloads
173// when proxying. Any payload exceeding this size will result in an error.
174//
175// Currently, it is set to 1 GB.
176const MAX_PAYLOAD_SIZE: usize = 1024 * 1024 * 1024; // 1 GB
177
178// Proxies requests to the Vite development server.
179//
180// This function forwards incoming requests to a local Vite server running on port 3000.
181// It buffers the entire request payload and response payload to avoid partial transfers.
182// Requests and responses larger than the maximum payload size will result in an error.
183//
184// # Arguments
185//
186// * `req` - The HTTP request object.
187// * `payload` - The request payload.
188//
189// # Returns
190//
191// An `HttpResponse` which contains the response from the Vite server,
192// or an error response in case of failure.
193async fn proxy_to_vite(
194 req: HttpRequest,
195 mut payload: web::Payload,
196) -> anyhow::Result<HttpResponse, Error> {
197 // Create a new HTTP client instance for making requests to the Vite server.
198 let client = Client::new();
199
200 // Construct the URL of the Vite server by reading the VITE_PORT environment variable,
201 // defaulting to 5173 if the variable is not set.
202 // The constructed URL uses the same URI as the incoming request.
203 let forward_url = format!(
204 "http://localhost:{}{}",
205 std::env::var("VITE_PORT").unwrap_or("5173".to_string()),
206 req.uri()
207 );
208
209 // Buffer the entire payload from the incoming request into body_bytes.
210 // This accumulates all chunks of the request body until no more are received or
211 // until the maximum allowed payload size is exceeded.
212 let mut body_bytes = web::BytesMut::new();
213 while let Some(chunk) = payload.next().await {
214 let chunk = chunk?;
215 // Check if the payload exceeds the maximum size defined by MAX_PAYLOAD_SIZE.
216 if (body_bytes.len() + chunk.len()) > MAX_PAYLOAD_SIZE {
217 return Err(actix_web::error::ErrorPayloadTooLarge("Payload overflow"));
218 }
219 // Append the current chunk to the body buffer.
220 body_bytes.extend_from_slice(&chunk);
221 }
222
223 // Forward the request to the Vite server along with the buffered request body.
224 let mut forwarded_resp = client
225 .request_from(forward_url.as_str(), req.head()) // Clone headers and method from the original request.
226 .no_decompress() // Disable automatic decompression of the response.
227 .send_body(body_bytes) // Send the accumulated request payload to the Vite server.
228 .await
229 .map_err(|err| ErrorInternalServerError(format!("Failed to forward request: {}", err)))?;
230
231 // Buffer the entire response body from the Vite server into resp_body_bytes.
232 // This accumulates all chunks of the response body until no more are received or
233 // until the maximum allowed payload size is exceeded.
234 let mut resp_body_bytes = web::BytesMut::new();
235 while let Some(chunk) = forwarded_resp.next().await {
236 let chunk = chunk?;
237 // Check if the response payload exceeds the maximum size defined by MAX_PAYLOAD_SIZE.
238 if (resp_body_bytes.len() + chunk.len()) > MAX_PAYLOAD_SIZE {
239 return Err(actix_web::error::ErrorPayloadTooLarge(
240 "Response payload overflow",
241 ));
242 }
243 // Append the current chunk to the response buffer.
244 resp_body_bytes.extend_from_slice(&chunk);
245 }
246
247 // Build the HTTP response to send back to the client.
248 let mut res = HttpResponse::build(forwarded_resp.status());
249
250 // Copy all headers from the response received from the Vite server
251 // and include them in the response to the client.
252 for (header_name, header_value) in forwarded_resp.headers().iter() {
253 res.insert_header((header_name.clone(), header_value.clone()));
254 }
255
256 // Return the response with the buffered body to the client.
257 Ok(res.body(resp_body_bytes))
258}
259
260/// Starts a Vite server by locating the installation of the Vite command using the system's
261/// `where` or `which` command (based on OS) and spawning the server in the configured working
262/// directory.
263///
264/// # Returns
265///
266/// Returns a result containing the spawned process's [`std::process::Child`] handle if successful,
267/// or an [`anyhow::Error`] if an error occurs.
268///
269/// # Errors
270///
271/// - Returns an error if the `vite` command cannot be found (`NotFound` error).
272/// - Returns an error if the `vite` command fails to execute or produce valid output.
273/// - Returns an error if the working directory environment variable or directory retrieval fails.
274///
275/// # Notes
276///
277/// - The working directory for Vite is set with the `VITE_WORKING_DIR` environment variable,
278/// falling back to the result of `try_find_vite_dir` or the current directory (".").
279///
280/// # Example
281/// ```no-rust
282/// let server = start_vite_server().expect("Failed to start Vite server");
283/// println!("Vite server started with PID: {}", server.id());
284/// ```
285///
286/// # Platform-Specific
287/// - On Windows, it uses `where` to find the `vite` executable.
288/// - On other platforms, it uses `which`.
289pub fn start_vite_server() -> anyhow::Result<std::process::Child> {
290 #[cfg(target_os = "windows")]
291 let find_cmd = "where"; // Use `where` on Windows to find the executable location.
292 #[cfg(not(target_os = "windows"))]
293 let find_cmd = "which"; // Use `which` on Unix-based systems to find the executable location.
294
295 // Locate the `vite` executable by invoking the system command and checking its output.
296 let vite = std::process::Command::new(find_cmd)
297 .arg("vite")
298 .stdout(std::process::Stdio::piped()) // Capture the command's stdout.
299 .output()? // Execute the command and handle potential IO errors.
300 .stdout;
301
302 // Convert the command output from bytes to a UTF-8 string.
303 let vite = String::from_utf8(vite)?;
304 let vite = vite.as_str().trim(); // Trim whitespace around the command output.
305
306 // If the `vite` command output is empty, the executable was not found.
307 if vite.is_empty() {
308 // Log an error message and return a `NotFound` error.
309 error!("vite not found, make sure it's installed with npm install -g vite");
310 Err(std::io::Error::new(
311 std::io::ErrorKind::NotFound,
312 "vite not found",
313 ))?;
314 }
315
316 // Vite installation could have multiple paths; using the last occurrence is a safeguard.
317 let vite = vite
318 .split("\n") // Split the results line by line.
319 .collect::<Vec<_>>() // Collect lines into a vector of strings.
320 .last() // Take the last entry in the result list.
321 .expect("Failed to get vite executable") // Panic if the vector for some reason is empty.
322 .trim(); // Trim any extra whitespace around the final path.
323
324 debug!("found vite at: {:?}", vite); // Log the found Vite path for debugging.
325
326 // Set the working directory for the Vite server. Use the environment variable if set, or:
327 // 1. Try to find the directory containing `vite.config.ts`.
328 // 2. Fallback to the current directory ('./') if none is found.
329 let working_dir = std::env::var("VITE_WORKING_DIR") // Tries the environment variable
330 .unwrap_or(
331 try_find_vite_dir() // Then tries to automagically find the vite directory
332 .unwrap_or(
333 std::env::current_dir() // Then will attempt to use the current working directory
334 // At this point, we've given up, as a hail mary we are
335 // just going to try to use the "." directory
336 // If that doesn't work, you might be SOL.
337 .map(|i| i.to_str().unwrap_or(".").to_string())
338 .unwrap_or(".".to_string()),
339 ),
340 );
341
342 // Start the Vite server with the determined executable and working directory.
343 Ok(
344 std::process::Command::new(vite) // Start command using Vite executable.
345 .current_dir(working_dir) // Set the working directory as determined above.
346 .arg("--port")
347 .arg(std::env::var("VITE_PORT").unwrap_or("5173".to_string()))
348 .arg("-l")
349 .arg("warn")
350 .spawn()?, // Spawn the subprocess and propagate any errors.
351 )
352}
353
354/// Attempts to find the directory containing `vite.config.ts`
355/// by traversing the filesystem upwards from the current working directory.
356///
357/// # Returns
358///
359/// Returns `Some(String)` with the path of the directory containing the `vite.config.[ts|js]` file,
360/// if found. Otherwise, returns `None` if the file is not located or an error occurs during traversal.
361///
362/// # Example
363/// ```no-rust
364/// if let Some(vite_dir) = try_find_vite_dir() {
365/// println!("Found vite.config.ts in directory: {}", vite_dir);
366/// } else {
367/// println!("vite.config.ts not found.");
368/// }
369/// ```
370pub fn try_find_vite_dir() -> Option<String> {
371 // Get the current working directory. If unable to retrieve, return `None`.
372 let mut cwd = std::env::current_dir().ok()?;
373
374 // Continue traversing upwards in the directory hierarchy until the root directory is reached.
375 while cwd != std::path::Path::new("/") {
376 // Check if 'vite.config.ts' exists in the current directory.
377 if cwd.join("vite.config.ts").exists() || cwd.join("vite.config.js").exists() {
378 // If found, convert the path to a `String` and return it.
379 return Some(cwd.to_str()?.to_string());
380 }
381 // Move to the parent directory if it exists.
382 if let Some(parent) = cwd.parent() {
383 cwd = parent.to_path_buf();
384 } else {
385 // Break the loop if the parent directory doesn't exist or if permissions were denied.
386 break;
387 }
388 }
389
390 // Return `None` if 'vite.config.[ts|js]' was not found.
391 None
392}
393
394/// Trait for configuring a Vite development proxy in an Actix web application.
395///
396/// This trait provides a method `configure_vite` to configure a web application
397/// for proxying requests to the Vite development server during development,
398/// while leaving the application unchanged in production.
399pub trait ViteAppFactory {
400 /// Configures the application to integrate with a Vite development proxy.
401 ///
402 /// This method configures the application to forward requests to a Vite
403 /// development server, enabling features such as hot module replacement (HMR)
404 /// during development. In a production environment, this configuration
405 /// typically has no effect, ensuring no unnecessary overhead when serving
406 /// static files or pre-compiled assets.
407 ///
408 /// # Returns
409 ///
410 /// Returns the modified application instance with the Vite proxy configuration applied.
411 fn configure_vite(self) -> Self;
412}
413
414// Implementation of the `AppConfig` trait for Actix `App` instances.
415impl<T> ViteAppFactory for App<T>
416where
417 T: actix_web::dev::ServiceFactory<
418 actix_web::dev::ServiceRequest, // Type of the incoming HTTP request.
419 Config = (), // No additional configuration is required.
420 Error = Error, // Type of the error produced by the service.
421 InitError = (), // No initialization error is expected.
422 >,
423{
424 fn configure_vite(self) -> Self {
425 if cfg!(debug_assertions) {
426 // Add a default service to catch all unmatched routes and proxy them to Vite.
427 self.default_service(web::route().to(proxy_to_vite))
428 // Route requests for static assets to the Vite server (e.g., "/assets/<file>").
429 .service(web::resource("/{file:.*}").route(web::get().to(proxy_to_vite)))
430 // Route requests for Node modules to the Vite server (e.g., "/node_modules/<file>").
431 .service(
432 web::resource("/node_modules/{file:.*}").route(web::get().to(proxy_to_vite)),
433 )
434 } else {
435 // If not in development mode, return the application without any additional configuration.
436 self
437 }
438 }
439}