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 #[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}