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
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -793,6 +796,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
Expand Down Expand Up @@ -835,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.
Expand Down
38 changes: 38 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 6 additions & 5 deletions examples/basic.nu
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"<html><body>
<h1>http-nu demo</h1>
<ul>
<li><a href='/hello'>Hello World</a></li>
<li><a href='/json'>JSON Example</a></li>
<li><a href='/echo'>POST Echo</a></li>
<li><a href='/time'>Current Time</a></li>
<li><a href='/info'>Request Info</a></li>
<li><a href='./hello'>Hello World</a></li>
<li><a href='./json'>JSON Example</a></li>
<li><a href='./echo'>POST Echo</a></li>
<li><a href='./time'>Current Time</a></li>
<li><a href='./info'>Request Info</a></li>
</ul>
</body></html>"
}
Expand Down Expand Up @@ -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}
Expand Down
6 changes: 3 additions & 3 deletions examples/datastar-sdk/serve.nu
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
)
)
Expand Down
4 changes: 2 additions & 2 deletions examples/mermaid-editor/serve.nu
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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"}
Expand Down
21 changes: 19 additions & 2 deletions examples/quotes/serve.nu
Original file line number Diff line number Diff line change
Expand Up @@ -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
(
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
)
)
Expand Down
69 changes: 69 additions & 0 deletions examples/serve.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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 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")
(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")
(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
}
2 changes: 1 addition & 1 deletion examples/templates/nav.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<nav><a href="/">Home</a> | Page (from disk)</nav>
<nav><a href="./">Home</a> | Page (from disk)</nav>
11 changes: 9 additions & 2 deletions examples/templates/serve.nu
Original file line number Diff line number Diff line change
@@ -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" }
Expand All @@ -6,8 +13,8 @@
{} | .mj --inline '<h1>Templates</h1>
<p>This page is rendered with <code>.mj --inline</code>.</p>
<ul>
<li><a href="/file">File mode</a> - extends and include from disk</li>
<li><a href="/topic">Topic mode</a> - extends and include from store</li>
<li><a href="./file">File mode</a> - extends and include from disk</li>
<li><a href="./topic">Topic mode</a> - extends and include from store</li>
</ul>'
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/templates/topics/nav.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<nav><a href="/">Home</a> | Page (from store)</nav>
<nav><a href="./">Home</a> | Page (from store)</nav>
37 changes: 31 additions & 6 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub topic: Option<String>,
pub expose: Option<String>,
pub tls: Option<String>,
pub services: bool,
}

#[derive(Clone)]
pub struct Engine {
pub state: EngineState,
Expand Down Expand Up @@ -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<String>| 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(())
}

Expand Down
Loading