From 16f82ea631517a8d0dfecc0093795bb2512a2fce Mon Sep 17 00:00:00 2001 From: Andy Gayton Date: Thu, 26 Feb 2026 03:55:57 +0000 Subject: [PATCH 1/5] feat: replace $env.HTTP_NU with immutable $HTTP_NU const Exposes all CLI options (dev, datastar, watch, store, topic, expose, tls, services) as an immutable const record so scripts can inspect how the server was started. --- README.md | 13 +++++++++++++ src/engine.rs | 37 +++++++++++++++++++++++++++++++------ src/main.rs | 37 ++++++++++++++++++++++++++++++++----- src/stdlib/http/mod.nu | 2 +- src/test_handler.rs | 7 ++++++- tests/eval_test.rs | 26 ++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 96f437e..ede254f 100644 --- a/README.md +++ b/README.md @@ -793,6 +793,19 @@ Make module paths available with `-I` / `--include-path`: $ http-nu -I ./lib -I ./vendor :3001 '{|req| use mymod.nu; ...}' ``` +### Runtime Constants + +The `$HTTP_NU` const is available in all scripts and reflects the CLI options +the server was started with: + +```nushell +$HTTP_NU +# => {dev: false, datastar: true, watch: false, store: "./store", topic: null, expose: null, tls: null, services: false} + +$HTTP_NU.store != null # check if store is available +$HTTP_NU.dev # true when --dev was passed +``` + ### Embedded Modules #### Routing diff --git a/src/engine.rs b/src/engine.rs index 78f384d..a7dbafb 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -26,6 +26,19 @@ use crate::logging::log_error; use crate::stdlib::load_http_nu_stdlib; use crate::Error; +/// CLI options exposed to scripts as the `$HTTP_NU` const +#[derive(Clone, Default)] +pub struct HttpNuOptions { + pub dev: bool, + pub datastar: bool, + pub watch: bool, + pub store: Option, + pub topic: Option, + pub expose: Option, + pub tls: Option, + pub services: bool, +} + #[derive(Clone)] pub struct Engine { pub state: EngineState, @@ -55,18 +68,30 @@ impl Engine { }) } - /// Sets `$env.HTTP_NU` with server configuration for stdlib modules - pub fn set_http_nu_env(&mut self, dev: bool) -> Result<(), Error> { - let mut stack = Stack::new(); + /// Sets `$HTTP_NU` const with server configuration for stdlib modules + pub fn set_http_nu_const(&mut self, options: &HttpNuOptions) -> Result<(), Error> { let span = Span::unknown(); + let opt_str = |v: &Option| match v { + Some(s) => Value::string(s, span), + None => Value::nothing(span), + }; let record = Value::record( nu_protocol::record! { - "dev" => Value::bool(dev, span), + "dev" => Value::bool(options.dev, span), + "datastar" => Value::bool(options.datastar, span), + "watch" => Value::bool(options.watch, span), + "store" => opt_str(&options.store), + "topic" => opt_str(&options.topic), + "expose" => opt_str(&options.expose), + "tls" => opt_str(&options.tls), + "services" => Value::bool(options.services, span), }, span, ); - stack.add_env_var("HTTP_NU".to_string(), record); - self.state.merge_env(&mut stack)?; + let mut working_set = StateWorkingSet::new(&self.state); + let var_id = working_set.add_variable(b"$HTTP_NU".into(), span, Type::record(), false); + working_set.set_variable_const_val(var_id, record); + self.state.merge_delta(working_set.render())?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index a3c2fba..5875dfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::time::Duration; use arc_swap::ArcSwap; use clap::Parser; use http_nu::{ - engine::script_to_engine, + engine::{script_to_engine, HttpNuOptions}, handler::{handle, AppConfig}, listener::TlsConfig, logging::{ @@ -138,12 +138,12 @@ fn create_base_engine( plugins: &[PathBuf], include_paths: &[PathBuf], store: Option<&Store>, - dev: bool, + options: &HttpNuOptions, ) -> Result> { let mut engine = Engine::new()?; engine.add_custom_commands()?; engine.set_lib_dirs(include_paths)?; - engine.set_http_nu_env(dev)?; + engine.set_http_nu_const(options)?; for plugin_path in plugins { engine.load_plugin(plugin_path)?; @@ -539,7 +539,10 @@ async fn main() -> Result<(), Box> { let mut engine = Engine::new()?; engine.add_custom_commands()?; engine.set_lib_dirs(&args.include_paths)?; - engine.set_http_nu_env(args.dev)?; + engine.set_http_nu_const(&HttpNuOptions { + dev: args.dev, + ..Default::default() + })?; for plugin_path in &args.plugins { engine.load_plugin(plugin_path)?; @@ -593,13 +596,37 @@ async fn main() -> Result<(), Box> { #[cfg(not(feature = "cross-stream"))] let store: Option = None; + // Build $HTTP_NU options from CLI args + let http_nu_options = HttpNuOptions { + dev: args.dev, + datastar: args.datastar, + watch: args.watch, + tls: args.tls.as_ref().map(|p| p.display().to_string()), + #[cfg(feature = "cross-stream")] + store: args.store.as_ref().map(|p| p.display().to_string()), + #[cfg(not(feature = "cross-stream"))] + store: None, + #[cfg(feature = "cross-stream")] + topic: args.topic.clone(), + #[cfg(not(feature = "cross-stream"))] + topic: None, + #[cfg(feature = "cross-stream")] + expose: args.expose.clone(), + #[cfg(not(feature = "cross-stream"))] + expose: None, + #[cfg(feature = "cross-stream")] + services: args.services, + #[cfg(not(feature = "cross-stream"))] + services: false, + }; + // Create base engine with commands, signals, and plugins let base_engine = create_base_engine( interrupt.clone(), &args.plugins, &args.include_paths, store.as_ref(), - args.dev, + &http_nu_options, )?; // Source: --topic (direct store read, with optional watch for live-reload) diff --git a/src/stdlib/http/mod.nu b/src/stdlib/http/mod.nu index f9478a4..149ead1 100644 --- a/src/stdlib/http/mod.nu +++ b/src/stdlib/http/mod.nu @@ -42,7 +42,7 @@ export def "cookie set" [ $"($name)=($value)" $"Path=($path)" (if not $no_httponly { "HttpOnly" }) - (if (not $env.HTTP_NU.dev) and (not $no_secure) { "Secure" }) + (if (not $HTTP_NU.dev) and (not $no_secure) { "Secure" }) $"SameSite=($same_site)" (if $max_age != null { $"Max-Age=($max_age)" }) (if $domain != null { $"Domain=($domain)" }) diff --git a/src/test_handler.rs b/src/test_handler.rs index 31b6cb1..4f423b4 100644 --- a/src/test_handler.rs +++ b/src/test_handler.rs @@ -346,7 +346,12 @@ fn test_engine_with_dev(script: &str, dev: bool) -> crate::Engine { Box::new(PrintCommand::new()), ]) .unwrap(); - engine.set_http_nu_env(dev).unwrap(); + engine + .set_http_nu_const(&crate::engine::HttpNuOptions { + dev, + ..Default::default() + }) + .unwrap(); engine.parse_closure(script, None).unwrap(); engine } diff --git a/tests/eval_test.rs b/tests/eval_test.rs index 01fc752..b48a338 100644 --- a/tests/eval_test.rs +++ b/tests/eval_test.rs @@ -73,6 +73,32 @@ fn test_eval_print() { .stdout(predicates::str::contains(r#""content":"hello""#)); } +#[test] +fn test_http_nu_const_defaults() { + Command::new(assert_cmd::cargo::cargo_bin!("http-nu")) + .args(["eval", "-c", "$HTTP_NU.dev"]) + .assert() + .success() + .stdout("false\n"); +} + +#[test] +fn test_http_nu_const_dev_flag() { + Command::new(assert_cmd::cargo::cargo_bin!("http-nu")) + .args(["--dev", "eval", "-c", "$HTTP_NU.dev"]) + .assert() + .success() + .stdout("true\n"); +} + +#[test] +fn test_http_nu_const_is_immutable() { + Command::new(assert_cmd::cargo::cargo_bin!("http-nu")) + .args(["eval", "-c", "$HTTP_NU = {}"]) + .assert() + .failure(); +} + #[test] fn test_eval_syntax_error() { Command::new(assert_cmd::cargo::cargo_bin!("http-nu")) From ca82514b63c18cd70f3c8f9c843b5695a331dfdf Mon Sep 17 00:00:00 2001 From: Andy Gayton Date: Thu, 26 Feb 2026 14:27:49 +0000 Subject: [PATCH 2/5] feat: add examples hub with mountable sub-handlers Root examples/serve.nu mounts individual examples under path prefixes. All sub-handlers use relative ./ paths so they work both standalone and mounted. Store-dependent examples (quotes) are conditionally mounted based on $HTTP_NU.store and greyed out in the index when unavailable. - basic: use relative paths, drain $in before generate for /time - datastar-sdk: @post('./increment') etc. - mermaid-editor: ./mermaid-diagram.js, @post('./') - quotes: .head -> .last, add threshold-gate filter, @get('./') - templates: seed store topics on startup, relative nav links --- examples/README.md | 38 +++++++++++++ examples/basic.nu | 11 ++-- examples/datastar-sdk/serve.nu | 6 +-- examples/mermaid-editor/serve.nu | 4 +- examples/quotes/serve.nu | 21 +++++++- examples/serve.nu | 85 ++++++++++++++++++++++++++++++ examples/templates/nav.html | 2 +- examples/templates/serve.nu | 11 +++- examples/templates/topics/nav.html | 2 +- 9 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/serve.nu diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..fd725ce --- /dev/null +++ b/examples/README.md @@ -0,0 +1,38 @@ +# Examples + +## Running all examples + +The examples hub mounts individual examples under one server: + +```bash +http-nu --datastar :3001 examples/serve.nu +``` + +With a store (enables quotes): + +```bash +http-nu --datastar --store ./store :3001 examples/serve.nu +``` + +Then visit http://localhost:3001. + +## Individual examples + +Each example can also be run standalone. + +| Example | Command | Description | +|---------|---------|-------------| +| basic | `http-nu :3001 examples/basic.nu` | Minimal routes, JSON, streaming, POST echo | +| datastar-counter | `http-nu --datastar :3001 examples/datastar-counter/serve.nu` | Client-side reactive counter | +| datastar-sdk | `http-nu --datastar :3001 examples/datastar-sdk/serve.nu` | Datastar SDK feature demo | +| mermaid-editor | `http-nu --datastar :3001 examples/mermaid-editor/serve.nu` | Live Mermaid diagram editor | +| templates | `http-nu --datastar --store ./store :3001 examples/templates/serve.nu` | `.mj` file, inline, and topic modes | +| quotes | `http-nu --datastar --store ./store :3001 examples/quotes/serve.nu` | Live quotes board with SSE | +| tao | `http-nu --datastar --dev -w :3001 examples/tao/serve.nu` | The Tao of Datastar | + +## Store-dependent examples + +Quotes and the `/topic` route in templates require `--store`. The hub +detects `$HTTP_NU.store` at runtime and greys out unavailable examples. +When `--store` is provided, templates automatically seeds its topics on +startup. diff --git a/examples/basic.nu b/examples/basic.nu index 7e7247e..aa5d809 100644 --- a/examples/basic.nu +++ b/examples/basic.nu @@ -9,11 +9,11 @@ "

