Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
.cargo/
store
www/assets/_/
__pycache__/
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,34 @@ Generate [Datastar](https://data-star.dev) SSE events for hypermedia
interactions. Follows the
[SDK ADR](https://github.com/starfederation/datastar/blob/develop/sdk/ADR.md).

Use `--datastar` to serve the embedded JS bundle at `$DATASTAR_JS_PATH`
(`/datastar@1.0.0-RC.7.js`) with immutable cache headers:

```bash
$ http-nu --datastar :3001 ./serve.nu
```

```nushell
use http-nu/datastar *
use http-nu/html *

{|req|
HTML (
HEAD (
SCRIPT {type: "module" src: $DATASTAR_JS_PATH}
)
) (
BODY (
DIV {"data-signals": "{count: 0}"} (
SPAN {"data-text": "$count"} "0"
) (
BUTTON {"data-on:click": "$count++"} "+1"
)
)
)
}
```

Commands return records that pipe to `to sse` for streaming output.

```nushell
Expand Down
20 changes: 20 additions & 0 deletions examples/datastar-counter/serve.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use http-nu/datastar *
use http-nu/html *

# Run: http-nu --datastar :3001 examples/datastar-counter/serve.nu

{|req|
HTML (
HEAD (
SCRIPT {type: "module" src: $DATASTAR_JS_PATH}
)
) (
BODY (
DIV {"data-signals": "{count: 0}"} (
SPAN {"data-text": "$count"} "0"
) (
BUTTON {"data-on:click": "$count++"} "+1"
)
)
)
}
2 changes: 1 addition & 1 deletion examples/datastar-sdk/serve.nu
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use http-nu/html *
HEAD
(META {charset: "UTF-8"})
(TITLE "Datastar SDK Demo")
(SCRIPT {type: "module" src: $DATASTAR_CDN_URL})
(SCRIPT {type: "module" src: $DATASTAR_JS_PATH})
)
(
BODY {"data-signals": "{count: 0}"}
Expand Down
2 changes: 1 addition & 1 deletion examples/quotes/serve.nu
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def quote-html []: record -> record {
(META {charset: "utf-8"})
(TITLE "Live Quotes")
(STYLE "* { box-sizing: border-box; margin: 0; }")
(SCRIPT {type: "module" src: $DATASTAR_CDN_URL})
(SCRIPT {type: "module" src: $DATASTAR_JS_PATH})
)
(
BODY {data-init: "@get('/')"}
Expand Down
46 changes: 45 additions & 1 deletion src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ use crate::worker::{spawn_eval_thread, PipelineResult};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
type HTTPResult = Result<hyper::Response<BoxBody<Bytes, BoxError>>, BoxError>;

const DATASTAR_JS_PATH: &str = "/datastar@1.0.0-RC.7.js";
const DATASTAR_JS: &[u8] = include_bytes!("stdlib/datastar/datastar@1.0.0-RC.7.js");
const DATASTAR_JS_BROTLI: &[u8] = include_bytes!("stdlib/datastar/datastar@1.0.0-RC.7.js.br");

pub async fn handle<B>(
engine: Arc<ArcSwap<crate::Engine>>,
addr: Option<SocketAddr>,
trusted_proxies: Arc<Vec<ipnet::IpNet>>,
datastar: Arc<bool>,
req: hyper::Request<B>,
) -> Result<hyper::Response<BoxBody<Bytes, BoxError>>, BoxError>
where
Expand All @@ -33,7 +38,7 @@ where
{
// Load current engine snapshot - lock-free atomic operation
let engine = engine.load_full();
match handle_inner(engine, addr, trusted_proxies, req).await {
match handle_inner(engine, addr, trusted_proxies, datastar, req).await {
Ok(response) => Ok(response),
Err(err) => {
eprintln!("Error handling request: {err}");
Expand All @@ -51,6 +56,7 @@ async fn handle_inner<B>(
engine: Arc<crate::Engine>,
addr: Option<SocketAddr>,
trusted_proxies: Arc<Vec<ipnet::IpNet>>,
datastar: Arc<bool>,
req: hyper::Request<B>,
) -> HTTPResult
where
Expand Down Expand Up @@ -136,6 +142,44 @@ where
// Phase 1: Log request
log_request(request_id, &request);

// Built-in route: serve embedded Datastar JS bundle (requires --datastar flag)
if *datastar && request.path == DATASTAR_JS_PATH {
let use_brotli = compression::accepts_brotli(&parts.headers);
let mut header_map = hyper::header::HeaderMap::new();
header_map.insert(
hyper::header::CONTENT_TYPE,
hyper::header::HeaderValue::from_static("application/javascript"),
);
header_map.insert(
hyper::header::CACHE_CONTROL,
hyper::header::HeaderValue::from_static("public, max-age=31536000, immutable"),
);
let body = if use_brotli {
header_map.insert(
hyper::header::CONTENT_ENCODING,
hyper::header::HeaderValue::from_static("br"),
);
header_map.insert(
hyper::header::VARY,
hyper::header::HeaderValue::from_static("accept-encoding"),
);
Full::new(Bytes::from_static(DATASTAR_JS_BROTLI))
.map_err(|never| match never {})
.boxed()
} else {
Full::new(Bytes::from_static(DATASTAR_JS))
.map_err(|never| match never {})
.boxed()
};
log_response(request_id, 200, &header_map, start_time);
let logging_body = LoggingBody::new(body, guard);
let mut response = hyper::Response::builder()
.status(200)
.body(logging_body.boxed())?;
*response.headers_mut() = header_map;
return Ok(response);
}

let reload_token = engine.reload_token.clone();
let (meta_rx, bridged_body) = spawn_eval_thread(engine, request, stream);

Expand Down
10 changes: 9 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ struct Args {
)]
expose: Option<String>,

/// Serve the embedded Datastar JS bundle at /datastar@<version>.js
#[clap(long)]
datastar: bool,

/// Trust proxies from these CIDR ranges for X-Forwarded-For parsing
#[clap(long = "trust-proxy", value_name = "CIDR")]
trust_proxies: Vec<ipnet::IpNet>,
Expand Down Expand Up @@ -284,6 +288,7 @@ async fn serve(
mut rx: mpsc::Receiver<Engine>,
interrupt: Arc<AtomicBool>,
trusted_proxies: Vec<ipnet::IpNet>,
datastar: bool,
start_time: std::time::Instant,
startup_options: StartupOptions,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Expand Down Expand Up @@ -341,6 +346,7 @@ async fn serve(

// Wrap trusted_proxies in Arc for sharing across connections
let trusted_proxies = Arc::new(trusted_proxies);
let datastar = Arc::new(datastar);

let shutdown = shutdown_signal(interrupt.clone());
tokio::pin!(shutdown);
Expand All @@ -353,9 +359,10 @@ async fn serve(
let io = TokioIo::new(stream);
let engine = engine.clone();
let trusted_proxies = trusted_proxies.clone();
let datastar = datastar.clone();

let service = service_fn(move |req| {
handle(engine.clone(), remote_addr, trusted_proxies.clone(), req)
handle(engine.clone(), remote_addr, trusted_proxies.clone(), datastar.clone(), req)
});

// serve_connection_with_upgrades supports HTTP/1 and HTTP/2
Expand Down Expand Up @@ -645,6 +652,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
rx,
interrupt,
args.trust_proxies,
args.datastar,
std::time::Instant::now(),
startup_options,
)
Expand Down
Loading