vite_actix/
lib.rs

1//!
2//! # Vite Actix
3//! ![badge](https://github.com/Drew-Chase/vite-actix/actions/workflows/rust.yml/badge.svg)
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}