http-nu demo

" } @@ -52,6 +52,7 @@ # Time stream example "/time" => { + let _ = $in generate {|_| sleep 1sec {out: $"Current time: (date now | format date '%Y-%m-%d %H:%M:%S')\n" next: true} diff --git a/examples/datastar-sdk/serve.nu b/examples/datastar-sdk/serve.nu index 96a1ed2..1aac4e6 100644 --- a/examples/datastar-sdk/serve.nu +++ b/examples/datastar-sdk/serve.nu @@ -24,18 +24,18 @@ use http-nu/html * DIV (H3 "to datastar-patch-signals") (P "Count: " (SPAN {"data-text": "$count"} "0")) - (BUTTON {"data-on:click": "@post('/increment')"} "Increment") + (BUTTON {"data-on:click": "@post('./increment')"} "Increment") ) ( DIV (H3 "to datastar-execute-script") - (BUTTON {"data-on:click": "@post('/hello')"} "Say Hello") + (BUTTON {"data-on:click": "@post('./hello')"} "Say Hello") ) ( DIV (H3 "to datastar-patch-elements") (DIV {id: "time"} "--:--:--.---") - (BUTTON {"data-on:click": "@post('/time')"} "Get Time") + (BUTTON {"data-on:click": "@post('./time')"} "Get Time") ) ) ) diff --git a/examples/mermaid-editor/serve.nu b/examples/mermaid-editor/serve.nu index 9dac13e..997a807 100644 --- a/examples/mermaid-editor/serve.nu +++ b/examples/mermaid-editor/serve.nu @@ -38,7 +38,7 @@ def mermaid-el []: string -> record { (META {name: "viewport" content: "width=device-width, initial-scale=1"}) (TITLE "dia2") (SCRIPT {type: "module" src: $DATASTAR_JS_PATH}) - (SCRIPT {type: "module" src: "/mermaid-diagram.js"}) + (SCRIPT {type: "module" src: "./mermaid-diagram.js"}) (STYLE " * { box-sizing: border-box; margin: 0; padding: 0; } body { height: 100dvh; display: flex; font-family: system-ui, sans-serif; } @@ -61,7 +61,7 @@ mermaid-diagram { (DIV {class: "pane"} (TEXTAREA { "data-bind:source": true - "data-on:input__debounce.500ms": "@post('/')" + "data-on:input__debounce.500ms": "@post('./')" } $default_source) ) (DIV {class: "pane"} diff --git a/examples/quotes/serve.nu b/examples/quotes/serve.nu index 13a5a2b..2b186a4 100644 --- a/examples/quotes/serve.nu +++ b/examples/quotes/serve.nu @@ -2,6 +2,21 @@ use http-nu/router * use http-nu/datastar * use http-nu/html * +# Filter for `.last --follow` / `.cat --follow` streams. +# Buffers frames until xs.threshold is seen, then emits the last +# buffered frame (or null if none) followed by all subsequent frames. +def threshold-gate [] { + generate {|frame, state = {}| + if $frame.topic == "xs.threshold" { + return {out: $state.last?, next: {reached: true}} + } + if ("reached" in $state) { + return {out: $frame, next: $state} + } + {next: ($state | upsert last $frame)} + } +} + def quote-html []: record -> record { let q = $in ( @@ -45,7 +60,9 @@ def quote-html []: record -> record { dispatch $req [ ( route {method: GET path: "/" has-header: {accept: "text/event-stream"}} {|req ctx| - .head quotes --follow + .last quotes --follow + | threshold-gate + | default {meta: {quote: "Waiting for quotes..."}} | each {|frame| $frame.meta | quote-html | to datastar-patch-elements } @@ -72,7 +89,7 @@ def quote-html []: record -> record { (SCRIPT {type: "module" src: $DATASTAR_JS_PATH}) ) ( - BODY {data-init: "@get('/')"} + BODY {data-init: "@get('./')"} ({quote: "Waiting for quotes..."} | quote-html) ) ) diff --git a/examples/serve.nu b/examples/serve.nu new file mode 100644 index 0000000..36db0b7 --- /dev/null +++ b/examples/serve.nu @@ -0,0 +1,85 @@ +# http-nu examples hub +# +# Run: http-nu --datastar :3001 examples/serve.nu +# With store: http-nu --datastar --store ./store :3001 examples/serve.nu + +use http-nu/router * +use http-nu/html * + +let basic = source basic.nu +let counter = source datastar-counter/serve.nu +let sdk = source datastar-sdk/serve.nu +let mermaid = source mermaid-editor/serve.nu +let templates = source templates/serve.nu +let quotes = source quotes/serve.nu + +let has_store = $HTTP_NU.store != null + +def mount [prefix: string handler: closure] { + route {|req| + if ($req.path == $prefix) or ($req.path | str starts-with $"($prefix)/") { + {prefix: $prefix} + } + } {|req ctx| + let body = $in + if $req.path == $ctx.prefix { + # Redirect to trailing slash for correct relative URL resolution + "" | metadata set { merge {'http.response': {status: 302 headers: {location: $"($ctx.prefix)/"}}} } + } else { + let path = $req.path | str replace $ctx.prefix "" + $body | do $handler ($req | upsert path $path) + } + } +} + +def example-link [href: string label: string desc: string --disabled] { + if $disabled { + LI (SPAN {style: {color: "#9ca3af"}} $label) $" — ($desc) " (SPAN {style: {color: "#9ca3af" font-size: "0.85em"}} "(requires --store)") + } else { + LI (A {href: $href} $label) $" — ($desc)" + } +} + +let routes = [ + ( + route {method: GET path: "/"} {|req ctx| + HTML ( + HEAD + (META {charset: "UTF-8"}) + (META {name: "viewport" content: "width=device-width, initial-scale=1"}) + (TITLE "http-nu examples") + (STYLE " +body { font-family: system-ui, sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; } +a { color: #2563eb; } +li { margin: 0.5rem 0; } +") + ) ( + BODY + (H1 "http-nu examples") + (UL + (example-link "./basic/" "basic" "minimal routes, JSON, streaming") + (example-link "./datastar-counter/" "datastar-counter" "reactive counter") + (example-link "./datastar-sdk/" "datastar-sdk" "SDK feature demo") + (example-link "./mermaid-editor/" "mermaid-editor" "live diagram editor") + (example-link "./templates/" "templates" ".mj template modes") + (example-link "./quotes/" "quotes" "live quotes board" --disabled=(not $has_store)) + ) + ) + } + ) + + (mount "/basic" $basic) + (mount "/datastar-counter" $counter) + (mount "/datastar-sdk" $sdk) + (mount "/mermaid-editor" $mermaid) + (mount "/templates" $templates) + ...(if $has_store { + [(mount "/quotes" $quotes)] + } else { + [] + }) +] + +{|req| + dispatch $req $routes +} diff --git a/examples/templates/nav.html b/examples/templates/nav.html index d14b29b..9f0b7c3 100644 --- a/examples/templates/nav.html +++ b/examples/templates/nav.html @@ -1 +1 @@ - + diff --git a/examples/templates/serve.nu b/examples/templates/serve.nu index 29d4389..f84662a 100644 --- a/examples/templates/serve.nu +++ b/examples/templates/serve.nu @@ -1,3 +1,10 @@ +# Seed store topics from disk files when --store is enabled +if $HTTP_NU.store != null { + open examples/templates/topics/page.html | .append page.html + open examples/templates/topics/base.html | .append base.html + open examples/templates/topics/nav.html | .append nav.html +} + {|req| match $req.path { "/file" => { {name: "World"} | .mj "examples/templates/page.html" } @@ -6,8 +13,8 @@ {} | .mj --inline '

Templates

This page is rendered with .mj --inline.

' } } diff --git a/examples/templates/topics/nav.html b/examples/templates/topics/nav.html index ca055fd..4401070 100644 --- a/examples/templates/topics/nav.html +++ b/examples/templates/topics/nav.html @@ -1 +1 @@ - + From cd39cdeaa078022718d04c3b48b10e33c0696f83 Mon Sep 17 00:00:00 2001 From: Andy Gayton Date: Thu, 26 Feb 2026 15:20:44 +0000 Subject: [PATCH 3/5] feat: add mount to router module, wire examples into www Move mount from inline defs into http-nu/router so it's reusable. Fixes path stripping (substring instead of str replace), adds $req.mount_prefix for sub-handlers that need absolute paths. www/serve.nu mounts examples hub at /examples/ with nav link. --- examples/serve.nu | 21 ++------------------- src/stdlib/router/mod.nu | 28 ++++++++++++++++++++++++++++ www/serve.nu | 5 +++++ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/examples/serve.nu b/examples/serve.nu index 36db0b7..cd8d76b 100644 --- a/examples/serve.nu +++ b/examples/serve.nu @@ -15,28 +15,11 @@ let quotes = source quotes/serve.nu let has_store = $HTTP_NU.store != null -def mount [prefix: string handler: closure] { - route {|req| - if ($req.path == $prefix) or ($req.path | str starts-with $"($prefix)/") { - {prefix: $prefix} - } - } {|req ctx| - let body = $in - if $req.path == $ctx.prefix { - # Redirect to trailing slash for correct relative URL resolution - "" | metadata set { merge {'http.response': {status: 302 headers: {location: $"($ctx.prefix)/"}}} } - } else { - let path = $req.path | str replace $ctx.prefix "" - $body | do $handler ($req | upsert path $path) - } - } -} - def example-link [href: string label: string desc: string --disabled] { if $disabled { - LI (SPAN {style: {color: "#9ca3af"}} $label) $" — ($desc) " (SPAN {style: {color: "#9ca3af" font-size: "0.85em"}} "(requires --store)") + LI (SPAN {style: {color: "#9ca3af"}} $label) $" --($desc) " (SPAN {style: {color: "#9ca3af" font-size: "0.85em"}} "(requires --store)") } else { - LI (A {href: $href} $label) $" — ($desc)" + LI (A {href: $href} $label) $" --($desc)" } } diff --git a/src/stdlib/router/mod.nu b/src/stdlib/router/mod.nu index 8806730..cf4ad05 100644 --- a/src/stdlib/router/mod.nu +++ b/src/stdlib/router/mod.nu @@ -225,6 +225,34 @@ def dispatch-execute [ # # Routes are tested in order until one matches (returns non-null context). # The matched route's handler receives the request, context, and body as $in. +# Mount a sub-handler under a path prefix +# +# Requests matching the prefix are forwarded to the handler with the prefix +# stripped from $req.path. Bare prefix requests (without trailing slash) get +# a 302 redirect so relative URLs resolve correctly. +# +# The handler receives $req with: +# - path: prefix stripped (e.g. "/examples/basic/" becomes "/basic/") +# - mount_prefix: the full prefix chain (composes for nested mounts) +# +# The request body ($in) streams through to the handler. +export def mount [prefix: string handler: closure] { + route {|req| + if ($req.path == $prefix) or ($req.path | str starts-with $"($prefix)/") { + {prefix: $prefix} + } + } {|req ctx| + let body = $in + if $req.path == $ctx.prefix { + "" | metadata set { merge {'http.response': {status: 302 headers: {location: $"($ctx.prefix)/"}}} } + } else { + let path = $req.path | str substring ($ctx.prefix | str length).. + let base = $req.mount_prefix? | default "" + $body | do $handler ($req | upsert path $path | upsert mount_prefix $"($base)($ctx.prefix)") + } + } +} + @example "dispatch to matching route" { let routes = [ (route {path: "/health"} {|req ctx| "OK" }) diff --git a/www/serve.nu b/www/serve.nu index d4fc2b4..1b8ee0f 100644 --- a/www/serve.nu +++ b/www/serve.nu @@ -76,6 +76,7 @@ def header-bar [] { (DIV {class: [font-mono text-header text-fluid-xl font-bold]} "http-nu") ( NAV {class: [flex gap-4]} [ + (A {href: "/examples/"} "Examples") (A {href: "https://github.com/cablehead/http-nu"} "GitHub") (A {href: "https://discord.gg/sGgYVKnk73"} "Discord") ] @@ -349,8 +350,12 @@ def install-section [] { ] } +let examples = source ../examples/serve.nu + {|req| dispatch $req [ + (mount "/examples" $examples) + ( route {method: GET path: "/syntax.css"} {|req ctx| .highlight theme Dracula | metadata set --content-type "text/css" From f261cb372e380a47a4b3038bbcf21250422de684 Mon Sep 17 00:00:00 2001 From: Andy Gayton Date: Thu, 26 Feb 2026 15:36:07 +0000 Subject: [PATCH 4/5] docs: document mount and examples hub in README --- README.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ede254f..0e87ad8 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,12 @@ Or from a file: $ http-nu :3001 ./serve.nu ``` -Check out the [`examples/basic.nu`](examples/basic.nu) file in the repository -for a complete example that implements a mini web server with multiple routes, -form handling, and streaming responses. +Run all examples under one server with the [examples hub](examples/README.md): + +```bash +$ http-nu --datastar :3001 examples/serve.nu +$ http-nu --datastar --store ./store :3001 examples/serve.nu # enables store-dependent examples +``` ### UNIX domain sockets @@ -848,6 +851,28 @@ Routes match in order. First match wins. Closure tests return a record (match, context passed to handler) or null (no match). If no routes match, returns `501 Not Implemented`. +**Mounting sub-handlers:** + +`mount` serves a handler under a path prefix. Requests to `/prefix` redirect to +`/prefix/`, then the prefix is stripped before dispatching to the handler. +Sub-handlers see `$req.mount_prefix` for absolute URL reconstruction. + +```nushell +let api = source api/serve.nu +let docs = source docs/serve.nu + +{|req| + dispatch $req [ + (mount "/api" $api) + (mount "/docs" $docs) + (route true {|req ctx| "Not Found"}) + ] +} +``` + +Mounts compose -- a mounted handler can mount further sub-handlers, and +`$req.mount_prefix` accumulates the full prefix chain. + #### HTML DSL Build HTML with Nushell. Lisp-style nesting with uppercase tags. From b06c4ec0b23367a8ca143b2f0e7d11d138e7f465 Mon Sep 17 00:00:00 2001 From: Andy Gayton Date: Thu, 26 Feb 2026 15:36:55 +0000 Subject: [PATCH 5/5] feat: add GitHub source link to examples index --- examples/serve.nu | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/serve.nu b/examples/serve.nu index cd8d76b..df08992 100644 --- a/examples/serve.nu +++ b/examples/serve.nu @@ -39,6 +39,7 @@ li { margin: 0.5rem 0; } ) ( BODY (H1 "http-nu examples") + (P (A {href: "https://github.com/cablehead/http-nu/tree/main/examples"} "source on GitHub")) (UL (example-link "./basic/" "basic" "minimal routes, JSON, streaming") (example-link "./datastar-counter/" "datastar-counter" "reactive counter")