nodejs_bundler/
lib.rs

1pub use phf;
2use std::path::Path;
3
4pub type Files = phf::Map<&'static str, &'static [u8]>;
5
6pub struct NodeJsBundle {
7    files: Files,
8}
9
10fn get_local_path(mut path: &str) -> &str {
11    if let Some(rest) = path.strip_prefix('/') {
12        path = rest;
13    }
14    if path.is_empty() {
15        path = "index.html";
16    }
17    path
18}
19
20impl NodeJsBundle {
21    #[must_use]
22    pub const fn new(files: Files) -> Self {
23        Self { files }
24    }
25
26    /// returns (path, bytes)
27    #[must_use]
28    pub fn get_file<'a>(&'static self, path: &'a str) -> Option<(&'a str, Vec<u8>)> {
29        let path = get_local_path(path);
30        let bytes = zstd::stream::decode_all(*self.files.get(path)?).ok()?;
31        Some((path, bytes))
32    }
33
34    #[must_use]
35    pub fn get_content_type(path: &str) -> &'static str {
36        let path = get_local_path(path);
37        let extension = Path::new(path)
38            .extension()
39            .unwrap_or_default()
40            .to_ascii_lowercase();
41
42        match extension.to_string_lossy().as_ref() {
43            "css" => "text/css; charset=utf-8",
44            "js" => "application/javascript; charset=utf-8",
45            "html" => "text/html; charset=utf-8",
46            _ => "application/octet-stream",
47        }
48    }
49
50    #[cfg(feature = "actix")]
51    #[must_use]
52    pub fn as_actix_handler(
53        &'static self,
54    ) -> impl actix_web::Handler<(actix_web::HttpRequest,), Output = actix_web::HttpResponse> {
55        use std::future;
56
57        use actix_web::{http::header::ContentType, HttpRequest, HttpResponse};
58
59        |req: HttpRequest| -> future::Ready<HttpResponse> {
60            let path = req.path();
61
62            future::ready(if let Some((path, bytes)) = self.get_file(path) {
63                let mut builder = HttpResponse::Ok();
64
65                if let Ok(content_type) = Self::get_content_type(path).parse() {
66                    builder.content_type(ContentType(content_type));
67                }
68
69                builder.body(bytes)
70            } else {
71                HttpResponse::NotFound().finish()
72            })
73        }
74    }
75
76    #[cfg(feature = "actix")]
77    #[must_use]
78    pub fn as_actix_route(&'static self) -> actix_web::Route {
79        use actix_web::web;
80
81        web::get().to(self.as_actix_handler())
82    }
83
84    #[cfg(feature = "actix")]
85    #[must_use]
86    pub fn as_actix_resource(&'static self) -> actix_web::Resource {
87        use actix_web::Resource;
88
89        Resource::new("/{path}*")
90            .name(env!("CARGO_PKG_NAME"))
91            .to(self.as_actix_handler())
92    }
93
94    #[cfg(feature = "warp")]
95    #[must_use]
96    pub fn as_warp_filter(&'static self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
97        use warp::Filter;
98
99        warp::path::full()
100            .map(move |path| self.as_warp_reply(&path))
101            .boxed()
102    }
103
104    #[cfg(feature = "warp")]
105    #[must_use]
106    pub fn as_warp_reply(&'static self, path: &warp::filters::path::FullPath) -> impl warp::Reply {
107        use warp::http::Response;
108
109        let path = path.as_str();
110
111        if let Some((path, data)) = self.get_file(path) {
112            Response::builder()
113                .header("Content-Type", Self::get_content_type(path))
114                .body(data)
115                .unwrap()
116        } else {
117            Response::builder().status(404).body(vec![]).unwrap()
118        }
119    }
120
121    #[cfg(feature = "rocket")]
122    #[must_use]
123    pub fn as_rocket_route(&'static self) -> rocket::Route {
124        use rocket::{
125            http::{ContentType, Method, Status},
126            route::{Handler, Outcome},
127            Data, Request, Route,
128        };
129
130        #[derive(Clone)]
131        struct RocketHandler(&'static NodeJsBundle);
132
133        #[rocket::async_trait]
134        impl Handler for RocketHandler {
135            async fn handle<'r>(&self, req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r> {
136                let Ok(path) = req.routed_segments(0..).to_path_buf(false) else {
137                    return Outcome::Forward((data, Status::Ok));
138                };
139                let Some((_, bytes)) = self.0.get_file(&path.to_string_lossy()) else {
140                    return Outcome::Forward((data, Status::Ok));
141                };
142                let content_type = path
143                    .extension()
144                    .and_then(|ext| ContentType::from_extension(&ext.to_string_lossy()))
145                    .unwrap_or(ContentType::HTML);
146
147                Outcome::from(req, (content_type, bytes))
148            }
149        }
150
151        Route::new(Method::Get, "/<path..>", RocketHandler(self))
152    }
153
154    #[cfg(feature = "rouille")]
155    #[must_use]
156    pub fn as_rouille_response(&'static self, request: &rouille::Request) -> rouille::Response {
157        if let Some((path, data)) = self.get_file(&request.url()) {
158            let extension = path.rsplit_once('.').map_or(path, |(_left, right)| right);
159
160            rouille::Response::from_data(rouille::extension_to_mime(extension), data)
161        } else {
162            rouille::Response::empty_404()
163        }
164    }
165}