pub mod app;
pub mod cli;
pub mod s3;
pub mod watcher;
pub use app::{serve_app, DevServer, DEV_STATE_DIR};
pub use s3::{DevS3, DEFAULT_BUCKET as DEV_S3_BUCKET};
pub use watcher::{BuildDriver, WatchOptions, Watcher};
pub use mesofact::proxy;
pub use mesofact::server;
pub use mesofact::{DistPointer, Identity, ProxyMap, ProxyState, Server, DEFAULT_PORT};
#[cfg(feature = "ssr")]
pub use mesofact::{
revalidate, ssr, tenants, ResiliencePolicy, RetryPolicy, SsrChild, SsrSlot, SsrSpawnOptions,
DEFAULT_RESILIENCE_TIMEOUT_MS,
};
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use mesofact_publisher::ObjectStore;
use std::sync::Arc;
use tempfile::tempdir;
use tower::ServiceExt;
async fn body_string(response: axum::response::Response) -> String {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
fn deferred_workload() -> tempfile::TempDir {
let dir = tempdir().unwrap();
let dist = dir.path().join("dist");
std::fs::create_dir_all(dist.join("html")).unwrap();
std::fs::write(
dist.join("manifest.json"),
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}}]}"#,
)
.unwrap();
dir
}
async fn flip_instance(store: &Arc<dyn ObjectStore>, key: &str, content_root: &str) {
use mesofact_publisher::{ObjectPointerStore, Pointer, PointerStore};
ObjectPointerStore::new(store.clone())
.flip(
key,
Pointer { content_root: content_root.into(), source_root: None, published_at: None },
)
.await
.unwrap();
}
async fn put_bytes(store: &Arc<dyn ObjectStore>, key: &str, body: &'static [u8]) {
use mesofact_publisher::PutOpts;
store
.put(
key,
axum::body::Bytes::from_static(body),
PutOpts { content_type: "text/html".into(), content_hash: "h".into(), cache_control: None },
)
.await
.unwrap();
}
#[cfg(feature = "ssr")]
#[tokio::test]
async fn deferred_route_resolves_through_dev_s3_store() {
use mesofact_publisher::S3Store;
let dir = deferred_workload();
let dev = DevS3::start(dir.path().join("s3-surface"), DEV_S3_BUCKET)
.await
.unwrap();
let store: Arc<dyn ObjectStore> = Arc::new(
S3Store::new(dev.endpoint.clone(), dev.bucket.clone(), "auto", "dev", "dev").unwrap(),
);
flip_instance(&store, "c/xyz", "content/xyz.html").await;
put_bytes(&store, "content/xyz.html", b"<h1>via dev s3</h1>").await;
let app = Server::from_workload(dir.path())
.unwrap()
.with_instance_store(store)
.router();
let response = app
.oneshot(Request::builder().uri("/c/xyz").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("cache-control").unwrap(),
"public, max-age=31536000, immutable"
);
assert!(body_string(response).await.contains("via dev s3"));
}
#[cfg(feature = "ssr")]
#[tokio::test]
async fn ssr_route_resolves_r2_source_against_dev_s3() {
let dir = tempdir().unwrap();
let dev = DevS3::start(dir.path().join("s3-surface"), DEV_S3_BUCKET)
.await
.unwrap();
let put_url = format!("{}/{}/greeting.txt", dev.endpoint, dev.bucket);
let put = reqwest::Client::new()
.put(&put_url)
.body("hello from dev r2")
.send()
.await
.unwrap();
assert!(put.status().is_success(), "seed PUT status: {}", put.status());
std::fs::write(
dir.path().join("mesofact.config.toml"),
"[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",
)
.unwrap();
let server_dir = dir.path().join("dist/server");
std::fs::create_dir_all(&server_dir).unwrap();
std::fs::write(
server_dir.join("greet.js"),
"import { r2 } from \"@mesofact/runtime\";\n\
export default async function () {\n\
const bytes = await r2('assets').fetch('greeting.txt');\n\
const text = bytes ? new TextDecoder().decode(bytes) : null;\n\
return new Response(text ?? 'MISSING', { status: 200 });\n\
}\n",
)
.unwrap();
std::fs::write(
dir.path().join("dist/manifest.json"),
r#"{"routes": [{"route": "/greet", "mode": "ssr", "render_entrypoint": "dist/server/greet.js"}]}"#,
)
.unwrap();
let opts = ssr::SpawnOptions::new(
dir.path().to_path_buf(),
dir.path().join("dist"),
dir.path().join(".mesofact-dev"),
)
.with_env(dev.env_vars());
let child = ssr::spawn(opts).await.unwrap().expect("ssr present");
let resp = child
.dispatch(
"/greet",
mesofact::ssr_runtime::DispatchRequest {
method: "GET".into(),
url: "http://dev/greet".into(),
headers: vec![],
body: None,
},
)
.await
.unwrap();
assert_eq!(resp.status, 200);
assert_eq!(String::from_utf8(resp.body).unwrap(), "hello from dev r2");
}
}