1pub mod app;
36pub mod cli;
37pub mod s3;
38pub mod watcher;
39
40pub use app::{serve_app, DevServer, DEV_STATE_DIR};
41pub use s3::{DevS3, DEFAULT_BUCKET as DEV_S3_BUCKET};
42pub use watcher::{BuildDriver, WatchOptions, Watcher};
43
44pub use mesofact::proxy;
46pub use mesofact::server;
47pub use mesofact::{DistPointer, Identity, ProxyMap, ProxyState, Server, DEFAULT_PORT};
48#[cfg(feature = "ssr")]
49pub use mesofact::{
50 revalidate, ssr, tenants, ResiliencePolicy, RetryPolicy, SsrChild, SsrSlot, SsrSpawnOptions,
51 DEFAULT_RESILIENCE_TIMEOUT_MS,
52};
53
54#[cfg(test)]
55mod tests {
56 use super::*;
62 use axum::body::Body;
63 use axum::http::{Request, StatusCode};
64 use mesofact_publisher::ObjectStore;
65 use std::sync::Arc;
66 use tempfile::tempdir;
67 use tower::ServiceExt;
68
69 async fn body_string(response: axum::response::Response) -> String {
70 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
71 .await
72 .unwrap();
73 String::from_utf8(bytes.to_vec()).unwrap()
74 }
75
76 fn deferred_workload() -> tempfile::TempDir {
77 let dir = tempdir().unwrap();
78 let dist = dir.path().join("dist");
79 std::fs::create_dir_all(dist.join("html")).unwrap();
80 std::fs::write(
81 dist.join("manifest.json"),
82 r#"{"version":"1","build_id":"b","routes":[{"route":"/c/:slug","mode":"static","render_entrypoint":"dist/server/c_slug.js","cache_policy":{"ttl":0},"prerender":{"deferred":true}}]}"#,
83 )
84 .unwrap();
85 dir
86 }
87
88 async fn flip_instance(store: &Arc<dyn ObjectStore>, key: &str, content_root: &str) {
89 use mesofact_publisher::{ObjectPointerStore, Pointer, PointerStore};
90 ObjectPointerStore::new(store.clone())
91 .flip(
92 key,
93 Pointer { content_root: content_root.into(), source_root: None, published_at: None },
94 )
95 .await
96 .unwrap();
97 }
98
99 async fn put_bytes(store: &Arc<dyn ObjectStore>, key: &str, body: &'static [u8]) {
100 use mesofact_publisher::PutOpts;
101 store
102 .put(
103 key,
104 axum::body::Bytes::from_static(body),
105 PutOpts { content_type: "text/html".into(), content_hash: "h".into(), cache_control: None },
106 )
107 .await
108 .unwrap();
109 }
110
111 #[cfg(feature = "ssr")]
118 #[tokio::test]
119 async fn deferred_route_resolves_through_dev_s3_store() {
120 use mesofact_publisher::S3Store;
121
122 let dir = deferred_workload();
123 let dev = DevS3::start(dir.path().join("s3-surface"), DEV_S3_BUCKET)
124 .await
125 .unwrap();
126 let store: Arc<dyn ObjectStore> = Arc::new(
127 S3Store::new(dev.endpoint.clone(), dev.bucket.clone(), "auto", "dev", "dev").unwrap(),
128 );
129
130 flip_instance(&store, "c/xyz", "content/xyz.html").await;
133 put_bytes(&store, "content/xyz.html", b"<h1>via dev s3</h1>").await;
134
135 let app = Server::from_workload(dir.path())
136 .unwrap()
137 .with_instance_store(store)
138 .router();
139 let response = app
140 .oneshot(Request::builder().uri("/c/xyz").body(Body::empty()).unwrap())
141 .await
142 .unwrap();
143 assert_eq!(response.status(), StatusCode::OK);
144 assert_eq!(
145 response.headers().get("cache-control").unwrap(),
146 "public, max-age=31536000, immutable"
147 );
148 assert!(body_string(response).await.contains("via dev s3"));
149 }
150
151 #[cfg(feature = "ssr")]
158 #[tokio::test]
159 async fn ssr_route_resolves_r2_source_against_dev_s3() {
160 let dir = tempdir().unwrap();
161 let dev = DevS3::start(dir.path().join("s3-surface"), DEV_S3_BUCKET)
162 .await
163 .unwrap();
164
165 let put_url = format!("{}/{}/greeting.txt", dev.endpoint, dev.bucket);
168 let put = reqwest::Client::new()
169 .put(&put_url)
170 .body("hello from dev r2")
171 .send()
172 .await
173 .unwrap();
174 assert!(put.status().is_success(), "seed PUT status: {}", put.status());
175
176 std::fs::write(
177 dir.path().join("mesofact.config.toml"),
178 "[sources.assets]\nkind = \"r2\"\nbucket = \"dev\"\nendpoint_env = \"R2_ENDPOINT\"\naccess_key_id_env = \"R2_ACCESS_KEY_ID\"\nsecret_access_key_env = \"R2_SECRET_ACCESS_KEY\"\n",
179 )
180 .unwrap();
181
182 let server_dir = dir.path().join("dist/server");
183 std::fs::create_dir_all(&server_dir).unwrap();
184 std::fs::write(
185 server_dir.join("greet.js"),
186 "import { r2 } from \"@mesofact/runtime\";\n\
187 export default async function () {\n\
188 const bytes = await r2('assets').fetch('greeting.txt');\n\
189 const text = bytes ? new TextDecoder().decode(bytes) : null;\n\
190 return new Response(text ?? 'MISSING', { status: 200 });\n\
191 }\n",
192 )
193 .unwrap();
194 std::fs::write(
195 dir.path().join("dist/manifest.json"),
196 r#"{"routes": [{"route": "/greet", "mode": "ssr", "render_entrypoint": "dist/server/greet.js"}]}"#,
197 )
198 .unwrap();
199
200 let opts = ssr::SpawnOptions::new(
201 dir.path().to_path_buf(),
202 dir.path().join("dist"),
203 dir.path().join(".mesofact-dev"),
204 )
205 .with_env(dev.env_vars());
206 let child = ssr::spawn(opts).await.unwrap().expect("ssr present");
207
208 let resp = child
209 .dispatch(
210 "/greet",
211 mesofact::ssr_runtime::DispatchRequest {
212 method: "GET".into(),
213 url: "http://dev/greet".into(),
214 headers: vec![],
215 body: None,
216 },
217 )
218 .await
219 .unwrap();
220 assert_eq!(resp.status, 200);
221 assert_eq!(String::from_utf8(resp.body).unwrap(), "hello from dev r2");
222 }
223}