vyrn

std/ui

28 exports, from std/ui.vyrn.

File-based routing as a generator: pages(dir) scans a directory of page modules at generation time and returns a router module.

import { pages } from "std/ui"
import { route } from pages("./pages")
fn handle(req: Request) -> Response { return route(req) }
pages/index.vyrn        -> GET /
pages/items/index.vyrn  -> GET /items
pages/items/[id].vyrn   -> GET /items/:id

A page module exports what the Page contract below allows. A page with [bracket] segments exports type Params, whose field names must equal the segments; a segment that does not parse as its type is a 404 before user code runs. The router module exports route(req: Request) -> Response, type RoutePath (the regex of every route), and a typed URL helper per route. A Params mismatch, an unsupported param type or a route collision fails the load with a report at the page module.

Dependencies

The import graph shows the whole library.

Imported by
type PageError = { status: Int64, message: String }

A page-load failure. The router renders the nearest error.vyx, or a built-in error body, at status.

type PageData<T> = enum { Loading, Ready(T) }

A lazy page's view data. Loading exists only on a client soft nav; the server always renders Ready. Defined once here, because each constructor may register only once.

type Head = {
    title: Option<String>,
    stylesheets: Array<String>,
    modules: Array<String>,
    scripts: Array<String>,
    meta: Array<Meta>,
}

What a page contributes to the document head, built from noHead() and the with* combinators:

export fn head() -> Head {
    return withModule(noHead(), "/app.js")
}
type Query<T> = { run: fn() -> T }

A page's data, produced by a deferred call. The page blocks until the data lands; return a Lazy to render first and fill in later.

export fn data() -> Query<Array<Paste>> {
    return query(|| listPastes().pastes)
}
type Lazy<T> = { run: fn() -> T }

A page's data when the page renders lazily: on a client soft nav the shell paints first and the data region fills in. A distinct type, not a flag, because laziness decides the view's type (PageData<T>), and the generator reads types, not function bodies.

type ParamQuery<P, T> = { run: fn(P) -> T }

A page's data when it depends on the route parameters. P is the page's own Params, which std/ui cannot name.

export fn data() -> ParamQuery<Params, Result<Paste, PageError>> {
    return paramQuery(|p: Params| fetch(p.id))
}
gen fn pagesThemed(dir: String, theme: String) -> String

pages, with every .vyx page's classes checked against theme. theme resolves relative to the importing module.

Tests

11 test blocks in std/ui.vyrn. CI runs all of them.

Test names
  • route segments and dynamic detection
  • String param types are supported and shape their segment regex
  • uiSplitFields is brace-aware for nested-record field types
  • typed-URL helper names and bodies
  • a slug converts to an identifier, and an alphanumeric slug is unchanged
  • an awkward slug still makes a legal helper name
  • two slugs may reach one helper name — that is the collision to report
  • record-field parsing pulls names and types
  • uiStem and route mapping
  • dispatch order answers a static route before a catch-all at the same depth
  • a static segment with reserved bytes is matched decoded, a plain one raw

Examples

6 programs in examples/ import this module. Each runs as WebAssembly and as a native binary.