web

package module
v2.16.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 62 Imported by: 0

README

web

A web framework for Vuka: routes, views and controllers are declared with decorators, services registered with app.Service, every hook's and handler's parameters filled by type, and web.New() gathers everything into an app.

package main

import (
	"github.com/vuka-lang/ui/v2"
	"github.com/vuka-lang/web/v2"
)

type PetStore struct{ db *DB }

func NewPetStore(db *DB) *PetStore { return &PetStore{db} } // a service: app.Service

@web.Get("/pets")
@web.Template("views/pets.templ")         // a templ component renders the result
func ListPets(store *PetStore) Result[PetList] { … }

@web.Get("/pets/{id}")
@web.Template("views/pet.html")           // html/template, the result as data
func ShowPet(id int, store *PetStore) Result[Pet] { return store.Find(id) }

@web.Get("/")
func Home(r *http.Request) ui.Node {    // JSX
	return <Layout title="Home"><h1>Hi</h1></Layout>
}

@web.Post("/api/pets")
func CreatePet(in NewPet, store *PetStore) Result[Pet] { … }   // JSON in, JSON out

@web.Controller("/admin/pets")
type PetAdmin struct{ store *PetStore }

@web.Get("/")
func (a *PetAdmin) Index() Result[[]Pet] { return a.store.All() }

func main() {
	app := web.New()            // every route and controller declared at init
	app.Service(NewPetStore)    // the app's services: a lazy singleton each
	app.Provide(OpenDB)         // more constructors and values, fx-style
	app.Run(":8080")            // graceful shutdown on SIGINT/SIGTERM
}

A complete app is in examples/pets; live components in examples/live.

go get github.com/vuka-lang/web/v2

Declaring

@web.Get(path), Post, Put, Patch, Delete, Handle(method, path) a route; works on functions and controller methods — on an @ui.Island type, a stateless page backed by that component (below)
@web.Template(file [, name]) render the result with a view (below); before or after the route decorator
@web.Use(mw…) HTTP middleware for this route, func(http.Handler) http.Handler
@web.Controller(prefix) a struct becomes a dependency whose route methods are served under prefix
@web.Live(path, opts…) a stateful component becomes a live page (below) — an @ui.Island type embedding ui.Live, a live page backed by that component; web.Style, web.Script, web.CSS, web.JS give it styles and scripts (below)
@web.Cron(spec) the function runs on a schedule (below)

Paths use {name} for a segment and {name...} for the rest of the path, and match exactly (/pets doesn't match /pets/); a request whose path differs from a route's only by a trailing slash is redirected to it (308, method and query kept), unless something else the app serves answers that path (web.ExactPaths() turns this off). Under a controller, "/" is the prefix itself.

Route decorators are Vuka declarers for a function and type decorators for a type (a vuka.Bundle made with vuka.Dual): they run once at init and record the function with its parameter names, or the type. Other decorators on the same function still wrap it — @audited below runs on every call.

The app

web.New(opts…) takes a snapshot of what has been declared, so several apps (in tests, say) each get their own container and singletons.

Option
web.Provide(ctors…) constructors for the container (also app.Provide)
web.Eager(ctors…) constructors built and started even when nothing needs them (also app.Eager)
web.OnStart(fn), web.OnStop(fn) lifecycle hooks (also app.OnStart/app.OnStop; below)
web.Check(fn) a check Build runs with its own (also app.Check)
web.Options(opts…), web.OptionFunc(f) several options as one; an option written as code (below)
web.Use(mw…) app-wide middleware (also app.Use); runs for every request, 404s included
web.WithRouter(r) another router (below)
web.Only(pkgs…) take only the declarations of these packages (import paths; "main")
web.Dev() development where the process runs in it (VUKA_DEV=1, VUKA_ENV=development) and in tests: internal error messages, Tailwind and Vite for go run . (below); nothing in production
web.DotEnv(files…), web.NoDotEnv(), web.Require(keys…) env files and required variables (below)
web.OnError(f) replace the error page / JSON error body
web.LiveLayout(f), LiveKey(k), LiveOrigins(p…), LiveTokenTTL(d), WithRelay(r) live pages (below)
web.LiveJoinTimeout(d), LiveIdleTimeout(d), LiveMaxConns(n), LiveMaxConnsPerIP(n), LiveMaxConnsPerPage(n), LiveCheckEvery(d) live connections' limits (below)
web.NoTailwind(), web.Tailwind(path) no stylesheet; or the project's Tailwind stylesheet at another path (below)
web.Style(paths…), Script(paths…), CSS(src), JS(src) the app's own stylesheets and scripts, unscoped (below)
web.NoToolbar() no development toolbar (below)
web.NoCrons(), web.CronClock(c) don't schedule the crons; run them by another clock (below)
web.CSRF(trusted…) refuse cross-origin form posts and the like (below)
web.CORS(prefix, cfg), web.TokenOnly(prefix) let other origins call an API; make it bearer-token only, cookies ignored and the cross-origin check off (below)
web.WrapRoutes, ParamFrom, LiveCheck, GuardLiveEvents hooks for libraries guarding routes and live events (below)
web.Frontend(fsys, root, opts…) a single-page app built by Vite (or anything else) served from the binary (below)
web.ExactPaths() no trailing-slash redirect (above)
web.Headers(h), web.NoSecurityHeaders() the security headers every response carries (below)
web.TrustedProxies(cidrs…) the proxies whose X-Forwarded-For/Forwarded/X-Forwarded-Proto count (below)
web.MaxRequestBody(n) the largest request body, 32 MiB by default; a route's own with @web.MaxBody(n) (below)

app.Build() resolves everything — every parameter of every route, every result and view, the whole dependency graph — and returns all problems at once, each naming the route, its source position and the parameter:

web: GET /pets (main.ListPets at pages.vuka:24): component PetsPage: main.PetList has no field for parameter title
web: GET /admin (main.Admin at admin.vuka:9): parameter db *sql.DB: no provider for *sql.DB, needed by main.Admin at admin.vuka:9
web: dependency cycle: *main.A -> *main.B -> *main.A
Options from libraries

A library hands the app one option that does all it needs: web.Options groups options, and web.OptionFunc runs code against the app inside New, before Build, where every App method is open to it.

func Cache(cfg Config) web.Option {
	return web.Options(
		web.Provide(cfg, NewCache),
		web.OptionFunc(func(a *web.App) {
			a.Use(cacheHeaders)
			a.Get("/cache/stats", (*Cache).Stats)
			a.OnStop(func(ctx context.Context) error { return flush(ctx) })
		}),
		web.Check(func(*web.App) error { return cfg.Validate() }),
	)
}

app := web.New(Cache(cfg), web.Dev())

A check's error is one of Build's, reported with the routes' and the container's.

app.Handler() (an http.Handler to mount anywhere; it panics when Build fails), app.Start(ctx) and app.Run(addr) call Build first, so nothing is served from a broken app. Register everything before the first of them. app.Run("") listens on :$PORT, else :8080. web.Resolve[T](app) returns a dependency, for tests and main.

.env files

Env files are read into the process environment when the program starts, as the web package is initialised and before main runs, so every option passed to web.New, constructor and Start method sees their values through os.Getenv or web.Env. By VUKA_ENV, the first file listed winning:

VUKA_ENV files
development, staging, … .env.<env>.local, .env.local, .env.<env>, .env
test (also when unset under go test) .env.test, .env
unset .env.local, .env

The real environment wins over every file: a key is set only when the process doesn't have it yet. .env.local files hold one machine's settings and are skipped in tests, so test runs don't depend on the developer's box. Keys starting with VUKA_ (VUKA_ENV, VUKA_DEV_DIR, VUKA_DEV_LIVE_KEY…) come from the real environment only: an env file setting one is ignored (and logged), so a file next to a deployed binary never switches it into development. web.IsDev() says whether the process runs in development (VUKA_ENV=development, VUKA_DEV=1, or vuka run).

Files are read from the working directory or, when it has none of them, from the nearest parent up to the module root (the directory with go.mod), so go test ./pkg/... finds the project's .env. Without a go.mod in the working directory or above it — a deployed binary — only the working directory is read. Missing files are fine. web.DotEnv("a.env", "b.env") replaces the list (first wins), web.NoDotEnv() reads none, and web.LoadEnv(files…) loads them before New; each takes back what the start's reading of the defaults set before reading its own files. Reading the same files again changes nothing, so an app calling web.LoadEnv() first is unaffected. A package reading the environment at its own initialisation without importing web may run before the files are read: read it in a function. With VUKA_ENV=development the app logs which files it read.

Syntax:

# a comment
PORT=8080
export HOST=localhost              # export is allowed; " #" starts a comment
NAME=two words                     # unquoted values are trimmed
RAW='no $expansion, no \n escapes'  # single quotes (and `backticks`) are literal
KEY="-----BEGIN KEY-----
multi-line, with \n \t \" \\ and \$ escapes"
DATABASE_URL=postgres://${HOST}:5432/app
REPLICA_URL="${REPLICA_HOST:-$HOST}"   # :- gives a default for unset or empty
app.name=pets                      # keys: [A-Za-z_][A-Za-z0-9_.]*

$VAR and ${VAR} expand in unquoted and double-quoted values from the real environment, then the keys defined above (in this file or a lower-precedence one); an undefined variable is empty. A malformed line is a Build error with the file and line; nothing of that file is set.

Reading:

port := web.Env("PORT", 8080)                   // int; the default when unset or empty
debug := web.Env[bool]("DEBUG")                 // true/false, 1/0, yes/no, on/off
ttl := web.Env("CACHE_TTL", 5*time.Minute)      // time.Duration
hosts := web.Env[[]string]("ALLOWED_HOSTS")     // comma-separated (any slice of these)
db := web.Env[*url.URL]("DATABASE_URL")         // absolute URLs; also encoding.TextUnmarshaler types
key := web.MustEnv[string]("SECRET_KEY")        // panics when unset or invalid

app := web.New(web.Require("DATABASE_URL", "SECRET_KEY"))

A value that doesn't convert gives the default, and the next app.Build() (or app.Start, for constructors) fails naming the variable. web.Require fails Build listing every missing variable, the files looked for and the keys of .env.example that are unset too:

web: missing environment variables: SECRET_KEY (read .env; looked for .env.local, .env); also unset, listed in .env.example: SMTP_HOST

Errors and logs name variables and files, never values. Keep .env and .env*.local out of git (this repository's .gitignore does) and commit an .env.example with the keys and harmless values instead; in production, set real environment variables.

Parameters

Each parameter binds by its type and name:

parameter from
*http.Request, http.ResponseWriter, context.Context the request (the context is the request's)
a provided type the container
a scalar: string, ints, floats, bool, a encoding.TextUnmarshaler; *T of one (nil when absent), []T (repeated) the path segment of the same name, else the query parameter ?name=; absent → zero value
one struct, or pointer to a struct that isn't provided (with exported fields; else it is a missing dependency) the body: JSON for application/json, else the form (urlencoded, multipart) or query; then path segments into fields of the same name

Struct fields bind by their form tag, else json tag, else name, matched case-insensitively. A malformed value answers 400 with a message (id: "x" is not an integer). Then the struct's validate: tags are checked, nested structs and slices of them included — required, omitempty, min=n/max=n/len=n, oneof=a b, email, the rules of a page backed by a component — and a failure answers 422 with each field's message, keyed by its path as the request named it (json names for a JSON body, else form names):

{"error":"invalid: address.city: required; email: an email address","fields":{"address.city":"required","email":"an email address"},"status":422}

The error is a web.Invalid, so an OnError handler reads the fields with errors.As. A rule that isn't one of these fails Build.

A request body is at most 32 MiB (web.MaxRequestBody(n); negative for none), uploads included — a multipart form spills to temporary files past 10 MiB, and never past the limit. A longer body answers 413, whoever reads it: the binding, or a handler reading r.Body. One route sets its own: @web.MaxBody(512 << 20), or web.Attrs(web.MaxBody(n)) from Go.

A call decorator reaches the request through the handler's context.Context argument: web.Request(c.Context()), web.ResponseWriter(c.Context()).

decorator audited(c) {
	r := web.Request(c.Context())
	c.Next()
	log.Println(r.Method, r.URL.Path, c.Err())
}

@web.Delete("/{id}")
@web.Use(requireToken)
@audited
func (a *PetAdmin) Remove(ctx context.Context, id int) error { … }

Results

A handler returns nothing, error, T, (T, error) or Result[T].

result response
nothing, a nil error 204 — unless the handler wrote the response itself through its http.ResponseWriter
ui.Node (JSX, templ) HTML
T with @web.Template the view, HTML
T without JSON, 200
web.Redirect(url) (as a value or an error) 302 for GET/HEAD, else 303
*web.Error, wrapped or not its status and message
any other error 500; its message only in dev

Errors are an HTML page on HTML routes (a view, a ui.Node or a redirect result) and {"error": …, "status": …} elsewhere. web.NotFound, Unauthorized, Forbidden, Conflict, web.BadRequest(msg), web.Status(code, format, args…); errors.Is(err, web.NotFound) matches by status. web.Redirect is also a ui.Node, so a page handler can return it. Panics in handlers, middleware, custom routers and page rendering are recovered per request. Before the response is committed, they answer 500 through OnError (HTML on page routes, JSON otherwise; middleware/router failures use the request's Accept header). Details and a stack trace stay in the server log; development responses may include the error message. If OnError itself panics before writing, a generic plain-text 500 is used.

A panic after writing or flushing a response aborts that response instead of appending an error body. http.ErrAbortHandler retains its normal abort semantics. Streaming and WebSocket upgrades are not buffered; a panic during live rendering or a live guard closes the affected socket with code 1011 and a generic reason, discarding its session. Other requests keep running. This does not catch panics in application-created goroutines or turn startup configuration errors into successful builds.

A handler that wrote the response and then returns an error can't change it any more: the error is logged.

Forms that fail. A page answering a POST (or PUT, PATCH, DELETE) is the fragment the handler rendered — for htmx, fetch and the like — unless the browser shows it as its document: a form posted without script (Sec-Fetch-Dest: document, or, from a browser that doesn't send it, an Accept: text/html request without HX-Request). That one is a whole page, as GET would serve it — stylesheet, scripts, toolbar — so a handler can re-render the form with its errors and what was typed instead of redirecting. Such a page isn't live (a reconnect would rebuild it with GET): its JS commands run, its stateful components render but don't connect.

@web.Post("/signup")
func SignUp(ctx context.Context, in Signup, users *Users) ui.Node {
	if err := users.Create(ctx, in); err != nil {
		return <SignupPage form={in} problem={err.Error()} />
	}
	return web.Redirect("/welcome")
}

Views

templ. @web.Template("views/pets.templ") uses the file's one exported component; with several, name one: @web.Template("cards.templ", "Card"). The result fills the component's parameters: whole, when it has one parameter the result is assignable to; otherwise the result is a struct (or pointer to one) and each parameter takes the field of its name (exactly, or with the first letter's case changed: title ↔ Title). Layouts are templ's own composition (@Layout(title) { … }).

templ PetsPage(title string, pets []Pet) { @Layout(title) { … } }
type PetList struct { Title string; Pets []Pet }

html/template. Any other file is parsed once at Build and executed with the result; a second argument names a {{define}}d template to execute.

JSX / render functions. Return a ui.Node; no view needed.

Views are embedded at compile time (vuka.File), so the binary carries them, from the handler's package directory or below it. A .templ file beside the handler is part of its package; one in a subfolder is that folder's package, as Go has it — views/pets.templ starts with package views — and Vuka compiles it and imports it for you:

examples/pets/
  views.vuka        @web.Template("views/pets.templ"), @web.Template("views/pet.html")
  model/pet.go      package model: Pet, shared by handlers and views
  views/layout.templ  package views: Layout
  views/pets.templ    package views: PetsPage, composed with @Layout
  views/pet.html

Only the subfolder's exported components can be views, and its types can't come from the handler's package (package main can't be imported): put shared types in a package of their own, like model here. A JSX page uses a subfolder's component through its import, <views.Layout title="Home">.

Live components

A stateful component (a struct embedding ui.Live) keeps its state on the server and answers its elements' events over a WebSocket; the page changes in place.

@web.Live("/counter")
type Counter struct {
	ui.Live
	Start int // ?start=3
	n     int
}

@ui.Mount
func (c *Counter) mount(ctx context.Context) error { c.n = c.Start; return nil }

func (c *Counter) Inc() { c.n++ }

func (c *Counter) Render() ui.Node {
	return <div><output>{c.n}</output><button onClick={c.Inc}>+</button></div>
}

How a page becomes live. Two ways, one mechanism:

  • @web.Live(path) on a stateful component serves it as a GET page. Its exported fields of provided types are injected; the others that hold text bind from the path segment or query parameter of their name (form tag, else the field name, ignoring case). The page is the component inside the layout web.Layout(f, assets…) names, else the app's web.LiveLayout(f, assets…), else a bare document — f is func(page ui.Node) ui.Node. web.Use(mw…) adds middleware. From Go: app.Live("/counter", NewCounter) (or &Counter{Start: 1}, whose fields are the defaults).

  • Any GET route whose page — a ui.Node result, or a templ view — renders a stateful component or an event handler is live by itself:

    @web.Get("/")
    func Home() ui.Node { return <Shell title="Home"><Counter Start={10} /></Shell> }
    

    A Vuka component rendered from a templ view is live too: templ passes the context on. html/template views and JSON are never live.

The first, HTTP render is complete HTML, and before </body> goes <script src="/_vuka/live.js" data-vk-token="…" defer>. The script opens /_vuka/live?t=<token> and sends join; the server runs the same route again — a GET to the URI in the token, with the WebSocket request's headers (cookies, auth), through the app's and the route's middleware, with its dependencies — and the page it renders becomes the connection's live.Session. That second run mounts the instances that keep state (LiveView's two-phase mount). A join the route answers with a redirect sends redirect (the browser follows it); any other non-page answer (a 404, JSON) is an error, and the connection closes.

The route's handler runs once per connection: its result is the page, and the page is rendered again after every event. State belongs in the components.

Side effects in a live page's handler. The route runs twice for one visit — the GET, then the join (and again on each reconnect) — so what it does besides building the page, sending mail or counting a visit, happens twice. web.IsLiveJoin(ctx) is true only on the join's run; skip them there:

@web.Get("/welcome")
func Welcome(ctx context.Context, m *mail.Mailer) (ui.Node, error) {
	if !web.IsLiveJoin(ctx) {
		if err := m.Send(ctx, welcomeMail); err != nil {
			return nil, err
		}
	}
	return <WelcomePage />, nil
}

A navigation over the connection (navigate) is a visit of its own and isn't a join. Web marks its own run, so a client sending X-Vuka-Live can't make it true; events after the join see false too.

Events. onClick={c.Inc} renders data-vk-on-click="c1:0"; the runtime listens on the document for each event type a page names and sends the nearest handler's id, the element's value (a checkbox's "true"/"false"), the key of a keyboard event, and a submitted form's fields. What a handler receives follows its type (Vuka's rules: nothing, the value, ui.Event, url.Values, a struct bound from the form). Two attributes tune it:

attribute
data-vk-debounce="150" send the element's events (not submits) 150ms after the last one
data-vk-key="Enter Escape" send a keyboard event only for these keys

Submits are preventDefaulted; so are clicks on links and on buttons of a form without a submit handler. A form whose submit handler succeeds is reset to what the server rendered; one whose handler returns an error keeps what the user typed. The element of a click or submit carries aria-busy="true" until the reply.

Patches. The reply carries the HTML of each component whose output changed (the page itself is c0). The runtime morphs the element in place: elements are matched by id, else data-vk-id, else position and tag; a focused field keeps its value and caret; another field takes the server's value when it changed. Give list rows an id for stable identity across inserts and removals. data-vk-ignore (with an id) leaves an element as the browser has it. A whole-document c0 patch morphs the <body> and sets the title.

Browser hooks. <html data-vk-state> is connecting, connected, disconnected, idle or closed (style a banner on it). The document receives vk:patched (detail.ids) after each render or patch and vk:error (detail.message) for a handler's error or a failed message; errors are also logged to the console.

Errors. What the browser reads of an error follows HTTP: a *web.Error's message (web.BadRequest("a name, please"), web.Status(409, …)) as it is, and a field that doesn't parse; an event naming a handler the session no longer has is live: no handler "c3:75"; any other error — a handler's, Mount's or Update's, or a panic — is Internal Server Error. What the browser doesn't get of an error (all of it, or the detail of a no-handler error: whose handler it was, by Go type) goes to the log (slog, with the session, the page, the handler and the component). In development (web.Dev(), VUKA_ENV=development, vuka run) the browser — its console and the toolbar — gets every error's full text.

Broadcasts. web.Broadcast(ctx, topic, msg) — from an event handler, an Update, or any request — delivers msg to every component subscribed to topic (c.Subscribe(topic) in Mount), in every connected page; each takes it in the Update overload of its type and its page gets the patch. app.Broadcast(ctx, topic, msg) does the same from outside a request. Delivery is in-process; web.WithRelay(r) sends broadcasts through a Relay (Publish(ctx, topic, msg) / Subscribe(deliver)) to reach every replica.

@web.Live("/chat/{room}")
type Chat struct {
	ui.Live
	Room  string   // the path segment
	Log   *ChatLog // injected
	lines []string
}

@ui.Mount
func (c *Chat) mount(ctx context.Context) error { c.Subscribe("chat:" + c.Room); return nil }

func (c *Chat) Update(m Said)   { c.lines = append(c.lines, m.Who+": "+m.Text) }
func (c *Chat) Update(j Joined) { c.lines = append(c.lines, j.Who+" joined") }

func (c *Chat) Send(ctx context.Context, f ChatForm) error {
	return web.Broadcast(ctx, "chat:"+c.Room, Said{c.name, f.Text})
}

Connections. One session per connection; its events and broadcasts are handled one at a time, in order, by the connection's goroutine. The server pings every 25 seconds; a closed connection unmounts its components. app.Stop (and Run's shutdown) closes every connection as "going away". The runtime reconnects with jittered exponential backoff (up to ~10s) and joins again — the route runs again and the state starts over; what was not saved elsewhere is lost.

Limits. A socket must join within LiveJoinTimeout (10s); a joined page the browser sends nothing for LiveIdleTimeout (30m; broadcasts don't count) closes with code 4002, dropping its session, and the runtime joins again at once if the page is in view, else when it is shown. At most LiveMaxConns (10,000) sockets are open, LiveMaxConnsPerIP (100) from one client address (web.ClientIP, so through TrustedProxies) and LiveMaxConnsPerPage (4) with one page token; a socket beyond a cap is refused (503, 429) and the runtime retries. A page is checked when it mounts — its join and each navigation run its route through the app's middleware and gates — and its events and broadcasts trust what it mounted with. LiveChecks run on each joined page every LiveCheckEvery (30s): one failing closes the page with 4001, and it reloads; app.CloseLive(match) closes the pages whose context match picks at once. Each limit is an option; 0 lifts it.

Security. The token is the page's URI and an expiry (LiveTokenTTL, 12h by default), HMAC-SHA256-signed with the app's key: a connection can only rebuild a page the app rendered, and it does so with the connecting browser's own cookies, through the route's own middleware. Each app draws a random key; replicas behind one hostname need a shared web.LiveKey(key). A bad or expired token closes the connection with code 4001 and the runtime reloads the page (at most once in 10 seconds) for a fresh one. WebSocket upgrades only from the page's own origin, unless web.LiveOrigins(patterns…) allows more ("*.example.com").

Navigation

A paged list, tabs kept in the URL, a list and its detail pages: moving between them shouldn't reload the document, drop the connection or lose the scroll position of the page you come back to. ui.Link is a link a live page follows over its connection:

<ui.Link href="/books/7">Dune</ui.Link>                 // navigate: another page
<ui.Link href={shelfURL(s.page + 1)} patch>Next</ui.Link> // patch: this page, other parameters

It renders <a href="…" data-vk-link> (="patch"), so it stays a plain link without JavaScript, on a page that isn't live, in a new tab (target, a modifier key, the middle button) and to another origin — the runtime leaves those clicks to the browser.

  • Patch moves to another URL of the same page. The route runs again for the new URL (its middleware too). A Live page whose type has an Update overload taking a web.URLParams struct stays mounted with its state: its props are bound from the URL again, and that Update gets the new params first — bound the same way, from the path segment or query parameter of each field's name. Then the usual patch is sent. The server decides, not the link: a page without such an Update mounts again, Mount seeing the new URL, as a navigate. So Update must repeat every check Mount makes of what the URL names (that the visitor may see order {id}): an error it returns fails the patch, nothing stays mounted, and the browser loads the URL, the route answering as for any request.
  • Navigate moves to another page: its route runs as a page load does (middleware, gates, dependencies, layout), its components mount on the same connection, and the old ones unmount. Under protocol v2 the page arrives as a change to the trees the connection holds: a shared layout isn't sent again.
type ShelfParams struct {
	web.URLParams
	Page int // ?page=
}

@ui.Mount
func (s *Shelf) mount(ctx context.Context) error { s.show(s.Page); return nil } // the first URL

func (s *Shelf) Update(p ShelfParams) { s.show(p.Page) } // each patch

From a handler, c.PushPatch(href) and c.PushNavigate(href) move the page once the reply is applied; in a chain, ui.JS.Patch(href) and ui.JS.Navigate(href) do it from the browser. A URL whose route answers anything but a page — a redirect, an error, JSON, another origin — is loaded as usual, and so is any navigation while the page isn't connected, or one that fails. Only paths and http and https URLs are followed: the runtime refuses to load a javascript:, data: or other URL, wherever it comes from (a command, a fallback, the server), and the server sends no redirect to one (an app's open redirect to javascript: is an error).

In the browser: the address changes with pushState, and back and forward patch or navigate to match (patch within one page's entries); a navigation scrolls to the top or to the URL's #hash, back restores where the page was, and focus moves to the new page's [autofocus] element, else its <main>, else its first component. The page's <title> follows, and its <head> elements the document lacks (stylesheets, scripts, meta tags) are added once; scripts in the new page's body don't run. <html data-vk-navigating> is set while a navigation is in flight, and vk:navigate fires on window when one completes (detail: {url, patch}). Events aren't sent while the page is on its way out, and a new page drops the optimistic pushes still waiting for the old one, without their hooks. A reconnect joins the page the connection moved to.

JS commands

Opening a menu, showing a dialog, moving focus or copying a snippet needs no server: a chain of JS commands as an on… value runs in the browser, at once, and a Push step in it calls a handler when the server should know too.

func (c *Chores) Render() ui.Node {
	return <section>
		<button onClick={ui.JS.RemoveClass(ui.Ref("menu"), "hidden")}>Pick one</button>
		<ul ref="menu" class="menu hidden">
			{for _, ch := range c.chores {
				<li><button value={ch} onClick={ui.JS.AddClass(ui.Ref("menu"), "hidden").Push(c.Pick)}>{ch}</button></li>
			}}
		</ul>
		{for _, ch := range c.chores {
			<p>{ch} <button value={ch} onClick={ui.JS.Confirm("Delete " + ch + "?").Push(c.Delete)}>delete</button></p>
		}}
		<p ref="status">{len(c.chores)} left</p>
	</section>
}

func (c *Chores) Delete(e ui.Event) {
	// …
	c.PushJS(ui.JS.On(ui.Ref("status")).AddClass("flash")) // runs once the reply is applied
}

A chain renders as data-vk-js-<event>="[[name, args], …]" (the live protocol's "JS commands"). The runtime runs it when the event reaches the nearest element with commands or a handler for it: classes and attributes, Show/Hide/Toggle (the style attribute's display, with optional transition classes during/from/to), Transition, Focus, FocusFirst, a focus stack (PushFocus/PopFocus, for dialogs), Dispatch (a CustomEvent), Exec (the commands held in another attribute), Copy/CopyText (the element gets data-copied for 1.5s), ScrollTo, SetValue (fires input and change), and the steps that steer the chain: Key("Escape") (stop for other keys), Confirm(msg) (stop on cancel), Debounce/Throttle (the steps after them, per element and event; a debounced step waiting in a form runs before the form's submit), and If/ElseIf/Else/End over a target's checked state, value, attribute, selector match or existence. Targets: the element itself, ui.Ref(name) (the refed element of the same component instance, not of the instances inside it), ui.Closest(sel), ui.Inner(sel), or a selector. A submit's commands preventDefault it; a push also does what a handler would (links, a form's buttons). An element with both commands and a handler for one event runs the commands first, and a declined Confirm keeps the event from the server. The attribute commands change no event handler attribute (on…) or srcdoc, and set no URL attribute (href, src, action, formaction, xlink:href, poster, data) to a javascript:, data: or vbscript: URL: ui.JS.SetAttr and its kin panic on such a name when the chain is built and sanitize such a value as an href in markup is, and the runtime refuses either in commands from elsewhere (an Exec's attribute).

Pushed commands. c.PushJS(chain) in a handler, an Update or Mount rides the reply ("js":[{"id","ops"}] on render and patch, both protocol versions) and runs after it is applied, on the component's root element.

Reconciliation. What a command changed — a class, an attribute, the style display — stays across the server's patches while the server keeps rendering that attribute as it did when the command ran: an open menu stays open while the list in it updates. When the server renders a different value, the server wins and the browser's change is dropped. A running transition's classes survive a patch until it ends.

Optimistic updates. A like should count at once and a deleted row should go at once, without waiting for the server — and come back if the server refuses. The steps before a Push in a chain are its predictions: they show immediately, and the push's reply settles them.

<button
	class={likedClass(p.liked)}
	onClick={ui.JS.AddClass(ui.This, "liked").SetText(ui.Ref("likes"), fmt.Sprint(p.likes+1)).
		Push(p.Like).OnError(ui.JS.Show(ui.Ref("like-error")))}
>
	Like
</button>
<b ref="likes">{p.likes}</b>
<p ref="like-error" hidden>Couldn't save your like.</p>
  • Success (a reply without an error): the predictions stay, and reconciliation goes on as for any command — wherever the server renders something else (the count with everyone else's likes), the server wins. The OnSuccess chain runs.
  • Failure (the handler returned an error, an error message, no reply within the push's timeout — 10s by default — or the connection dropping before the reply): each prediction is undone, latest first, except where the server has rendered a different value since or a later command changed it. The OnError chain runs, and vk:error fires as for any error. A push made while the page isn't connected isn't sent (the runtime doesn't queue events) and fails at once.
  • Push(h, ui.KeepOnError()) never undoes (the hooks still run); ui.Timeout(d) sets how long a push waits. A reply that comes after its push timed out is applied as usual, without running hooks again.

What counts: classes, attributes, the style display (Show/Hide, a hidden attribute), the text SetText sets — reconciled like an attribute: it stays while the server renders the same text — and a field's value or checked state from SetValue. Each push in a chain settles only the steps since the previous push; a push a branch skips ends its segment too. Hooks run on the element the chain ran for, or the component's root once that element is gone; a server-pushed chain's pushes work the same way. While a push waits, its element has data-vk-pending; after a failure it has data-vk-failed until its next push — hooks for CSS:

[data-vk-pending] { opacity: .6; }
[data-vk-failed] { outline: 1px solid crimson; }

Calling JavaScript. A chart to redraw, confetti on a save, a date picker to open: Call("name", args…) calls a page function with ctx — {el, event, root, target(spec)} — and the arguments, which may be reads of the page at that moment (ui.Ref("q").Value(), .Checked(), .Attr("name")). Register functions with vuka.register; the first line lets a script run before the runtime (its registrations wait in vuka.q):

<script>
  window.vuka ||= { q: [], register(name, fn) { this.q.push([name, fn]); } };
  vuka.register("confetti", (ctx, n) => fire(ctx.el, n));
</script>

Otherwise the name is a dotted path on window ("Picker.refresh", called with Picker as this), never evaluated as code. A returned promise holds the rest of the chain until it settles; false stops the chain as a declined Confirm does. An unknown function, a throw or a rejection stops the chain and fires vk:error on the element (detail: {error, fn}); when it happens among a push's predictions, that push fails without being sent — its predictions are undone and its OnError chain runs. (A chain that waits on a promise doesn't hold back a data-vk-on-… handler on the same element; put the Push in the chain.)

Without a connection. Commands need no session: a page whose only data-vk- attributes are commands gets the runtime script without a token, runs its commands, and never opens a WebSocket — a static page can have menus and dialogs. A Push on such a page does nothing (the console warns once). A page is live when it renders a stateful component, a handler, or a command that pushes to one.

Protocol versions

The runtime joins asking for protocol v2 ({"type":"join","v":2}) and follows whatever the server answers:

  • v2, render trees. Each component instance arrives as a tree: its statics (the markup the JSX fixes) around its dynamics (what Go expressions render), loops as item frames keyed by key, nested instances as references. Statics travel once per connection and are referred to by id; long strings are kept by the connection too. After an event or a broadcast the server sends, for each instance whose output changed, only the dynamics that changed: a new text or attribute, loop steps (keep, skip, insert, move a keyed item, change one in place), a token patch of a long string — or the instance's plain HTML when that is smaller. The runtime applies the changes to the trees it holds, renders the changed instances' HTML from them and morphs the page with it exactly as in v1, so focus, caret, typed values, busy states and data-vk-ignore behave the same. A counter's click is ~70 bytes instead of ~250; a todo list's events ~3–4× less than v1 (the examples' browser tests log both). A reply the runtime can't apply to its trees makes it join again.
  • v1, HTML. Each changed instance's whole HTML (updates), morphed in. A Vuka session that doesn't speak v2 answers a v2 join in v1, and the page works as before.

web.LiveProtocol(1) makes the app answer in v1 whatever the page asks (LiveProtocol(2), the default, uses v2 when the Vuka session speaks it). The format is Vuka's protocol v2; <html data-vk-protocol="2"> says which one a page got.

The wire, beyond Vuka's protocol

Vuka's wire protocol as is, plus:

GET /_vuka/live?t=<token> the WebSocket; the token in the URL, so a bad one is refused before any message
GET /_vuka/live.js?v=<hash> the runtime, immutable for its hash (ETag otherwise)
join runs the route; a second join on the same connection starts over; its v picks the protocol (above)
redirect sent for a join the route redirects; the connection then closes normally (1000); never to a URL that isn't http or https (an error instead)
navigate runs the URL's route (X-Vuka-Live: navigate) and makes its page the session's; the patch reply carries url and token, the new page's join token; a route that doesn't answer with a page gets redirect (its Location, else the URL); patch: true asks to keep the page mounted, which the server does only for an Update taking web.URLParams; a failed Mount, Update or render answers error, nothing staying mounted
close 4001 invalid or expired token, or a LiveCheck failed: reload
close 4002 the page sat idle (LiveIdleTimeout): reconnect once the page is in view
close 1008 no join within LiveJoinTimeout
close 4004 the route isn't a live page: the error message precedes it; no retry
close 1001 the server is stopping: reconnect
request header X-Vuka-Live: join on the route's run for a join (navigate for a navigation's)

A broadcast arriving before join is dropped (the join renders fresh).

Client state

ui.State keeps a value in the browser — a menu's open flag, a tab, a filter — and the page follows it with no round trip. The runtime (live.js) evaluates the bindings and runs the writes, live page or not:

var theme = ui.State("light") // the page's: one value

func Menu() ui.Node {
	open := ui.State(false) // each Menu's own
	return <div>
		<button aria-expanded={open} onClick={open.Toggle()}>Menu</button>
		<ui.Show when={open}>
			<ul class="transition data-closed:opacity-0">…</ul>
		</ui.Show>
	</div>
}

func Tabs() ui.Node {
	tab := ui.State("env")
	f := ui.State(Filters{Sort: "name"})
	return <section>
		<button aria-selected={tab == "env"} onClick={tab.Set("env")}>Env</button>
		<button onClick={tab.Set("logs").Set(f.Kind, "")}>Logs</button>
		<div hidden={tab != "logs" || f.Kind == ""}>…</div>
		<Count kind={f.Kind} /> {/* the same state */}
	</section>
}

A live page's renders leave what the browser set alone; a component sets a state from the server with c.PushJS(tab.Set("logs")). Guide: client state; example: examples/live/pages/state.

Dependency injection

A service is a struct with a constructor whose parameters are its own dependencies, registered in main:

type Todos struct {
	db   *DB
	mu   sync.Mutex
	rows []Todo
}

func NewTodos(db *DB) *Todos { return &Todos{db: db} }

app := web.New()
app.Service(NewTodos)
app.Provide(OpenDB) // constructors and ready values of any type

The container holds constructors — func(deps…) T or func(deps…) (T, error) — and ready values (anything that isn't a function). Every value is a lazy singleton per app; dependencies match by exact type (provide a constructor returning the interface to inject an interface). Missing providers, cycles and duplicates are Build errors. A @web.Controller struct is constructed by the constructor Vuka writes, which takes its pointer and interface fields (or by its own New).

Parameters, by type

Hooks and handlers declare what they need by parameter type, and each parameter is filled from the one source its type names — the same rule for a route's handler, a controller's method, a live page's hooks (@ui.Mount, @ui.OnChange, @ui.Loads) and event handlers, a layout's or guard's Load, and a middleware's constructor:

Parameter Filled from
context.Context the request's, or the live session's
a struct, or pointer to one the form or JSON body (a route), the event's form fields (a live handler); validate: tags checked, 422
url.Values the form's fields, as sent
a string, number, bool or TextUnmarshaler a route: the path segment of its name, else the query parameter; a live handler: the event's value
*http.Request, http.ResponseWriter the request (a live page's: the one it joined with)
*auth.Identity (a ParamFrom type) the signed-in visitor
a type app.Service or web.Provide registered the container

A parameter nothing fills is a Build error with the file, the line and its type — E0407 for a pointer or interface nothing provides, E0405 for a type a request or an event can't carry:

error[E0407]: GET / (live page pages.Index): Index.Add's 2nd parameter, *sql.DB, is not provided
 --> index.vuka.html:41
  = help: register it in main with app.Service(NewX), or web.Provide(NewX)

A live page's exported methods are checked as event handlers: a method taking two values from the page's own Go (p.Toggle(id, done) in a closure) is no event, and only a missing service is reported for it. A scalar's name comes from the compiler, which records it (vuka.RegisterParams); a handler registered from Go binds its scalars to the path's segments in order, the last scalar the last segment, and takes the query in a struct.

Lifecycle

app.Start (and Run) builds what the routes need, then runs the start sequence; app.Stop runs the stop sequence, its reverse. The sequence holds, in order:

  • ready values given to Provide, and hooks added with OnStart/OnStop, in the order they were added — a ready value was given explicitly, so it starts even when nothing needs it;
  • values built from constructors, in dependency order — only those something needs (a route, a live page, another value), and those provided with web.Eager.

A value implementing Start(context.Context) error is started and one implementing Stop(context.Context) error is stopped. The first failing start ends Start with its error; every stop runs, and their errors are joined.

app := web.New(
	web.Provide(cfg),                 // cfg.Start, if any, runs first
	web.OnStart(migrate),
	web.Eager(NewMailer),             // started though no route takes it
)

Crons

A function runs on a schedule with @web.Cron. It takes a context.Context and dependencies, like a handler, and returns nothing or an error:

@web.Cron("@every 10m")
func forgetUnconfirmed(ctx context.Context, owners *Owners) error { … }

@web.Cron("CRON_TZ=Africa/Dar_es_Salaam 0 7 * * 1-5")
func (r *Reports) Morning(ctx context.Context) error { … }   // a method of a service

app.Cron("digest", "0 8 * * *", sendDigest)                  // from Go
spec
*/15 * * * * five fields: minute, hour, day of the month, month, day of the week; *, a-b, a,b, /step, JAN, MON; Sunday is 0 or 7
@hourly, @daily (@midnight), @weekly, @monthly, @yearly the usual shorthands
@every 30s at each multiple of the duration — :00 and :30 here, on every replica and after a restart
CRON_TZ=Europe/Paris 0 9 * * * the fields read in that zone; else the process's local time

When both day fields are restricted a day matching either runs. Across daylight-saving changes a time the clock skips runs at the jump, and a time it passes twice runs once — unless the hour field is *, when the job keeps its rhythm by the clock. Package web/cron parses specs and computes the next run on its own.

app.Start schedules the crons after building everything they need; Stop stops scheduling, waits for the runs in progress until its context is done, then cancels their context. A run never overlaps the previous one: a tick that finds it still running is skipped and logged. Missed ticks (the machine slept) aren't run late. A panic is recovered and logged with its stack, and errors go to slog with the cron's name. An invalid spec, a parameter that isn't provided or a cron declared twice is a Build error:

web: @web.Cron("61 * * * *") on main.cleanUp at jobs.vuka:12: cron: "61 * * * *": minute: 61 is out of range 0-59

A cron's name is its function's (forgetUnconfirmed; jobs.cleanUp from another package; Reports.Morning). app.RunCron(ctx, name) runs one now and returns its error, for tests and commands; web.NoCrons() serves without scheduling them.

Several replicas. Each process schedules its crons, so every replica runs every tick. Provide a web.CronLock and only the replica that claims a tick runs it:

type CronLock interface {
	Claim(ctx context.Context, name string, tick time.Time) (bool, error)
}

Every replica asks for the same name and tick, so a lock is a unique key — an INSERT of (name, tick) into a table with a unique index (false on a conflict), or Redis's SET cron:name:tick 1 NX EX 3600. Without one the app uses web.NewMemoryCronLock(), which claims each tick once in its process.

Testing. crontest.Run(app, name) runs a cron with its dependencies. crontest.NewClock(t) is a virtual clock (web.CronClock(clock)): nothing fires until the test moves it.

clock := crontest.NewClock(time.Date(2026, 1, 5, 6, 59, 0, 0, time.UTC))
app := web.New(web.CronClock(clock), web.Provide(NewReports))
app.Start(ctx)
clock.WaitTimers(1)          // the scheduler waits for 07:00
clock.Advance(time.Minute)   // the cron runs
crontest.Wait(app)           // and has finished

Mail

web/mail sends email. Provide a *mail.Mailer and inject it:

app := web.New(mail.Provide())   // configured from the environment

@web.Post("/signup")
func SignUp(ctx context.Context, in Signup, m *mail.Mailer) error {
	return m.Send(ctx, mail.Message{
		To:      []string{in.Email},
		Subject: "Welcome to Pets",
		HTML:    <WelcomeEmail name={in.Name} />,   // JSX or templ; the text part is made from it
	})
}
variable
MAIL_DRIVER log (the default: log the message, send nothing) or smtp
MAIL_HOST, MAIL_PORT the SMTP server; the port defaults to 587, 465 with tls, 25 with none
MAIL_USERNAME, MAIL_PASSWORD PLAIN authentication, when set
MAIL_ENCRYPTION starttls (the default; fails when the server can't), tls (from the start), none (local catchers)
MAIL_FROM, MAIL_FROM_NAME the sender when a message names none

mail.Provide(mail.Config{…}) configures it in code instead (also a Transport of your own, a TLS config, timeouts). A Message has From, To, Cc, Bcc, ReplyTo, Subject, Text, HTML (a ui.Node; ui.Safe(s) for a string), Headers and Attachments. Text and HTML go as multipart/alternative, attachments in multipart/mixed, non-ASCII headers and file names encoded. Addresses are parsed, and a line break in any header value is refused before anything is sent (*mail.InvalidError). SMTP is net/smtp with a dial timeout and a deadline; no other dependency.

m.SendLater(ctx, msg) checks and renders the message now and sends it in the background, retrying a temporary failure (a 4xx answer, a failed connection) 3 times, waiting 2s, 4s, 8s; the last failure is logged. The app's Stop waits for the queue within its context. The queue holds at most Config.Queue messages (1000 by default); past it, SendLater returns mail.ErrQueueFull.

Testing. The log driver keeps what it was given: m.Sent() (each with its rendered HTML, Text, Recipients and Raw MIME). mailtest.New() is a quiet one to provide; mailtest.Last(t, m), mailtest.To(t, m, addr) read it, waiting for SendLater first.

A live page's route runs again when its socket joins: mail sent from it goes once with if !web.IsLiveJoin(ctx) { … } around the send (see "Side effects in a live page's handler").

Router

The default router is net/http's ServeMux with Go 1.22 patterns. Any router plugs in through

type Router interface {
	http.Handler
	Handle(method, pattern string, h http.Handler)   // canonical {name} / {name...} patterns
	Param(r *http.Request, name string) string
}

with web.New(web.WithRouter(r)). A chi adapter is a dozen lines (Handle → r.Method(method, pattern, h), Param → chi.URLParam); the tests carry a tiny one.

Tailwind

Write Tailwind classes anywhere — JSX, .templ and .html views, the class names a JS command adds — and they are styled, with no setup:

@web.Get("/")
func Home() ui.Node {
	return <Layout title="Home">
		<button class="rounded bg-emerald-500 px-4 py-2 text-white"
			onClick={ui.JS.AddClass(ui.Ref("note"), "opacity-50")}>Save</button>
		<p ref="note" class="mt-2 text-sm text-gray-500">Unsaved changes</p>
	</Layout>
}

The vuka command builds the stylesheet with the Tailwind CLI (brew install tailwindcss, or npm i -D @tailwindcss/cli) from the module's .vuka, .templ, .html and .go files and those of the dependencies that ship components (github.com/vuka-lang/ui/v2), and every full page the app serves links it: <link rel="stylesheet" href="/_vuka/app.css?v=…"> before </head>, unless the page links it already. Fragments, JSON and live updates don't.

  • vuka run keeps it rebuilt (tailwindcss --watch, into .vuka/tailwind/) and the app serves it from there; open pages swap in each new build without reloading, live state kept.
  • vuka build builds it once, minified, and embeds it in the binary: served as /_vuka/app.css?v=<hash>, cached for good; revalidated by ETag without the hash. vuka gen writes the file that embeds it (vuka_tailwind_gen.go in the main package) for plain go build.

Your own theme, plugins or components go in app.css or styles/app.css at the module root — any file with @import "tailwindcss"; it replaces the built-in one, which is just that line. Elsewhere, name it: web.New(web.Tailwind("assets/site.css")) (a string literal: the vuka command reads it from the source).

Without the vuka command, web.Dev() runs the watcher for go run . (below).

web.NoTailwind() turns it all off, as does VUKA_TAILWIND=off for the vuka command. Without the Tailwind CLI, vuka run says how to install it and serves pages unstyled; vuka build fails.

web.RegisterCSS(name, css) serves another stylesheet at /_vuka/<name>; the file vuka build generates registers app.css with it.

go run

The vuka command is a convenience; a plain go run . gets live CSS and the frontend's dev server from one option:

app := web.New(web.Dev(), web.Frontend(dist, "frontend/dist"))
app.Run("")
VUKA_DEV=1 go run .        # or VUKA_ENV=development

In development Start (Run) runs tailwindcss --watch on the app's stylesheet (as vuka run does, into .vuka/tailwind/) and each frontend's Vite dev server (node_modules/.bin/vite in its project, the Vite plugin copied in), and the app serves through them: pages swap in each new stylesheet, and load their modules from Vite. Their output is logged with tailwind: and vite: prefixes. Stop ends them — SIGTERM to each one's process group, SIGKILL after 2s — and a dev server whose app is gone, however it ended, exits by itself.

What is missing isn't installed for you: the app serves the last build and logs one line saying how to get live CSS and the dev server. Nothing runs twice — not under vuka run, which runs both itself, nor beside a Vite dev server or a VUKA_TAILWIND_CSS watcher already running. In a test binary web.Dev() turns on development (error messages) and starts nothing; in a production binary it does nothing at all, so main keeps it.

Styles and scripts

A component's own stylesheet and script — a keyframe animation, a canvas drawn on mount — are declared beside it, and the vuka command compiles them:

@web.Live("/counter",
	web.Layout(counterPage, web.Style("layout.css")),
	web.Style("counter.css", "parts/*.css"),
	web.Script("counter.ts"),
)
type Counter struct { … }
  • web.Style(paths…), web.Script(paths…), web.CSS(`…`) and web.JS(`…`) are options of @web.Live (the component's), of web.Layout and web.LiveLayout (the layout's own markup) and of web.New (the app's, unscoped). web.Global among the arguments makes one unscoped. Paths are string literals relative to the .vuka file; globs work.
  • Scoped CSS. Every element a scoped component's markup writes carries data-vk-s="<scope>", fixed markup the transpiler adds (a templ layout's too), and its selectors are rewritten to match only those: :global(…) and :root stay unscoped, :deep(…) reaches into child components, :scope is the root, keyframes are the component's own. They compile into the app's Tailwind app.css, so @apply and variants work.
  • Scripts (TypeScript or JavaScript, bundled with esbuild inside the vuka command) are one module per component under /_vuka/c/; every full page of an app with scripts loads a small loader (/_vuka/c/loader.js), which imports a component's module the first time it renders and runs its hooks: mounted(el, ctx) on load, join or arrival, updated after a patch that keeps it, destroyed when it leaves (live navigation too). ctx.push("Inc", payload) calls a Go method of the component as an event — only those its scripts name in ctx.push("…") literals, which the vuka command lists in the manifest and the app allows (live.AllowPush); any other name is refused, ctx.on(name, fn) receives c.PushEvent(name, payload), ctx.ref(name) finds its ref element. ui.JS.Call("bump") reaches the component's own methods before vuka.register's and window's.
  • vuka run keeps them compiled in .vuka/assets (VUKA_ASSETS): a saved stylesheet restyles open pages, a saved script loads on the next mount. vuka build embeds them with a generated vuka_assets_gen.go, which calls web.RegisterAssets(manifest) and web.RegisterScript(name, js).
  • The development toolbar's Assets panel lists the page's scoped components and their files.

Options given from Go (app.Live(…, web.Style(…))) do nothing: the vuka command reads them from .vuka files.

Development toolbar

Why is this page slow, which statements did it run, what did that click do? In development every full page answers on the spot: a handle on its right edge shows the request's time and statement count, and opens a drawer of panels for that request.

  • Request: method, path, the route's pattern, the handler (its name, file and line), the status, the arguments the handler was called with and where each came from (path, query, body, dependency), and the request's and response's headers (cookies and credentials hidden).
  • SQL: each statement in order, with its arguments, rows and time as a bar. A statement run more than once is amber; run three times or more with different arguments it is named as an N+1, one statement per row of an earlier result. A failed or refused statement is red.
  • Timeline: the app's and the route's middleware, each call decorator (@timed), the handler, the render and the statements, as bars against the request's time.
  • Logs: what slog's default logger wrote for the request — with its context (slog.InfoContext(ctx, …)) or from the request's goroutine. The console gets it as before.
  • Live: on a live page, each message its connection carried — the join, each event (Counter.Inc (click)), navigation and broadcast — with its time, statements, reply size and what it ran. A message that failed is red, and so are failures only the browser saw (a push timing out, a dropped connection).
  • Crons: the app's crons, their next and last runs, and a Run now button. Each run is a record of its own, listed under Cron runs: its statements, timeline, logs and error, like a request's.
  • Mail: with web/mail, the messages the request (or cron run) sent or queued — headers, attachments, state — and the HTML in a sandboxed preview.

The list at the top holds the page and each fetch and XHR call it made since it loaded: every response of a development app carries its record's id in X-Vuka-Toolbar. Under vuka run -watch the handle follows the next build: building… 1.2s while it compiles (the last good build still answering), build failed with the compiler's output in the drawer.

It is on under vuka run, web.Dev() or VUKA_ENV=development, and off in a test binary (so tests see the pages the app serves), with web.NoToolbar(), and in any other app: no recording, no routes, no header, no hooks. The toolbar draws itself in a shadow DOM, so the page's styles and its own don't meet, light or dark as the system prefers. The page's HTML is the same but for its script tag (in <head>, beside the stylesheet's link); live updates never carry it. The app keeps its latest 100 requests in memory and serves them under /_vuka/toolbar/.

Statements. orm's ormweb feeds the SQL panel by itself. Another database layer reports its statements with web.NoteStatement(ctx, web.Statement{SQL, Args, DB, Start, Duration, Rows, Err}); app.Toolbar() says whether to bother.

Panels of your own. web.Note(ctx, panel, v) records a value for a panel from request code — a no-op outside development, so it can stay — and web.AddPanel(name, render) draws it:

func (c *Cache) Get(ctx context.Context, key string) ([]byte, bool) {
	v, ok := c.m[key]
	web.Note(ctx, "Cache", Lookup{key, ok})
	return v, ok
}

web.AddPanel("Cache", func(notes []any) ui.Node {
	return <ul>{for _, n := range notes { <li>{n.(Lookup).Key}: {n.(Lookup).Hit}</li> }}</ul>
})

A panel noted without AddPanel lists its values. A panel's markup may use the toolbar's classes: kv and grid tables, dim, errtext, status, and preview for an <iframe> (the Mail panel's).

Authentication

web/auth signs users in and guards the app. The app writes one interface — where users and their passwords live — and every route, live page and live event then needs a sign-in unless marked public:

type Users struct{ db *DB } // registered in main: app.Service(NewUsers)

func (u *Users) FindLogin(ctx context.Context, login string) (*auth.Identity, string, error) { … } // + the encoded password
func (u *Users) Load(ctx context.Context, id string) (*auth.Identity, error) { … }

@web.Get("/login")
@auth.Public
func Login(r *http.Request) ui.Node { return <auth.LoginPage request={r} /> }

@web.Get("/orders")
@auth.Requires("orders.read")
func Orders(me *auth.Identity, store *Store) Result[[]Order] { return store.For(me.ID) }

@auth.Requires("orders.delete")
func (b *Board) Clear(ctx context.Context) error { … }  // a live event: refused to anyone else, however reached

func main() {
	web.New(auth.Module(auth.Config{Endpoints: "/auth"})).Run("")
}

Cookie sessions for pages and live pages (rotated on sign-in, HttpOnly, SameSite=Lax, Secure under HTTPS), opaque bearer tokens for APIs (POST /auth/token), JWTs issued elsewhere, Argon2id hashes (bcrypt ones upgraded at login; another system's encodings read by Config.Hashers), one error for an unknown login and a wrong password, throttled failures, "remember me" (RememberMe), auth.RevokeUser closing the user's live pages, and authtest for tests. A live event's gate is checked before its handler runs, and again by each gated method it reaches — given the page's context, on any goroutine. A gated call no identity accounts for is refused (LaxGates lets one outside any request run); main and jobs mark their context with auth.System(ctx), and crons are marked by themselves. A failed login takes at least FailedLoginTime (300ms), whatever failed, and throttling counts the account a login finds, not only its spelling. A live page's sign-in is checked when the page mounts (its join, each navigation), its events trusting the identity it mounted with; a sign-in ended through auth (SignOut, Revoke, RevokeUser, a new sign-in, a session found expired) closes its pages at once, and one ended behind auth's back (a store row deleted, another process) is caught within web.LiveCheckEvery (30s), when its identity is also loaded again if LiveRecheck (60s) has passed. Its sessions and tokens are kept in memory unless the app gives it stores. An app's own login handler re-renders its page on a failure (see Forms that fail) with auth.LoginMessage(err); auth.LoginPage keeps the posted login and next. See the auth guide and examples/auth.

CSRF. web.CSRF() (which auth.Module turns on) answers 403 to a POST, PUT, PATCH or DELETE a browser sends from another site's page, read from Sec-Fetch-Site and Origin by net/http's CrossOriginProtection: no token in forms. Requests with Authorization: Bearer pass, as browsers never add one. Live connections check their origin themselves.

CORS and token APIs. web.CORS("/api/", web.CORSConfig{Origins: []string{"https://shop.example"}}) answers preflights under the prefix (204, before routing, middleware and the cross-origin check) and sets Access-Control-Allow-Origin (exact origins, or web.AnyOrigin) and Vary: Origin on its answers; Methods (GET, HEAD, POST), Headers (Content-Type, plus Authorization under TokenOnly; web.AnyHeader echoes what a preflight asks, as a literal * doesn't cover Authorization), ExposeHeaders, MaxAge (5m) and Credentials (the origins may send cookies, and pass the cross-origin check under the prefix). Credentials with AnyOrigin, an origin with a path, a wildcard or null fail Build. web.TokenOnly("/api/") takes the Cookie header off every request under the prefix before any middleware and drops Set-Cookie from its answers, and with that turns the cross-origin check off there: a bearer token (or none, for a sign-in) is all that signs a request in. The check is never off for a request that keeps its cookies.

Hooks for libraries. What auth builds on, open to any library:

web.WrapRoutes(name, f) f(web.RouteInfo) returns middleware for each route (its method, path, attributes, whether it is a page or a Live page, and a Live page's type); an attribute implementing RouteWrapper() string fails Build when no WrapRoutes of that name is installed
web.Attrs(attrs…) typed attributes for a route or App.Live page added in code
web.ParamFrom(f) parameters of f's result type bind with f(r)
web.Implementation[I](app) the provided value implementing interface I
web.LiveCheck(f) f(ctx) on each joined live page every LiveCheckEvery (30s); an error closes it (4001) and the page reloads
app.CloseLive(match) closes (4001, reload) the joined live pages whose context match(ctx) picks, at once: a library ending a session closes its pages
web.GuardLiveEvents(f) f(ctx, web.LiveHandler) before each live event's handler runs, with the page's context: the handler's name, its component's type, its receiver type and method, its declaration's attributes (Vuka's registry: vuka.FuncOf), whether it is a function literal; an error refuses the event as a handler's error would
web.CurrentLiveEvent() the live event the goroutine handles: its context, its handler's name, Handling() (its handler, or what the handler calls, runs; not the render after it), and Refuse(err) — for a decorator of a handler without a context.Context
web.LiveOrigin() on a goroutine a live connection's goroutine started (one a handler starts), that page's context; nil elsewhere
web.IsLiveJoin(ctx) whether the route runs again for a live page's join
web.TrackRequests(), web.CurrentRequest() whether the calling goroutine serves a request (a live connection's included), and the context of the route it runs
web.CronContext(f) every cron runs with its context passed through f
web.ClientIP(r), web.IsHTTPS(r) the client's address and scheme, through web.TrustedProxies only
web.NoteRequest(ctx, note) a row in the toolbar's Request panel
app.WriteError(w, r, err, html) answer as a handler's error is answered

Frontends

A single-page app — React, Vue, Svelte, anything that builds to static files — is served from the binary beside the app's routes:

//go:embed all:frontend/dist
var dist embed.FS

app := web.New(web.Frontend(dist, "frontend/dist"))
a path naming a file (/assets/index-B3x9kQa1.js, /robots.txt) that file, or 404
any other GET path (/, /pets/7) index.html, so client-side routes survive a reload
the app's routes, live pages, /_vuka/ matched first
.vite/ never served

Files Vite's manifest (.vite/manifest.json) lists as build output, with a content hash in their names, are cached for good (Cache-Control: immutable); everything else, index.html included, is revalidated by its ETag and answered with a 304 when unchanged. Outside development a root without index.html fails Build ("build it first").

web.FrontendAt("/admin") serves it under a prefix (build it with Vite's base: "/admin/"); web.FrontendEnv("APP_NAME", "API_URL") passes those variables' values (the environment's, .env's) to the page: index.html sets them before any module runs, and import.meta.env.APP_NAME reads them. Only the keys named reach the browser.

Development. In development (web.Dev(), VUKA_ENV=development, vuka run) the files are read from disk, and a running Vite dev server takes over: the Vite plugin in vite/vuka.js (vuka() in vite.config) writes <dist>/.vite/vuka-hot.json with the dev server's origin and pid, and the app's pages load their modules from it — the browser stays on the app's address and gets hot module replacement, with no proxy to configure. Other files redirect to the dev server, which has the current public/ ones. A hot file whose process is gone is ignored. With neither a build nor a dev server, pages say how to build. vuka run starts the frontend's Vite (and installs its dependencies first), vuka build runs vite build before embedding; both copy the plugin into frontend/vuka/vite.js. Without the vuka command, web.Dev() starts the dev server for go run . (above). A frontend without a package.json is served as it is. The development toolbar is added to the page; the app's Tailwind stylesheet isn't. An example is in examples/spa.

Islands

A Vue, React or Svelte component placed in a server-rendered page hydrates on its own; the rest of the page ships no JavaScript. Import the component into a .vuka file and its props, read from its TypeScript, are type-checked like any component's; a function prop takes a live view method:

import Week "./web/islands/Week.vue"

<Week days={p.days} onBook={p.Book} client:visible />

A page can be a component: @web.Live on an @ui.Island type embedding ui.Live serves the component as a live page, @web.Get on one without it as a stateless page. Through useLive (from vuka:live, typed from the Go struct) the component reads the exported fields (live.rows.value), sets the ones tagged bind: (v-model="live.q.value", debounced; the server runs Changed after) and calls the exported methods (live.cancel(id)). A live page's sets and calls are live events; a stateless page's are each a signed POST that carries the page's props back — signed for the page and the visitor's sign-in, so no one else can replay them. validate: tags on a bound field, and on a method's struct arguments at any depth, are checked before anything is applied or run: a failure answers 422 with each field's message (fields: {"q": "at most 50 characters"}, level.code), shown as live.q.error or the method's error.fields. ui.Layout in the decorator is the page's layout (over web.Layout and web.LiveLayout): a func(ui.Node) ui.Node, or a func(ctx, ui.Node) ui.Node reading the request; several nest.

@web.Live("/orders")
@ui.Island("./web/components/Orders.vue", ui.Layout(layouts.Page("Orders")))
type Orders struct {
	ui.Live
	Q    string  `json:"q" url:"q" bind:"q"`
	Rows []Order `json:"rows"`
	db   *Store
}

func (o *Orders) Changed(ctx context.Context) error           { … }
func (o *Orders) Cancel(ctx context.Context, id int64) error { … }

The guide: Islands. An example is in examples/islands.

Static files

app.Static("/assets", fsys) serves an fs.FS (an embed.FS sub-tree, os.DirFS(…)). A directory is its index.html, else 404 — never a listing — and dotfiles and dot directories (.env, .git/) are never served, but .well-known/. web.Frontend serves the same way.

Link to a file with web.Asset, giving the path it is served at:

<img src={web.Asset("/static/images/logo.png")} alt="Logo" />

Under vuka run that is the path as written. In a binary vuka build made it is the file's content-hashed name (/static/images/logo.3f9a1c2b.png), served with Cache-Control: public, max-age=31536000, immutable; the plain name keeps working. vuka build and vuka vet check each literal path against the mounts of embedded directories. url(…)s in the stylesheets a mount serves, and in the app's Tailwind CSS, name the hashed files too. web.AssetURL(ctx, path) is the absolute URL, for emails: the request's scheme and host, or web.BaseURL("https://example.com")'s where there is no request.

Security headers

Every response carries, unless its handler sets them itself:

header default
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
X-Frame-Options DENY
Content-Security-Policy frame-ancestors 'none' ('self' with SAMEORIGIN)
Strict-Transport-Security max-age=31536000, on HTTPS requests only

web.Headers(web.SecurityHeaders{FrameOptions: "SAMEORIGIN", HSTS: -1}) changes them ("-" leaves one out; a negative HSTS too), and web.NoSecurityHeaders() turns them off.

Behind a proxy, web.TrustedProxies("10.0.0.0/8") names it: from those addresses only, web.ClientIP(r) reads the client's address from X-Forwarded-For (else Forwarded) — the nearest one that isn't a trusted proxy — and X-Forwarded-Proto: https counts as HTTPS. Without it, web.ClientIP(r) is the connection's peer and those headers are ignored. Login throttling and the per-address live connection cap count by it.

Plain Go

Everything works from Go without decorators, with the calls they are: @web.Get("/pets/{id}") on ShowPet is web.Routes.Get("/pets/{id}", ShowPet) at init, after vuka.RegisterParams(ShowPet, "id", "store") recording the names reflection can't see, and an App's methods of the same names register on that app alone. A handler registered from Go binds its scalars to the path's segments in order, the last scalar the last segment, and takes the query in a struct; structs, request values and dependencies bind by type as usual.

app := web.New()
app.Service(NewStore)
app.Get("/pets/{id}", ShowPet, auth.Required()) // func ShowPet(id int, s *Store)
app.Post("/pets", func(in NewPet, s *Store) (Pet, error) { … })
app.Get("/page", page, web.Template(vuka.File("views/page.html")), web.Use(mw))
app.Live("/board", NewBoard)
app.Live("/applicants", NewApplicants, ui.Island(ui.Layout(layouts.Page("Applicants"))))
app.Controller("/admin/pets", NewPetAdmin)
app.Get("/{id}", (*PetAdmin).Show)

A page's constructor builds each page's component, its parameters of provided types injected and the others zero. Options are RouteOptions: web's own, an auth gate, ui.Island, or any value carrying typed attributes. From Go, vuka.File("…") reads from disk relative to the working directory.

Generated code only makes things faster. Live components written as nodes (ui.El, no JSX) patch as whole components: the same page in the browser, larger replies (ui). An island's component works untyped without vuka-live.d.ts: useLive() comes from the island build; only defineProps<T>() of its types needs the file.

Testing

go test ./...                     # the package
vuka test ./...                   # also examples/ and internal/vukatest, written in Vuka

For an app's crons and mail, see crontest and mailtest above.

The browser tests (examples/live, TestBrowserReconnect, TestBrowserJS…) drive a headless Chrome over the DevTools protocol (internal/chrome) and skip when none is found; VUKA_CHROME names the binary. The runtime's JS commands and navigation also run in node over a small DOM (testdata/livejs/js.js, TestLiveJSCommands), skipped without node.

Limits

  • One struct parameter per handler; scalars bind from the path or query only (use *http.Request for headers and cookies).
  • Views live in the handler's package directory or below it (go:embed doesn't reach up); ../shared/x.templ is an error.
  • A vuka.File literal is embedded only where a decorator or attribute takes one, so there is no web.Layout(file) option; compose layouts in templ, or {{define}} them in one html file.
  • Dependencies match by exact type; no named values or groups.
  • Status codes for success are fixed (200/204); write through http.ResponseWriter for anything else.
  • Live patches are whole components' HTML, morphed in the browser (Vuka's phase 1); a live connection's state starts over when it reconnects, and events sent while disconnected are dropped. The live endpoints are at /_vuka/… from the root: an app mounted under a path prefix needs them routed there.
  • Event handlers of a live page get the join's context: web.Request is the join's request, and writing to web.ResponseWriter goes nowhere.

Versions

From v0.50.0, vuka, ui, web and the vuka command are released together under one version; require them at the same version (vuka upgrade moves an app between versions). See Versions and compatibility.

License

MIT

Documentation

Overview

Package web is a web framework for Vuka: routes, views and services are declared with decorators, and web.New gathers them into an app.

@web.Get("/pets/{id}")
@web.Template("pet.html")
func ShowPet(id int, store *PetStore) Result[Pet] { … }

func main() { web.New().Run(":8080") }

Everything a decorator declares is a call of the Routes registry, and an App's methods of the same names register on it alone:

app.Get("/pets/{id}", ShowPet)

Index

Constants

View Source
const (

	// ToolbarHeader names the toolbar record of a development app's
	// response; the toolbar lists a page's fetch and XHR calls by it.
	ToolbarHeader = "X-Vuka-Toolbar"
	// DevDirEnv names the session directory vuka run passes the app: the
	// toolbar is on, and with -watch the directory's build.json says how
	// the next build is going.
	DevDirEnv = "VUKA_DEV_DIR"
)
View Source
const AnyHeader = "*"

AnyHeader, in CORSConfig.Headers, allows whatever request headers a preflight asks for, Authorization included. (A browser reads a literal "Access-Control-Allow-Headers: *" as every header but Authorization, and as the header named "*" on a request with credentials: AnyHeader answers with the headers asked for instead.)

View Source
const AnyOrigin = "*"

AnyOrigin, as CORSConfig's only origin, lets every origin read the answers. It can't go with Credentials.

View Source
const (

	// AssetsEnv names the directory vuka run keeps the compiled assets in:
	// manifest.json, the modules (c/), and the island chunks (i/).
	AssetsEnv = "VUKA_ASSETS"
)
View Source
const (

	// CSSEnv names the stylesheet file vuka run keeps up to date.
	CSSEnv = "VUKA_TAILWIND_CSS"
)
View Source
const DefaultPageSize = 20

DefaultPageSize is the page size Paginate and the request binders use when none is given or asked for.

View Source
const FormHeader = "Vk-Form"

FormHeader marks a form the runtime sends over HTTP, from a page that isn't live: "check" for its checks as the visitor leaves a field (the page answers its errors, nothing saved), "submit" for its submit (its errors, 422, or where to go). Either answer is JSON (formAnswer), so the runtime puts the errors in place without touching the fields.

View Source
const Global = ":global"

Global, among an asset's arguments, makes it unscoped: web.Style("print.css", web.Global) styles the whole page, and a global script's hooks run once on the page, el being document.body.

View Source
const MaxPageSize = 200

MaxPageSize caps a size taken from the request, so a client can't ask for an unbounded page.

View Source
const (
	NavigateHeader = "Vk-Navigate"
)

In-place navigation for pages that aren't live: a click on a ui.Link (data-vk-link) on a page the runtime runs without a connection is fetched with the header NavigateHeader, its value the build the page was rendered by (the runtime's script tag says, data-vk-build-id). The route runs as for a page load — guards, every layout's Load, the page — and answers with the page's HTML, which the browser patches into <body> in place; a few response headers tell it what it needs to:

Vk-Page    the page's root layout (data-vk-root), "-" for none: a
           page under another one is another document, a page load
Vk-Live    a live page's join token: the runtime joins it
Vk-Reload  why the browser must load the URL instead: the build
           changed ("build"), the route asks for it ("route", FullLoad),
           the page's body has scripts of its own ("scripts")
Vk-Reload-Detail  in development, why in words, for the browser's
           console and the toolbar (the terminal says it too, once)

A response without Vk-Page (a redirect elsewhere, a file, JSON, a page the app wrote itself) is the browser's to load too. Nothing in the request is trusted for auth or rendering: the header only picks the response's format. A GET without it is answered as ever.

View Source
const Off = -1

Off turns a compression option off: LiveCompression(Off) sends live messages as they are.

View Source
const OptionsHeader = "Vk-Options"

OptionsHeader is PushHeader as an Options step's call sent it before Options became a Push; a call carrying it is answered the same.

View Source
const PartsHeader = "Vk-Parts"

PartsHeader, on a GET of a page, asks for the page's deferred parts named in its value (space-separated ids) instead of the page; a reply carrying it is the parts.

View Source
const PushHeader = "Vk-Push"

PushHeader carries a Push step's call: the method's reference as the page rendered it.

Variables

View Source
var (
	NotFound     = &Error{Status: http.StatusNotFound, Message: "not found"}
	Unauthorized = &Error{Status: http.StatusUnauthorized, Message: "unauthorized"}
	Forbidden    = &Error{Status: http.StatusForbidden, Message: "forbidden"}
	Conflict     = &Error{Status: http.StatusConflict, Message: "conflict"}
)
View Source
var ErrBodyTooLarge = &Error{Status: http.StatusRequestEntityTooLarge, Message: "request body too large"}

ErrBodyTooLarge answers a request whose body is over its limit.

View Source
var ErrCronRunning = errors.New("the previous run is still running")

ErrCronRunning is RunCron's error when the cron's previous run hasn't finished: runs never overlap.

View Source
var Routes = &Registry{r: registry{views: map[string]declView{}, uses: map[string][]Middleware{}}}

Routes is the registry of the program's declared routes, pages and controllers.

Functions

func AcceptsHTML

func AcceptsHTML(r *http.Request) bool

AcceptsHTML reports whether r asks for an HTML page (its Accept header names text/html): a browser's navigation or form post, not a script's fetch.

func AddPanel

func AddPanel(name string, render func(notes []any) ui.Node)

AddPanel adds a panel to the development toolbar: render draws it for a request from the values the request noted for it (Note), in order. A panel added again under the same name replaces it.

func Asset

func Asset(ctx context.Context, p string) string

Asset is the URL path of a file an app.Static mount serves, given as the path it is served at: web.Asset(ctx, "/static/images/logo.png"). In a binary vuka build made it is the file's content-hashed name, cached for good; under vuka run, the path as given. The path the app is mounted under (http.StripPrefix, or BaseURL's path without a request) comes first. A path no mount serves stays as given, and a warning is logged once. In markup the context is implicit: {web.Asset("/static/site.css")}.

func AssetURL added in v2.11.0

func AssetURL(ctx context.Context, p string) string

AssetURL is Asset's path as an absolute URL, for an email or a feed: the scheme and host the request came to, or, without one (a cron, a job), the app's BaseURL. Without either it is Asset's path, and a warning is logged once.

func Broadcast

func Broadcast(ctx context.Context, topic string, msg any) error

Broadcast delivers msg to the Update methods of every live component subscribed to topic, in every connected page of the app serving ctx (a request's, or a live event's), and sends each page what changed. A topic of "" reaches every component.

func ClientIP

func ClientIP(r *http.Request) string

ClientIP is the address r comes from: its connection's peer, or, when that is one of the app's TrustedProxies, the nearest address in X-Forwarded-For (else Forwarded) that isn't. Throttles and connection caps count by it.

func Controller

func Controller(prefix string) func(*vuka.Type)

Controller makes a struct a service whose route methods are served under prefix.

func Cron

func Cron(spec string) func(*vuka.Decl)

Cron runs the function on a schedule (see package cron for the syntax):

@web.Cron("@every 30s")
@web.Cron("0 7 * * *")
@web.Cron("CRON_TZ=Africa/Dar_es_Salaam 0 7 * * 1-5")
func cleanUp(ctx context.Context, store *PetStore) error { … }

Its parameters are a context.Context and provided types; on a method, the receiver is provided too. The cron's name is the function's ("cleanUp"; "jobs.cleanUp" outside package main; "PetStore.Refresh" for a method). An invalid spec fails Build, naming the function and where it is declared.

func CurrentRequest

func CurrentRequest() (ctx context.Context, serving bool)

CurrentRequest reports whether the calling goroutine serves an HTTP request of an app with TrackRequests — a live page's connection included — rather than being one the app's own work runs on (a cron's, a job's, main), and the context of the route it is running: a handler's, with its middleware's values; nil outside a route (in app-wide middleware, on a live connection between its routes' runs).

func Delete

func Delete(path string) vuka.Bundle

func Env

func Env[T any](key string, def ...T) T

Env returns the environment variable key as a T: a string, bool, integer, float, time.Duration, *url.URL (absolute), a type implementing encoding.TextUnmarshaler, or a slice of those (comma-separated). When key is unset or empty it returns def (or the zero value); when the value doesn't convert, it returns def too and the next Build or Start reports the error.

func Flash added in v2.4.0

func Flash(ctx context.Context, level, text string) error

Flash adds a message for the visitor, which the next page reading the variable messages (MessagesContext) shows once: a form's handler flashes "Saved" and redirects, and the page it redirects to shows it. Over HTTP it travels in a signed cookie, so it must be called before the response is written; from a live event it shows in the page's next render. It needs a request's or a live page's context of an app listing MessagesContext.

func FullLoad added in v2.8.0

func FullLoad() vuka.Bundle

FullLoad has a route's page always load as a document: a link to it on a page that isn't live is followed by the browser, not fetched and patched in place. For a page whose body runs its own scripts as it loads, or that must start from a fresh document. On a page type:

vuka.RegisterType[Report](web.FullLoad())

and in a .vuka file @web.FullLoad above the type. A link marked data-vk-reload does the same for one link.

func Get

func Get(path string) vuka.Bundle

func Handle

func Handle(method, path string) vuka.Bundle

Handle declares a route for method and path: @web.Handle("OPTIONS", "/x"). Path segments in braces bind parameters of the same name: "/pets/{id}"; "{rest...}" matches the rest of the path.

On a type — an @ui.Island struct, with @web.Get — it declares a stateless page backed by that component (see "Pages backed by a component" in the islands guide): its exported fields are the component's props, its exported methods what the component calls. A live one (embedding ui.Live) is declared with @web.Live.

func Implementation

func Implementation[T any](a *App) (T, error)

Implementation returns the app's value implementing the interface T: the one provided as T, else the one provided type implementing it. It is an error when none does, or several do. Unlike Resolve it doesn't build the app, so a Check may call it.

func IsDev

func IsDev() bool

IsDev reports whether the process runs in development: VUKA_ENV is "development", VUKA_DEV is 1, or vuka run names its session directory (VUKA_DEV_DIR). Both come from the real environment only — an env file can't set them — so a deployed binary is never switched into development by a file next to it. (An app made with the Dev option is in development whatever IsDev says.)

func IsHTTPS

func IsHTTPS(r *http.Request) bool

IsHTTPS reports whether r came over HTTPS: its own TLS, or a trusted proxy's X-Forwarded-Proto.

func IsLiveJoin

func IsLiveJoin(ctx context.Context) bool

IsLiveJoin reports whether ctx is a live page's route running again for its connection's join: the page was served already (its GET), and the connection rebuilds it — on the first join, and on each reconnect. A handler with side effects — sending mail, counting a visit — skips them then, so they happen once per visit:

if !web.IsLiveJoin(ctx) {
	mailer.Send(ctx, welcome)
}

It is false for the GET itself, for a navigation over the connection (a visit of its own, replacing an HTTP load), and for the page's events and renders after the join, though their context descends from the join's. A client can't make it true: web marks its own run, not a header.

The route runs for every join whether or not the join adopts the GET's session (LiveAdopt) — middleware, gates and the handler itself, which sees IsLiveJoin true either way. Adoption only skips mounting the page's components again, so Mount runs once per view when the join arrives in time; an expired window, a restart or a reconnect mounts them again.

func Live

func Live(path string, opts ...LiveOption) func(*vuka.Type)

Live serves a stateful component as a page at path, a GET route:

@web.Live("/rooms/{room}")
type Chat struct {
	vuka.Live
	Room  string     // a prop bound from the path segment, else ?room=
	Store *ChatStore // a prop of a provided type, injected
	log   []string   // state
}

Its exported fields of provided types are injected; the others that hold text bind from the path segment or query parameter of their name. The page is the component inside the app's layout (LiveLayout), or the one Layout names, or else a bare HTML document. Use adds middleware.

On an @ui.Island struct embedding ui.Live it is a live page backed by that component (see islandpage.go); the island's ui.Layout wins over Layout.

func LiveOrigin

func LiveOrigin() context.Context

LiveOrigin is the context of the live page whose connection started the calling goroutine — the page's route's, carrying its identity — when a goroutine the connection's own goroutine started calls it: a goroutine an event's handler starts (go b.Clear()), or one a decorator runs a call on. It is nil on any other goroutine: one started further down (by such a goroutine), a pool's worker, a request's, a cron's; and once the page's connection has closed. It reads the goroutine's stack (a few microseconds): a check that has a context.Context asks it instead.

func LivePage

func LivePage() context.Context

LivePage is the context of the live page whose connection the calling goroutine serves — the page's route's, carrying its identity — whatever it is doing: handling an event, rendering, delivering a broadcast. It is nil on any other goroutine, and before the connection's first join.

func LoadEnv

func LoadEnv(files ...string) error

LoadEnv reads env files into the process environment, as New does. The default list is already read when the program starts, before main runs, so options built from the environment see it without a call; LoadEnv is for another list, read before New. No files means the default list (see the README); a key is set only when the environment doesn't have it yet, and the first file listed wins over the later ones. Loading the same files again is a no-op, so New reading them afterwards changes nothing.

func LogError added in v2.8.0

func LogError(ctx context.Context, err error)

LogError logs err for the request ctx belongs to, as a handler's server error is logged: one line with the method and path.

func MaxBody

func MaxBody(n int64) vuka.Bundle

MaxBody bounds the request body of one route, in bytes (negative for none), in place of the app's MaxRequestBody:

@web.Post("/videos")
@web.MaxBody(512 << 20)
func Upload(r *http.Request) error { … }

From Go: app.Post("/videos", h, web.Attrs(web.MaxBody(512 << 20))).

func MustEnv

func MustEnv[T any](key string) T

MustEnv is Env for values that must be there: it panics, naming key, when the variable is unset, empty or doesn't convert.

func Note

func Note(ctx context.Context, panel string, v any)

Note records v for the toolbar's panel named panel, drawn by the function AddPanel registered for it, else as a list of the values. It does nothing for a context that no development toolbar watches, so it can stay in production code.

func NoteGraphQLBatch added in v2.8.0

func NoteGraphQLBatch(ctx context.Context, b GraphQLBatch)

NoteGraphQLBatch records a loader's batch for the toolbar's GraphQL panel.

func NoteGraphQLField added in v2.8.0

func NoteGraphQLField(ctx context.Context, f GraphQLField)

NoteGraphQLField records a resolved field for the toolbar's GraphQL panel.

func NoteRequest

func NoteRequest(ctx context.Context, n RequestNote)

NoteRequest adds a row to the development toolbar's Request panel for the request (or live message) of ctx: what a library decided about it — who is signed in, which gate let it through. It does nothing for a context that no toolbar watches. Never note a secret.

func NoteStatement

func NoteStatement(ctx context.Context, q Statement)

NoteStatement records a statement the request of ctx (or its live page's socket message) ran, for the toolbar's SQL panel. It does nothing for a context that no development toolbar watches.

func PageQuery

func PageQuery(r *http.Request) (page, size int)

PageQuery reads page and size straight from a request's query string, for a handler that took the *http.Request rather than a bound struct.

func Paginate

func Paginate[Q Pageable[Q, T], T any](ctx context.Context, q Q, page, size int) (ui.Page[T], error)

Paginate reads page (1-based) and size from q — an ORM query (orm.For[T](), a QuerySet) or anything matching Query — with one COUNT and one windowed fetch, and returns the page:

p, err := web.Paginate(ctx, orm.For[Book]().Where(…), page, size)
for _, b := range p.Items { … }

page and size are clamped (size to at least 1, page to the list); it does not run the window fetch when the count is zero.

func PaginateSource

func PaginateSource[T any](ctx context.Context, src Source[T], page, size int) (ui.Page[T], error)

PaginateSource is Paginate over a Source: a store that counts and slices itself, with no ORM.

func Patch

func Patch(path string) vuka.Bundle

func Post

func Post(path string) vuka.Bundle

func Put

func Put(path string) vuka.Bundle

func RecordInput added in v2.4.0

func RecordInput(ctx context.Context, in Input)

RecordInput records in, when ctx is a context processor's: what makes its result. A library whose values processors read records its own — package auth records the identity and its permission version when auth.Current is called with a processor's context. Elsewhere it does nothing.

func Recorded added in v2.8.0

func Recorded(ctx context.Context) bool

Recorded reports whether the development toolbar records the request (or live message) of ctx: what is worth noting only for it.

func RefreshVars added in v2.4.0

func RefreshVars(ctx context.Context, names ...string)

RefreshVars makes the processors giving the variables names run again on their next read, as when an input of theirs changed: an event that changed what a processor loads (the user's own record) calls it, and the components showing the variables render again if their values changed.

func RegisterAssets

func RegisterAssets(manifest []byte)

RegisterAssets registers the manifest of the app's compiled styles and scripts. vuka build calls it from a file it generates in the main package.

func RegisterCSS

func RegisterCSS(name string, css []byte)

RegisterCSS registers a stylesheet the app serves at /_vuka/<name>; the one named "app.css" is linked from every full page. vuka build calls it from a file it generates in the main package, with the Tailwind CSS built for the binary.

func RegisterEncoded added in v2.6.0

func RegisterEncoded(path, encoding string, data []byte)

RegisterEncoded registers a fixed asset's encoding made at build time: path is where the app serves it (/_vuka/app.css, /_vuka/c/<module>, /_vuka/i/<chunk>), encoding "br" or "gzip", data the encoded bytes. vuka build calls it from the file it generates, so the binary serves its assets compressed with no work at run time.

func RegisterGraphQL added in v2.8.0

func RegisterGraphQL(build func(GraphQLHost) (GraphQLHandler, error))

RegisterGraphQL is how package graphql serves web.GraphQL: build makes an app's endpoint when the app is built. Package graphql calls it at init; nothing else needs to.

func RegisterIsland

func RegisterIsland(name string, js []byte)

RegisterIsland registers an island's bundled chunk (component or adapter), served at /_vuka/i/<name> and cached for good: its name carries its content's hash. vuka build calls it from the file it generates.

func RegisterScript

func RegisterScript(name string, js []byte)

RegisterScript registers a component's module, served at /_vuka/c/<name> and cached for good: its name carries its content's hash. vuka build calls it from the file it generates.

func RegisterStatic added in v2.11.0

func RegisterStatic(manifest map[string]string)

RegisterStatic registers the content-hashed names of the files app.Static serves: each plain URL path (/static/images/logo.png) to its hashed one (/static/images/logo.3f9a1c2b.png). vuka build calls it from a file it generates in the main package.

func Request

func Request(ctx context.Context) *http.Request

Request is the request being served, from a context.Context a handler received: call decorators reach it through c.Context().

Called with a context processor's context, it records the URL as an input of the processor's result (RecordInput).

func Resolve

func Resolve[T any](a *App) (T, error)

Resolve returns the app's T, building it and its dependencies.

func ResponseWriter

func ResponseWriter(ctx context.Context) http.ResponseWriter

ResponseWriter is the response being written, from a handler's context.

func Slice

func Slice[T any](rows []T, page, size int) ui.Page[T]

Slice paginates a slice already in memory, for small lists and tests.

func StatementOrigin added in v2.8.0

func StatementOrigin(ctx context.Context, path, field string) context.Context

StatementOrigin is ctx with the statements noted under it (NoteStatement) marked as path's and field's — a GraphQL field's, "pets.0.owner" of "Pet.owner", or a loader's ("" and "loader OrdersByUser"): the SQL panel names them, and an N+1 the field repeating it. It is ctx itself when no toolbar records the request.

func ValidCSRFToken added in v2.4.0

func ValidCSRFToken(ctx context.Context, token string) bool

ValidCSRFToken reports whether token is the csrfToken CSRFContext gives the visitor of ctx's request.

func Var added in v2.4.0

func Var[T any](ctx context.Context, name string) T

Var is the template variable name, as a context processor gives it: what a template's bare name compiles to, and what Go code calls for one. Its processor runs on the first read of the request (or live event) and every later read shares the result; on a live page the read is recorded against the component rendering, which renders again when the value changes. Var returns the zero T when the read fails — no such variable, a T that isn't the variable's type, a processor's error, a gate refusing (Auth) — and VarErr says why.

func VarErr added in v2.4.0

func VarErr[T any](ctx context.Context, name string) (T, error)

VarErr is Var with its error.

func WithPendingParts added in v2.9.0

func WithPendingParts(ctx context.Context) context.Context

WithPendingParts is ctx with the deferred parts of the page it renders left pending: the page shows their fallbacks and their methods never run. viewtest.Pending sets it, to test a page's first paint.

func WithVars added in v2.4.0

func WithVars(ctx context.Context, s any) context.Context

WithVars is ctx in which the variables of s — a struct, the result type of a context processor — are s's fields: below ctx, every read of them returns s's values, and the processor returning that type doesn't run. A part of a page shows other values this way, and a test gives its own (viewtest.With, viewtest.Context). s may be a pointer to the struct; it panics for anything else.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is a web application: the routes, services and controllers declared with decorators, and whatever was added in code.

func New

func New(opts ...Option) *App

New makes an app of every route, service and controller declared so far (each app takes its own snapshot, with its own singletons). It first reads the env files into the process environment (see LoadEnv), so constructors and Start methods see their values through os.Getenv.

func (*App) Broadcast

func (a *App) Broadcast(ctx context.Context, topic string, msg any) error

Broadcast delivers msg to the components subscribed to topic, as the package's Broadcast does.

func (*App) Build

func (a *App) Build() error

Build resolves the app: every route's parameters, results and view, and the dependency graph. It reports every problem at once, before anything is served; Handler, Start and Run call it.

func (*App) Check

func (a *App) Check(fn func(*App) error) *App

Check adds fn to the checks Build runs once the routes are compiled; its error is reported with Build's own.

func (*App) CloseLive

func (a *App) CloseLive(match func(ctx context.Context) bool) int

CloseLive closes the app's joined live pages whose context — the one their route ran with, as LiveCheck gets it — match reports true for, with code 4001: each page reloads, its route running again for the browser. It returns how many it closes. A page closes before it handles another message or broadcast; one handling a message closes once that is done. match runs on the caller's goroutine, once per joined page: a library ending a session closes the pages signed in with it this way.

func (*App) Controller

func (a *App) Controller(prefix string, ctor any, opts ...RouteOption) *App

Controller makes the struct ctor builds a service whose route methods are served under prefix, as Routes.Controller does for every app.

func (*App) Cron

func (a *App) Cron(name, spec string, fn any) *App

Cron runs fn on a schedule, as @web.Cron does; name names it for RunCron, the logs and the toolbar. fn takes a context.Context and provided types.

func (*App) Delete

func (a *App) Delete(path string, fn any, opts ...RouteOption) *App

func (*App) Eager

func (a *App) Eager(ctors ...any) *App

Eager adds constructors whose values Start builds (and starts, when they implement Starter) even when nothing needs them. Provided values that aren't functions always start; constructors otherwise only when needed.

func (*App) Get

func (a *App) Get(path string, fn any, opts ...RouteOption) *App

func (*App) Handle

func (a *App) Handle(method, path string, fn any, opts ...RouteOption) *App

Handle registers fn for method and path, as Routes.Handle does for every app.

func (*App) Handler

func (a *App) Handler() http.Handler

Handler is the app as an http.Handler, to serve or mount anywhere. It panics when Build fails.

func (*App) Live

func (a *App) Live(path string, page any, opts ...RouteOption) *App

Live serves a stateful component as a page at path, as Routes.Live does for every app: page is its constructor (NewBoard), or a pointer to the component whose fields are the defaults.

func (*App) OnStart

func (a *App) OnStart(fn func(context.Context) error) *App

OnStart adds fn to the app's start sequence, after the values supplied and the hooks added before it and before the values Start constructs.

func (*App) OnStop

func (a *App) OnStop(fn func(context.Context) error) *App

OnStop adds fn to the app's stop sequence, which runs in the reverse order of the start sequence: fn runs after everything started after it was added has stopped.

func (*App) Page

func (a *App) Page(path string, component any, attrs ...any) *App

Page serves a page backed by component's island at path, as @web.Get on its type does: component is a pointer to the @ui.Island struct (only its type counts). attrs are the type's attributes as a decorator would give them (auth gates…).

func (*App) Patch

func (a *App) Patch(path string, fn any, opts ...RouteOption) *App

func (*App) Post

func (a *App) Post(path string, fn any, opts ...RouteOption) *App

func (*App) Provide

func (a *App) Provide(ctors ...any) *App

Provide adds constructors (or ready values) to the app's container.

func (*App) Put

func (a *App) Put(path string, fn any, opts ...RouteOption) *App

func (*App) Run

func (a *App) Run(addr string) error

Run serves the app on addr (":" + $PORT, else ":8080", when empty) until SIGINT or SIGTERM, then shuts down gracefully and stops the services.

func (*App) RunCron

func (a *App) RunCron(ctx context.Context, name string) error

RunCron runs the cron name now, in the caller's goroutine with ctx, and returns its error (a panic's too). It doesn't wait for a run in progress: it returns ErrCronRunning. For tests, and commands running a cron by hand; the schedule's lock isn't asked.

func (*App) Service

func (a *App) Service(ctor any) *App

Service registers a service: ctor, func(deps…) *T or func(deps…) (*T, error) with T a struct, builds the app's one T, when something first needs it, its parameters filled from the container. Hooks, handlers, constructors, layouts' and guards' Load and middleware constructors then take *T as a parameter. It is how an app declares its services.

app := web.New()
app.Service(NewTodos) // func NewTodos(db *DB) *Todos

func (*App) Start

func (a *App) Start(ctx context.Context) error

Start builds every dependency the routes and crons need and every Eager one, then runs the start sequence: each supplied or built value implementing Starter and each OnStart hook, in dependency order (supplied values and hooks as added, before the values built from constructors), and last the crons' scheduler, which Stop stops first.

func (*App) Static

func (a *App) Static(prefix string, fsys fs.FS) *App

Static serves the files of fsys under prefix: a directory as its index.html, never as a listing, and no dotfile or dot directory but .well-known.

func (*App) Stop

func (a *App) Stop(ctx context.Context) error

Stop runs the stop sequence: each value implementing Stopper and each OnStop hook, in the reverse order of the start sequence.

func (*App) Toolbar

func (a *App) Toolbar() bool

Toolbar reports whether the app shows the development toolbar: for a library feeding it (NoteStatement) only then.

func (*App) Use

func (a *App) Use(mw ...Middleware) *App

Use adds app-wide middleware; the first added runs first.

func (*App) WaitCrons

func (a *App) WaitCrons(ctx context.Context) error

WaitCrons waits until no run the schedule (or the toolbar) started is in progress, or ctx is done: for tests moving a virtual clock, before they move it again — a tick finding the previous run still going is skipped.

func (*App) WriteError

func (a *App) WriteError(w http.ResponseWriter, r *http.Request, err error, html bool)

WriteError answers r with err as a handler's error is answered: its status and message (a *web.Error), an HTML page when html is set, JSON otherwise, or what OnError writes.

type BodyLimit

type BodyLimit int64

BodyLimit is the attribute MaxBody puts on a route.

type CORSConfig added in v2.14.0

type CORSConfig struct {
	// Origins are the origins allowed, exactly as a browser sends them:
	// scheme, host and port, no path ("https://shop.example",
	// "http://localhost:5173", "capacitor://localhost"). AnyOrigin alone
	// allows every one.
	Origins []string
	// Methods a request may use: GET, HEAD and POST when empty.
	Methods []string
	// Headers a request may carry besides the CORS-safelisted ones:
	// Content-Type when empty, and Authorization too under a TokenOnly
	// prefix. AnyHeader allows every one.
	Headers []string
	// ExposeHeaders are the response headers a script may read besides
	// the CORS-safelisted ones (Content-Type, Cache-Control…).
	ExposeHeaders []string
	// Credentials lets the origins send cookies: the browser sends them
	// and the app's cross-origin check lets these origins' requests
	// through under the prefix (as web.CSRF's trusted origins). It needs
	// listed origins, never AnyOrigin, and can't go with TokenOnly.
	Credentials bool
	// MaxAge is how long a browser may keep a preflight's answer: 5 minutes
	// when zero, never with a negative value. (Browsers cap it: Chrome at 2
	// hours.)
	MaxAge time.Duration
}

CORSConfig is what CORS lets other origins do under its prefix.

type CSRFVars added in v2.4.0

type CSRFVars struct {
	CSRFToken string
}

CSRFVars is CSRFContext's result: the variable csrfToken.

func CSRFContext added in v2.4.0

func CSRFContext(ctx context.Context) (CSRFVars, error)

CSRFContext is a context processor giving templates the variable csrfToken: a token bound to the visitor's browser (a random cookie it sets the first time) and sign-in (LiveIdentity), for a form whose handler checks it with ValidCSRFToken — one posted from another origin the app trusts, say. web's own protection (CSRF) checks the request's origin and needs no token.

type CompressionLevel added in v2.6.0

type CompressionLevel int

CompressionLevel is how hard the app compresses what handlers write: 1 (fastest) to 11 (smallest), brotli's quality; gzip's level is the same, up to 9. Fixed content (the runtime, stylesheets, scripts) is compressed once, harder.

const (
	// DefaultCompression is brotli's quality 5 (gzip's level 5): a page of
	// 250 KB in about 2 ms, to a 30th of its size.
	DefaultCompression CompressionLevel = 0
	// BestSpeed and BestCompression are the ends of the scale.
	BestSpeed       CompressionLevel = 1
	BestCompression CompressionLevel = 11
)

type CronLock

type CronLock interface {
	Claim(ctx context.Context, name string, tick time.Time) (bool, error)
}

CronLock decides which process runs a scheduled tick. Each process of an app schedules its crons by itself, so with several replicas every one runs every tick; provide a CronLock (backed by a database or Redis) and only the replica that claims a tick runs it. Every replica asks for the same name and tick — @every ticks too are aligned to a fixed origin — so a lock is a unique key: an INSERT of (name, tick) into a table with a unique index, or Redis's SET name:tick NX with an expiry. Without one, the app uses NewMemoryCronLock, which claims every tick. RunCron doesn't ask.

func NewMemoryCronLock

func NewMemoryCronLock() CronLock

NewMemoryCronLock is the lock of a single process: it claims each tick of a cron once.

type Error

type Error struct {
	Status  int
	Message string
	Err     error // the cause, never shown
}

Error is an error with an HTTP status. Returned by a handler (wrapped or not), it answers with that status and message: an HTML page for an HTML route, JSON otherwise.

func BadRequest

func BadRequest(msg string) *Error

BadRequest is a 400 error with msg.

func Status

func Status(status int, format string, args ...any) *Error

Status is an error answering with status and the formatted message.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches another *Error of the same status: errors.Is(err, web.NotFound).

func (*Error) PublicMessage

func (e *Error) PublicMessage() string

PublicMessage is the message the browser reads (live.PublicError): a live page's handler returning an *Error shows its message, as an HTTP route's answer does, where any other error's text stays in the log.

func (*Error) StatusCode

func (e *Error) StatusCode() int

StatusCode is the error's status.

func (*Error) Unwrap

func (e *Error) Unwrap() error

type FrontendOption

type FrontendOption func(*frontend)

FrontendOption configures a Frontend.

func FrontendAt

func FrontendAt(prefix string) FrontendOption

FrontendAt serves the frontend under prefix ("/admin") instead of the root: build it with the same base (Vite's base: "/admin/").

func FrontendEnv

func FrontendEnv(keys ...string) FrontendOption

FrontendEnv passes the values of these environment variables (and .env keys, which New has loaded) to the frontend's pages: index.html sets them on a global before any module runs, and the Vite plugin turns import.meta.env.KEY into a read of it. Only the keys named are passed; they reach every visitor, so name only public values. Unset keys are left out.

func FrontendRoot

func FrontendRoot(dir string) FrontendOption

FrontendRoot names the frontend project folder — the directory that holds the Vite project (package.json, src, node_modules) whose build output is the dist Frontend serves. The vuka command reads it to root the island build: it is esbuild's resolve base (node_modules for framework islands) and where islands.d.ts is written. It does not change where an @ui.Island("…") path resolves — that is relative to the Go file declaring the island. When unset, the build uses the parent of Frontend's root (its dist's directory).

type GraphQLBatch added in v2.8.0

type GraphQLBatch struct {
	Loader string
	Keys   int
	Start  time.Time
	Dur    time.Duration
	Err    string
}

GraphQLBatch is a batch a GraphQL loader ran.

type GraphQLField added in v2.8.0

type GraphQLField struct {
	Path  string // "pets.0.owner"
	Field string // "Pet.owner"
	Start time.Time
	Dur   time.Duration
	Err   bool
}

GraphQLField is a field a GraphQL request resolved, for the toolbar's GraphQL panel.

type GraphQLHandler added in v2.8.0

type GraphQLHandler func(ctx context.Context, w http.ResponseWriter, r *http.Request)

GraphQLHandler answers a GraphQL request, given the request's context: the one the app's handlers get, with its injector, identity and toolbar.

type GraphQLHost added in v2.8.0

type GraphQLHost struct {
	Path    string
	Options []any // GraphQL's options, as given
	Dev     bool  // the app runs in development
	Where   string
	// Dependency fills a resolver's parameter of type t, as a handler's
	// is filled: a provided service, the request, a ParamFrom type. Its
	// error is a Build error naming where.
	Dependency func(t reflect.Type, where string) (func(context.Context) (any, error), error)
	// Guard decides a call by its declaration's attributes (GuardCalls);
	// nil when no library gates calls. byDefault applies the library's
	// rule for a declaration carrying no gate (an operation's), rather than
	// letting it through (a field's).
	Guard func(ctx context.Context, attrs []any, byDefault bool) error
	// SetCheck, when the development toolbar is on, takes what checks a
	// document for its GraphiQL panel as the query is typed: given a
	// request's JSON body, the errors running it would meet before
	// execution, as a JSON object {"errors": […]}. Nil otherwise.
	SetCheck func(check func(body []byte) []byte)
}

GraphQLHost is what web gives package graphql to build an app's endpoint (RegisterGraphQL).

type Input added in v2.4.0

type Input struct {
	// Key names it, once per processor run: "auth", "request".
	Key string
	// Now is its current value for ctx — fast, and recording nothing.
	Now func(ctx context.Context) string
	// Personal says it is who the visitor is: a Browser processor reading
	// it must be Personal.
	Personal bool
}

Input is something a context processor's result depends on, which RecordInput records: a live page runs the processor again when Now returns another value than it did when the processor ran.

type Invalid

type Invalid map[string]string

Invalid is an error naming fields and what is wrong with each: a component's call answering with it shows each field's message (live.error.fields), a route answers 422 with them.

return web.Invalid{"q": "type at least 2 letters"}

func (Invalid) Error

func (e Invalid) Error() string

func (Invalid) FieldErrors

func (e Invalid) FieldErrors() map[string]string

func (Invalid) PublicMessage

func (e Invalid) PublicMessage() string

func (Invalid) StatusCode

func (Invalid) StatusCode() int

func (Invalid) Unwrap

func (e Invalid) Unwrap() error

type LiveCompressionMode added in v2.6.0

type LiveCompressionMode int

LiveCompressionMode is how live sockets compress their messages (permessage-deflate, RFC 7692), set with LiveCompression.

const (
	// NoContextTakeover, the default, compresses each message on its own:
	// a compressor is borrowed from a pool for the write and given back,
	// so an open socket holds none. The browser does the same for its
	// messages.
	NoContextTakeover LiveCompressionMode = iota
	// ContextTakeover keeps each socket's compressor and its 32 KB window
	// between messages, so a message can refer to the ones before it: a
	// few percent fewer bytes on a live page's session (2% on a 200-row
	// list), for about 800 KB more held by every open socket (8 GB at
	// LiveMaxConns' 10,000; NoContextTakeover adds about 2 KB).
	ContextTakeover
)

type LiveEvent

type LiveEvent struct {
	// contains filtered or unexported fields
}

LiveEvent is the live event a goroutine is handling: CurrentLiveEvent returns it to the event's handler and to its decorators, which, unlike the handler, may have no context.Context to read.

func CurrentLiveEvent

func CurrentLiveEvent() *LiveEvent

CurrentLiveEvent is the live event the calling goroutine handles, nil outside one: web's connections handle an event's handler, and every decorator around it, on the connection's goroutine.

func (*LiveEvent) Context

func (e *LiveEvent) Context() context.Context

Context is the context the page's route ran with (the join's, or the latest navigation's): its request's values, cookies and middleware's.

func (*LiveEvent) Handler

func (e *LiveEvent) Handler() string

Handler names the handler the event targets: "Admin.Delete", or "Admin.Render.func1" for a function literal.

func (*LiveEvent) Handling

func (e *LiveEvent) Handling() bool

Handling reports whether the event's handler is running — the handler itself, or anything it calls, a function literal's calls included — rather than the render of what it changed, which follows it on the same goroutine. A gate refusing an event checks calls made while Handling.

func (*LiveEvent) Refuse

func (e *LiveEvent) Refuse(err error)

Refuse makes err the event's error, which the page receives as a handler's (vk:error); for a decorator that doesn't let a handler without an error result run.

type LiveHandler

type LiveHandler struct {
	Name string // "Board.Clear"; "Board.Render.func1" for a function literal
	// Component is the struct type of the component whose event it is; nil
	// for a handler of a page that is no component.
	Component reflect.Type
	// Recv and Method are a method handler's receiver type (Board for
	// *Board) and name; Recv is nil for a type Vuka registered nothing for.
	Recv   reflect.Type
	Method string
	// Attrs are the handler's typed attributes, written on its declaration
	// (vuka.FuncOf); none for a function literal or a function without.
	// The types' own are vuka.TypeAttrs(Component) and
	// vuka.TypeAttrs(Recv).
	Attrs []any
	// Literal says the handler is a function literal (onClick={func() { … }}):
	// it has no attributes, and the methods it calls are checked by their
	// own decorators when it runs.
	Literal bool
}

LiveHandler describes the handler a live event targets, for a guard (GuardLiveEvents): what Vuka's registry knows of its declaration and of the component it belongs to.

type LiveOption

type LiveOption interface {
	RouteOption
	// contains filtered or unexported methods
}

LiveOption configures a page declared with Live; it is a RouteOption too.

func Layout

func Layout(layout func(page ui.Node) ui.Node, assets ...PageAsset) LiveOption

Layout wraps a Live page's component: the page is layout(component). Styles and scripts (Style, Script, CSS, JS) given with it belong to the layout's own markup.

type Message added in v2.4.0

type Message struct {
	Level string `json:"level"` // "info", "success", "warning", "error": the app's own words
	Text  string `json:"text"`
}

Message is a flash message: one Flash added, shown by the next page that renders the variable messages.

type MessagesVars added in v2.4.0

type MessagesVars struct {
	Messages []Message
}

MessagesVars is MessagesContext's result: the variable messages.

func MessagesContext added in v2.4.0

func MessagesContext(ctx context.Context) (MessagesVars, error)

MessagesContext is a context processor giving templates the variable messages: what Flash added for the visitor, each shown once — a page rendering them clears them.

type Middleware

type Middleware = func(http.Handler) http.Handler

Middleware wraps an http.Handler.

type NotForBrowser added in v2.4.0

type NotForBrowser interface{ NotForBrowser() }

NotForBrowser is a type whose values Browser never sends to the browser: auth.Perms, a list of what the visitor may do, is one.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures an App.

func BaseURL added in v2.11.0

func BaseURL(u string) Option

BaseURL is the app's public URL (https://example.com, or with the path it is mounted under): what AssetURL builds on where there is no request.

func CORS added in v2.14.0

func CORS(prefix string, cfg CORSConfig) Option

CORS lets other origins call the routes under prefix ("/api/" covers /api and every path under it): a preflight (OPTIONS with Access-Control-Request-Method) is answered 204 before any routing, middleware or cross-origin check, and every answer under prefix says which origin may read it. An origin not listed gets answers without those headers, which its browser keeps from the page; CORS is not access control, so gate the routes as any others.

web.New(
	web.TokenOnly("/api/"),
	web.CORS("/api/", web.CORSConfig{Origins: []string{"https://shop.example"}}),
)

The most specific prefix wins when several cover a path. A config that can't work (no origins, an origin with a path, Credentials with AnyOrigin) fails Build.

func CSRF

func CSRF(trusted ...string) Option

CSRF refuses cross-origin requests that change state: a POST, PUT, PATCH or DELETE a browser sends from another site's page (a form posted to the app, a fetch) answers 403, so a signed-in user's cookie can't be used by a page they happen to visit. Browsers say where a request comes from (Sec-Fetch-Site, else Origin), and net/http's CrossOriginProtection reads that: no token to put in every form. GET, HEAD and OPTIONS are never refused, so keep them free of side effects.

trusted are other origins allowed to send such requests ("https://admin.example.com"). A request carrying an Authorization: Bearer header is let through: a browser never adds one by itself, so it can't be forged by another site. Requests that say nothing of where they come from (curl, servers) are let through too, and so are those of a TokenOnly route, whose cookies are taken off, and those from an origin a CORS rule with Credentials lists. Live connections check their origin themselves (LiveOrigins).

func CSRFBypass added in v2.8.0

func CSRFBypass(patterns ...string) Option

CSRFBypass lets cross-origin requests of these patterns (net/http ServeMux patterns, "/webhooks/", "POST /sso/callback") through the app's CSRF protection: a payment provider's webhook, an SSO callback.

func Check

func Check(fn func(*App) error) Option

Check adds a check Build runs; see App.Check.

func Compression added in v2.6.0

func Compression(level CompressionLevel) Option

Compression sets how hard the app compresses its responses; Off turns compression off, for an app behind a proxy that already compresses. It is on, at DefaultCompression, unless set. Live sockets' messages are LiveCompression's.

func ContextProcessors added in v2.4.0

func ContextProcessors(processors ...any) Option

ContextProcessors lists the app's context processors: functions func(ctx context.Context, deps…) (S, error) with S a struct, whose parameters after ctx the app fills as it fills a handler's (what app.Service and Provide register, ParamFrom types, the request); or one wrapped by Auth, Browser or Personal. Build fails when one has another shape, a parameter nothing provides, or a variable another processor gives too (each error names both). It may be given more than once; the lists add up.

func CronClock

func CronClock(c cron.Clock) Option

CronClock runs the app's crons by clock instead of the system's: a virtual one in tests (crontest.Clock).

func CronContext

func CronContext(f func(context.Context) context.Context) Option

CronContext has every cron run with its context passed through f: a library marking crons as the app's own work.

func Dev

func Dev() Option

Dev turns on development where the process runs in it: VUKA_DEV=1 or VUKA_ENV=development (`VUKA_DEV=1 go run .`). Responses then show internal errors' messages and the toolbar is on, and Start runs tailwindcss --watch and each frontend's Vite dev server, when installed, and, for an app registering islands, vuka build-islands -watch (the vuka command a tool of the module or on PATH), the app serving through them; Stop ends them. Without the tools the app serves the last build and logs one line saying how to get them. Under vuka run, which runs them itself, it starts nothing. In a test binary it turns on development without starting anything; elsewhere — a production binary — it does nothing, so main can keep it.

func DotEnv

func DotEnv(files ...string) Option

DotEnv replaces the env files New reads (see LoadEnv); the first listed wins. Relative paths are read from the working directory, or the nearest parent up to the module root holding one of them.

func Eager

func Eager(ctors ...any) Option

Eager provides constructors whose values are built and started by Start even when no route needs them (workers, schedulers).

func ExactPaths

func ExactPaths() Option

ExactPaths turns off the trailing-slash redirect: a path differing from a route only by a trailing slash is then a 404 (or whatever else answers it, a frontend's index.html).

func FormStore added in v2.8.0

func FormStore(s ui.FormStore) Option

FormStore keeps what came of the app's form submits, by their idempotency keys, instead of in this process's memory: an app with several replicas keeps them where every replica sees them.

func Frontend

func Frontend(fsys fs.FS, root string, opts ...FrontendOption) Option

Frontend serves a built single-page app from fsys, the files under root:

//go:embed all:frontend/dist
var dist embed.FS

app := web.New(web.Frontend(dist, "frontend/dist"))

A GET path naming a file (its last segment has an extension) is that file, or 404; any other GET path is index.html, so the app's client-side routes work on a reload and from a link. The app's own routes, live pages and /_vuka/ are matched first. Files Vite's manifest lists as build output whose names carry a content hash are cached for good; everything else, index.html included, is revalidated by its ETag. The .vite directory is never served.

The app's islands are embedded with it: go tool vuka build-islands writes them into root's .vuka directory, and the app serves them from there. A build of islands alone, with no index.html, serves them and answers other paths 404:

//go:generate go tool vuka build-islands

Outside development a root without index.html or islands is a Build error. In development (Dev, VUKA_ENV=development, or vuka run) the files are read from disk when root is found there (beside the source calling Frontend, else in the working directory), a running Vite dev server serves the page's modules, and a frontend not built yet shows a page saying how to build it.

func GraphQL added in v2.8.0

func GraphQL(path string, opts ...any) Option

GraphQL serves the app's GraphQL schema at path, by GET and POST: the operations its packages declare with @graphql.Query and @graphql.Mutation (package github.com/vuka-lang/web/v2/graphql, which the code the compiler generates for them imports). opts are that package's options: graphql.Naming, graphql.Limits, graphql.Introspection.

app := web.New(web.GraphQL("/graphql"))

The route runs the app's middleware, CSRF protection and toolbar as any other; a library gating routes (web/auth) leaves the route itself open and gates each operation and field by its attributes instead (GuardCalls). Build fails when no operation is linked into the program.

func GuardCalls added in v2.8.0

func GuardCalls(f func(ctx context.Context, attrs []any, byDefault bool) error) Option

GuardCalls has f decide calls the app serves by their declarations' attributes: GraphQL's operations and fields (web.GraphQL). An error refuses the call — web.Unauthorized, web.Forbidden. byDefault asks for the library's rule for a declaration carrying none of its attributes (an operation: web/auth needs a sign-in unless the app is public by default); without it such a declaration passes (a field, gated by its operation already). A library gating routes registers one.

func GuardLiveEvents

func GuardLiveEvents(f func(ctx context.Context, h LiveHandler) error) Option

GuardLiveEvents has f decide each live event before its handler runs, given the page's context (the identity it joined with) and the handler: an error refuses the event — the handler doesn't run, and the page receives the error as a handler's (vk:error). A library gating events reads the handler's attributes and its component's type's here, where web dispatches, rather than only inside the handler's call.

func GuardVars added in v2.4.0

func GuardVars(f func(ctx context.Context, gates []any) error) Option

GuardVars has f decide each read of a variable of a processor wrapped in Auth, given the read's context and the gates Auth was given (each vuka.Bundle flattened into its attributes): an error refuses the read. A redirect (Redirect) or a *Error answers a page with it; on a live page the connection closes, and the page loads again. Package auth installs one.

func Headers

func Headers(h SecurityHeaders) Option

Headers replaces the defaults of the security headers every response carries (see SecurityHeaders): a field left zero keeps its default.

func IslandRoot

func IslandRoot(dir string) Option

IslandRoot names the folder island components build in, for an app that serves no frontend of its own (no Frontend): the one holding package.json and node_modules (vue, react, svelte, esbuild…), where islands.d.ts and vuka-live.d.ts are written. The vuka command reads it; it changes nothing at run time. The module root is the default.

func LiveAdopt

func LiveAdopt(window time.Duration) Option

LiveAdopt is how long after a live page's GET its join may adopt the session the render mounted, instead of mounting the page's components again: 10 seconds by default, enough for the page to load and connect. 0 (or less) turns adoption off: every join mounts fresh, and renders park nothing. The join still runs the page's route — middleware, gates and the handler, with the connection's own cookies — whether or not it adopts; only the mounting of components it skips.

func LiveCheck

func LiveCheck(f func(ctx context.Context) error) Option

LiveCheck has each joined live page checked with f every LiveCheckEvery (30 seconds by default), given the context the page's route ran with: an error closes the connection with code 4001 and the page reloads, its route running again for the browser. A page is checked when it mounts by its route itself — the join and each navigation run it through the app's middleware and gates — and its events and broadcasts trust what it mounted with; LiveCheck is the net for what changes while it sits open without the app hearing of it (a session deleted by another process). A library that ends sessions itself closes their pages at once with CloseLive.

func LiveCheckEvery

func LiveCheckEvery(d time.Duration) Option

LiveCheckEvery runs the app's LiveChecks on each joined page this often: 30 seconds by default; 0 never runs them. A connection's first check comes at a random point of the period, so pages opened together don't all check together. A page some check fails closes with code 4001 and reloads, so a session ended where the app doesn't hear of it (another process, a store edited by hand) or a revoked permission reaches pages that sit open; an ending the app hears of closes them at once (CloseLive).

func LiveCompression added in v2.6.0

func LiveCompression(m LiveCompressionMode) Option

LiveCompression sets how live sockets compress their messages: NoContextTakeover by default, ContextTakeover, or Off. Compression is negotiated with each client when its socket opens (every current browser offers it); one that doesn't offer it gets messages as they are. Messages under 256 bytes are sent as they are.

func LiveIdentity

func LiveIdentity(f func(ctx context.Context) string) Option

LiveIdentity tells live adoption and stateless pages' signed props who a context is signed in as: a stable key for the identity and its sign-in ("" for no one). A join adopts a parked session only when every registered function returns for the join's context what it returned for the render's — the same sign-in, or anonymous both times — and a stateless page's call verifies only for the visitor its props were signed for. A library that signs visitors in registers one, so one visitor's page is never adopted, or its props replayed, by another.

func LiveIdleTimeout

func LiveIdleTimeout(d time.Duration) Option

LiveIdleTimeout is how long a joined page may send nothing before its connection closes and its session, with its components' state, is dropped: 30 minutes by default; 0 never closes one. The runtime connects again, rebuilding the page's session, at once when the page is in view and otherwise when it is shown again; broadcasts don't keep a page from being idle.

func LiveJoinTimeout

func LiveJoinTimeout(d time.Duration) Option

LiveJoinTimeout is how long a live connection may stay open without joining its page: 10 seconds by default. A socket that sends nothing, or anything but its join, is closed when it runs out.

func LiveKey

func LiveKey(key []byte) Option

LiveKey is the key signing live pages' join tokens. Without it each app draws a random one, so a page rendered by one process can't join another: set it when several replicas serve the app.

func LiveLayout

func LiveLayout(layout func(page ui.Node) ui.Node, assets ...PageAsset) Option

LiveLayout is the layout of every Live page without its own. Styles and scripts given with it belong to the layout's own markup, as Layout's do.

func LiveMaxConns

func LiveMaxConns(n int) Option

LiveMaxConns caps the app's live connections: 10,000 by default. A socket beyond it is refused with 503 Service Unavailable before it opens, and the runtime tries again later; 0 sets no cap.

func LiveMaxConnsPerIP

func LiveMaxConnsPerIP(n int) Option

LiveMaxConnsPerIP caps the live connections from one client address: 100 by default (a building behind one address has many visitors). A socket beyond it is refused with 429 Too Many Requests; 0 sets no cap.

func LiveMaxConnsPerPage

func LiveMaxConnsPerPage(n int) Option

LiveMaxConnsPerPage caps the live connections open with the same page token, one rendered page's: 4 by default, room for a reconnect overlapping its old socket. A socket beyond it is refused with 429; 0 sets no cap.

func LiveOrigins

func LiveOrigins(patterns ...string) Option

LiveOrigins lets pages of other origins open live connections: host patterns as path.Match takes them ("app.example.com", "*.example.com"). By default only the app's own origin may.

func LiveProtocol

func LiveProtocol(v int) Option

LiveProtocol caps the live protocol version the app speaks: 2 (render trees: a page gets each component's statics once, then only the dynamics that changed) by default, or 1 (each changed component's HTML). A page asks for the newest it knows and gets at most this; v2 also needs a Vuka live session that speaks it, else pages get v1.

func LiveTokenTTL

func LiveTokenTTL(d time.Duration) Option

LiveTokenTTL is how long a rendered live page may (re)join; 12 hours by default. A page older than that reloads when it reconnects.

func LiveTrimAfter added in v2.16.0

func LiveTrimAfter(d time.Duration) Option

LiveTrimAfter makes a joined page that sends and receives nothing for d release what its session can rebuild (the hashes of what the browser holds), to make them again on its next event or broadcast. It is off by default (0): it saves about a tenth of an idle page's memory, and a mistake in it would show as a wrong update rather than an error. Replies don't change, and the page stays open; LiveIdleTimeout is what ends it.

func MaxRequestBody

func MaxRequestBody(n int64) Option

MaxRequestBody bounds every request's body, in bytes: a body longer gets 413. 32 MiB by default; negative for none. A route sets its own with @web.MaxBody.

func NoCrons

func NoCrons() Option

NoCrons keeps Start from scheduling the crons, for a replica that only serves requests; RunCron still runs them.

func NoDotEnv

func NoDotEnv() Option

NoDotEnv keeps New from reading env files.

func NoSecurityHeaders

func NoSecurityHeaders() Option

NoSecurityHeaders turns the default security headers off: a proxy in front of the app sets them, say.

func NoTailwind

func NoTailwind() Option

NoTailwind turns the app's stylesheet off: pages get no link to /_vuka/app.css and the vuka command builds none.

func NoToolbar

func NoToolbar() Option

NoToolbar turns the development toolbar off.

func OnError

func OnError(f func(w http.ResponseWriter, r *http.Request, e *Error, html bool)) Option

OnError replaces the error page and JSON error body.

func OnStart

func OnStart(fn func(context.Context) error) Option

OnStart adds a hook Start runs; see App.OnStart.

func OnStop

func OnStop(fn func(context.Context) error) Option

OnStop adds a hook Stop runs; see App.OnStop.

func Only

func Only(pkgs ...string) Option

Only limits the declared routes and controllers an app takes to those of the given packages (import paths; "main" for the main package).

func Options

func Options(opts ...Option) Option

Options is one Option applying opts in order; nil options are skipped.

func ParamFrom

func ParamFrom[T any](f func(r *http.Request) (T, error)) Option

ParamFrom binds handler parameters of type T with f, before any other rule: f's error answers the request as a handler's would. A library gives handlers its own values this way — the signed-in user, say.

func PartsWindow added in v2.13.0

func PartsWindow(d time.Duration) Option

PartsWindow is how long a page's deferred parts, their methods started with the page's render, wait on the server for the runtime's request for them once the page has loaded: 30 seconds by default. A request after it (or on another replica, or after a restart) renders the page again for its parts. 0 (or less) parks nothing: the methods run on that request, after the page has loaded.

func PersonalPackage added in v2.4.0

func PersonalPackage(path string) Option

PersonalPackage says that what package path (an import path) provides reads who the visitor is: a Browser processor of that package, or taking one of its types as a parameter, must be Personal too. Package auth declares itself.

func Provide

func Provide(ctors ...any) Option

Provide adds constructors to the app's container: func(deps…) T or func(deps…) (T, error). A value that isn't a function is provided as is.

func Require

func Require(keys ...string) Option

Require fails Build when one of the environment variables is unset or empty, naming each missing one and the env files looked for.

func SessionCookies added in v2.8.0

func SessionCookies(names ...string) Option

SessionCookies names cookies that carry a visitor's session, for the CSRF protection to know a request carrying one: an unsafe request saying nothing of where it comes from (no Sec-Fetch-Site, no Origin) needs the form's token when it carries one of them, or the app's own (vk_csrf). The auth module names its session cookie itself.

func Tailwind

func Tailwind(path string) Option

Tailwind names the project's Tailwind stylesheet, relative to the module root, when it isn't app.css or styles/app.css. The vuka command reads the path from the source, so it must be a string literal.

func TokenOnly added in v2.14.0

func TokenOnly(prefix string) Option

TokenOnly declares the routes under prefix ("/api/" covers /api and every path under it) a token API: a request there signs in with its Authorization header alone. Its cookies are taken off before any middleware reads them, so no session or CSRF cookie rides along, the Set-Cookie headers of its answers are dropped, and, with no cookie to abuse, the app's cross-origin check is off there: another origin's script may POST, with a bearer token or none (a sign-in). The two can't be had apart: the check is never off for a request carrying cookies.

Pages, forms and live views need cookies: keep them off a TokenOnly prefix. Pair it with CORS for browsers on other origins.

func TrackRequests

func TrackRequests() Option

TrackRequests has the app note the goroutine serving each request, and the context of the route it runs, for CurrentRequest: a library checking work a request's goroutine does without being given the request's context.

func TrustedProxies

func TrustedProxies(cidrs ...string) Option

TrustedProxies names the proxies in front of the app, as CIDRs ("10.0.0.0/8", "fd00::/8") or single addresses: from them, and only from them, ClientIP reads X-Forwarded-For (else Forwarded), and an HTTPS request is told by X-Forwarded-Proto. An entry that doesn't parse fails Build.

func WithRelay

func WithRelay(r Relay) Option

WithRelay sends the app's broadcasts through r instead of delivering them in-process only.

func WithRouter

func WithRouter(r Router) Option

WithRouter serves the app's routes with r instead of net/http's ServeMux.

func WrapRoutes

func WrapRoutes(name string, f func(RouteInfo) Middleware) Option

WrapRoutes has Build call f with each route of the app (decorated or added in code, Live pages included); the middleware f returns, if not nil, wraps the route outside its own (@web.Use) middleware and inside the app's. Functions added first wrap outermost. name is what a RouteAttr names.

type OptionFunc

type OptionFunc func(*App)

OptionFunc is an Option written as a function of the App, for libraries defining their own options: it runs inside New, before Build, and may call any of the App's methods (Provide, Use, Handle, OnStart, Check, …).

type PageAsset added in v2.11.0

type PageAsset struct {
	// contains filtered or unexported fields
}

PageAsset is a stylesheet or script for a page: an option of @web.Live, web.Layout, web.LiveLayout and web.New. The vuka command reads it from the source and compiles it; the app itself only serves what it compiled.

func CSS

func CSS(src ...string) PageAsset

CSS is a stylesheet written inline, as Style's files are: web.CSS(`output { … }`).

func JS

func JS(src ...string) PageAsset

JS is a hook module written inline, as Script's files are.

func Script

func Script(paths ...string) PageAsset

Script names hook modules (TypeScript or JavaScript) for the component the option belongs to, as Style does stylesheets. They are bundled in order into one module, loaded when the component first renders on a page; each may export default { mounted(el, ctx), updated(el, ctx), destroyed(el, ctx), …methods }. ui.JS.Call("name") reaches the component's own methods before vuka.register's and window's.

func Style

func Style(paths ...string) PageAsset

Style names stylesheets for the component the option belongs to: the live component, or the layout's own markup (app-wide, for web.New). Paths are relative to the .vuka file and may be globs (parts/*.css, styles/**/*.css), expanded in name order; they must be string literals, since the vuka command reads them from the source. Selectors match only the elements the component's own markup renders: :global(…) and :root stay unscoped, :deep(…) reaches into child components, :scope is the component's root element. The stylesheets compile with the app's Tailwind CSS, in the order listed (later ones win), so @apply, theme() and variants work.

func (PageAsset) Attrs added in v2.11.0

func (PageAsset) Attrs() []any

type PageParams

type PageParams struct {
	Page int `form:"page" json:"page"`
	Size int `form:"size" json:"size"`
}

PageParams binds ?page= and ?size= for a plain handler:

func List(p web.PageParams, …) (…) {
	page, size := p.Normalize()
	…
}

Its tags let it bind as a handler's request struct; Normalize applies the defaults and the size cap.

func (PageParams) Normalize

func (p PageParams) Normalize() (page, size int)

Normalize returns the 1-based page and the size, defaulting size to DefaultPageSize and capping it at MaxPageSize.

type Pageable

type Pageable[Q any, T any] interface {
	Count(ctx context.Context) (int64, error)
	Limit(n int) Q
	Offset(n int) Q
	All(ctx context.Context) ([]T, error)
}

Pageable is the shape of an ORM query a paginator needs: count the matches, narrow to a window with Limit and Offset, and fetch it with All. Q is the query's own type, so Limit and Offset chain. github.com/vuka-lang/orm/v2's QuerySet[T] satisfies Pageable[orm.QuerySet[T], T], so Paginate takes one directly with no import of orm here.

type Pager

type Pager[T any] struct {
	// contains filtered or unexported fields
}

Pager is a live view's paging state: the current page and size, and the events a ui.DataTable's controls call (GoTo, Next, Prev, SetSize). A view keeps one, gives it a loader that fetches a page, and renders Result — so it never hand-rolls the page arithmetic or the event handlers:

type Books struct {
	web.Live        // ui.Live
	DB    *DB       // injected
	pager *web.Pager[Book]
}

@ui.Mount
func (b *Books) mount(ctx context.Context) error {
	b.pager = web.NewPager(20, func(ctx context.Context, page, size int) (ui.Page[Book], error) {
		return web.Paginate(ctx, orm.For[Book](), page, size)
	})
	return b.pager.Load(ctx)
}

func (b *Books) Render() ui.Node {
	return ui.DataTable[Book]{
		Columns: cols,
		Page:    b.pager.Result(),
		OnPage:  b.pager.GoTo,  // func(context.Context, int) error
	}
}

The loader is called with the page and size the pager holds; after each load the pager adopts the page the result settled on, so Next past the last page is a no-op (and the DataTable disables it anyway).

func NewPager

func NewPager[T any](size int, load func(ctx context.Context, page, size int) (ui.Page[T], error)) *Pager[T]

NewPager makes a pager starting on page 1 with the given size (or DefaultPageSize when size < 1); load fetches one page.

func (*Pager[T]) Err

func (p *Pager[T]) Err() error

Err is the error of the last Load, if any.

func (*Pager[T]) GoTo

func (p *Pager[T]) GoTo(ctx context.Context, n int) error

GoTo moves to page n (1-based) and fetches it.

func (*Pager[T]) Load

func (p *Pager[T]) Load(ctx context.Context) error

Load fetches the current page, remembering the result and any error, and clamps the current page to the page the result settled on.

func (*Pager[T]) Next

func (p *Pager[T]) Next(ctx context.Context) error

Next moves to the following page and fetches it.

func (*Pager[T]) Page

func (p *Pager[T]) Page() int

Page is the current page number.

func (*Pager[T]) Prev

func (p *Pager[T]) Prev(ctx context.Context) error

Prev moves to the preceding page and fetches it.

func (*Pager[T]) Reload

func (p *Pager[T]) Reload(ctx context.Context) error

Reload returns to page 1 and fetches it, after the data behind the pager changed (a new search or sort).

func (*Pager[T]) Result

func (p *Pager[T]) Result() ui.Page[T]

Result is the last loaded page (the zero Page before the first Load).

func (*Pager[T]) SetSize

func (p *Pager[T]) SetSize(ctx context.Context, size int) error

SetSize changes the page size, returns to page 1 and fetches it.

func (*Pager[T]) Size

func (p *Pager[T]) Size() int

Size is the current page size.

type Processor added in v2.4.0

type Processor struct {
	// contains filtered or unexported fields
}

Processor is a context processor wrapped by Auth, Browser or Personal, for ContextProcessors' list.

func Auth added in v2.4.0

func Auth(p any, gates ...any) Processor

Auth makes reads of p's variables pass gates first, as a route's: given auth.Required, a read by an anonymous visitor sends them down the sign-in path — a page answers with the redirect to the login page (or 401), a live page loads again — instead of the processor running. A gate is an attribute value, a vuka.Bundle, or a function returning one (pass auth.Required itself, or auth.Requires("x")); a library guarding reads (GuardVars, which auth.Module installs) decides it, and Build fails when the app has none.

func Browser added in v2.4.0

func Browser(p any) Processor

Browser lets the browser read p's variables: islands' components and the page's scripts get them with ctx.value("name"). No other processor's variable reaches the browser. Build refuses a Browser processor that reads the signed-in user (one of package auth, or taking one of its types) unless it is wrapped in Personal too, and a value whose type is NotForBrowser (auth.Perms) is never sent.

func Personal added in v2.4.0

func Personal(p any) Processor

Personal says p's variables are the visitor's own, which Browser may send to their browser though p reads the signed-in identity: web.Browser(web.Personal(p)). It means nothing outside Browser, which Build reports.

type QueryEdits added in v2.4.0

type QueryEdits struct{}

QueryEdits is the type of Query.

var Query QueryEdits

Query edits the URL of a live page from one of its events: the change is a live patch of the page, sent once the event's reply is applied, that keeps the other parameters and takes the current history entry's place (Push adds one instead). The page then takes the URL as any patch does — its Update taking web.URLParams, or else it mounts again — and its components' @ui.URL fields fill from it.

func (t *Requests) Filter(ctx context.Context, status string) {
	web.Query.Set(ctx, "status", status)
	web.Query.Del(ctx, "page")
}

Several edits in one event are one patch. Outside a live page's event an edit does nothing.

func (QueryEdits) Del added in v2.4.0

func (QueryEdits) Del(ctx context.Context, key string, opts ...QueryOption)

Del removes the query parameter key.

func (QueryEdits) Get added in v2.4.0

func (QueryEdits) Get(ctx context.Context, key string) string

Get is the query parameter key of the page whose event (or request) ctx belongs to, with the event's edits so far.

func (QueryEdits) Set added in v2.4.0

func (QueryEdits) Set(ctx context.Context, key string, value any, opts ...QueryOption)

Set sets the query parameter key to value: a string as it is, a number or bool as strconv writes it, a type with MarshalText as it writes it, a slice as one value per element.

func (QueryEdits) Update added in v2.4.0

func (QueryEdits) Update(ctx context.Context, edit func(url.Values), opts ...QueryOption)

Update edits the query in place: edit gets the page's parameters, as the event's edits so far left them.

type QueryOption added in v2.4.0

type QueryOption interface {
	// contains filtered or unexported methods
}

A QueryOption changes how an edit of the query moves the page.

var Push QueryOption = pushOption{}

Push makes an edit of the query add a history entry, so that back returns to the URL before it.

type Redirect

type Redirect string

Redirect, returned by a handler as its result or as its error, redirects to the URL: 302 for GET and HEAD, 303 See Other otherwise. It is also a vuka.Node, so a handler returning a page can redirect instead.

func (Redirect) Error

func (r Redirect) Error() string

func (Redirect) RedirectLocation added in v2.8.0

func (r Redirect) RedirectLocation() string

RedirectLocation is the URL: a live page's handler returning a Redirect moves the page there (ui.RedirectOf).

func (Redirect) Render

func (r Redirect) Render(context.Context, io.Writer) error

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds what an app is made of: routes, live pages and controllers. Routes is the package's, filled at init by the declarations of every package the program links; New gives each app a copy, and an App's methods of the same names add to its own:

func init() {
	web.Routes.Get("/pets/{id}", ShowPet)
	web.Routes.Live("/board", NewBoard)
}

app.Get("/pets/{id}", ShowPet)

A handler registered from Go binds its scalar parameters to the path's segments in order; one the compiler registers binds them by name, which it records (vuka.RegisterParams), from the path or the query. Services are the app's: app.Service in main.

It is what the decorators are: @web.Get("/pets/{id}") on ShowPet is the call above.

func (*Registry) Controller

func (g *Registry) Controller(prefix string, ctor any, opts ...RouteOption) *Registry

Controller makes the struct ctor builds a service whose route methods (their method expressions registered: (*PetAdmin).Index) are served under prefix.

func (*Registry) Delete

func (g *Registry) Delete(path string, fn any, opts ...RouteOption) *Registry

func (*Registry) Get

func (g *Registry) Get(path string, fn any, opts ...RouteOption) *Registry

func (*Registry) Handle

func (g *Registry) Handle(method, path string, fn any, opts ...RouteOption) *Registry

Handle registers fn for method and path: a handler, its parameters filled by type (see inject.go), or the constructor of an @ui.Island struct (a page backed by its island).

func (*Registry) Live

func (g *Registry) Live(path string, page any, opts ...RouteOption) *Registry

Live serves a stateful component as a page at path (see the Live decorator): page is its constructor, NewBoard, which builds each page's component, its parameters of provided types injected and the others zero. With ui.Island among opts it is a page backed by its island.

func (*Registry) Patch

func (g *Registry) Patch(path string, fn any, opts ...RouteOption) *Registry

func (*Registry) Post

func (g *Registry) Post(path string, fn any, opts ...RouteOption) *Registry

func (*Registry) Put

func (g *Registry) Put(path string, fn any, opts ...RouteOption) *Registry

type Relay

type Relay interface {
	Publish(ctx context.Context, topic string, msg any) error
	Subscribe(deliver func(topic string, msg any))
}

Relay carries broadcasts between the replicas of an app: Publish sends a message to every replica, and each calls the function Subscribe was given with what arrives (its own messages included). Encoding the messages, whose dynamic types pick the Update methods they reach, is the relay's business.

type RequestInfo added in v2.4.0

type RequestInfo struct {
	Path    string            // "/users/42"
	Query   url.Values        // the URL's query parameters
	Params  map[string]string // the route's path parameters by name: {"id": "42"}
	Name    string            // the route's handler: "users.Show"
	Pattern string            // the route: "GET /users/{id}"
}

RequestInfo is the request a page renders for, as templates read it.

type RequestNote

type RequestNote struct {
	Section string // the heading it goes under: "Auth"
	Name    string
	Value   string
	Refused bool // shown as a failure
}

RequestNote is a row of the development toolbar's Request panel.

type RequestVars added in v2.4.0

type RequestVars struct {
	Request RequestInfo
}

RequestVars is RequestContext's result: the variable request.

func RequestContext added in v2.4.0

func RequestContext(ctx context.Context) (RequestVars, error)

RequestContext is a context processor giving templates the variable request: the path, query, path parameters and route of the request the page renders for. Its input is the URL: on a live page that moves to another, the components showing it render again, a layout that stays among them.

type Rest

type Rest string

Rest is a catch-all path parameter: a field of type Rest binds the rest of the path a "{name...}" segment matches, slashes included.

type FileParams struct{ Path web.Rest } // GET /files/{path...}

type RouteAttr

type RouteAttr interface{ RouteWrapper() string }

A RouteAttr is a typed attribute that only a WrapRoutes function gives a meaning to — a permission a route requires, say. Build fails when a route carries one and the app has no WrapRoutes of the name RouteWrapper returns, so a route never goes unguarded because the library guarding it wasn't installed.

type RouteInfo

type RouteInfo struct {
	Method, Path string
	Where        string // "main.ShowPet at pages.vuka:24", "live page main.Counter"
	// Attrs are the typed attributes of the route's declaration, after those
	// of its controller's type: the attributes of a @web.Live page's type,
	// or those given with web.Attrs from Go.
	Attrs []any
	Live  bool // a Live page
	// LiveType is a Live page's component type (the struct, not a
	// pointer); nil for other routes.
	LiveType reflect.Type
	// HTML says the route answers with a page (a ui.Node, a view or a Live
	// page): its errors are pages, and a browser may be sent elsewhere.
	HTML bool
	// RootLayout is the route's root layout, the outermost of its
	// web.LayoutOf layouts (the struct type); nil when it has none. Pages
	// under one root layout share a document: in-app navigation between
	// them patches the page, and between two roots is a full page load.
	RootLayout reflect.Type
	// Calls says the route gates each call it serves itself, by the
	// call's declaration (GuardCalls): web.GraphQL's operations and
	// fields. A library gating routes leaves such a route open unless its
	// attributes carry a gate.
	Calls bool
}

RouteInfo describes a route for the function WrapRoutes registers.

type RouteOption

type RouteOption interface{ Attrs() []any }

RouteOption configures a registration (App.Get, Registry.Live, …): one of web's (Template, Use, Layout, Wrap, Attrs), another package's (an auth gate, ui.Island) or any value carrying typed attributes, which become the route's, as attributes written on a decorated handler are.

func Attrs

func Attrs(attrs ...any) RouteOption

Attrs gives a route registered in code typed attributes, as attributes written on a decorated handler are: they reach WrapRoutes. A vuka.Bundle (a composed decorator, such as one a library's attribute function returns) gives its attributes. It also goes to App.Live.

func GuardOf

func GuardOf[T any]() RouteOption

GuardOf runs the guard T before a route's page or handler: a struct, rendering nothing, whose Load(ctx context.Context, deps…) error, its dependencies provided types, checks the request:

type Guard struct{ Params }

func (g *Guard) Load(ctx context.Context, perms *Perms) error {
	if !perms.Can(ctx, "employers.view", g.Employer) {
		return web.Forbidden
	}
	return nil
}

Per request it is a zero T, its path parameters bound as a page's are, then its Load run; guards and layouts load in the order their options are given, before the page mounts or the handler runs, and the first error answers the request as the handler's would: NotFound a 404, Forbidden a 403, a Redirect redirects, as a page or as JSON. A Live page's guards run on its request and on its join. GuardOf implies PageRoute.

func LayoutOf

func LayoutOf[T any]() RouteOption

LayoutOf wraps a route's page in the layout T, a stateless struct component (T or *T a ui.Node) with a field Children ui.Node, the page or the next layout. Several nest in the order given, the first outermost, and replace Layout and LiveLayout for the route:

type Layout struct {
	Params            // path parameters, bound as a page's are
	Children ui.Node
	Employer Employer
}

func (l *Layout) Load(ctx context.Context, store *Store) error { … }

Per request each layout is a zero T, its path parameters bound and its unexported fields of provided types injected (as PageRoute's page's), then its Load run, if it has one: Load(ctx, deps…) returning nothing or an error, its dependencies provided types; its error answers the request. Layouts and guards (GuardOf) load in the order their options are given, before a stateless page's mount hook. A Live page's layouts render around it on the request's render; the page's patches render the page. On a handler's route (Post, …) every layout is loaded before the handler runs, whatever it answers, a Load error answering instead; a ui.Node the handler returns for a whole page is wrapped in them. LayoutOf implies PageRoute.

func PageRoute

func PageRoute() RouteOption

PageRoute makes a route a page as a pages/ folder declares it, what vuka's folder routes register:

web.Routes.Get("/employers/{employer}", employer.NewIndex, web.PageRoute())

The page's struct is built by its constructor, its parameters of provided types injected and the others zero; its exported fields, promoted ones from embedded structs included, bind as a Live page's do (a provided type injected, text from the path segment, else the query parameter, of its name); a path parameter that doesn't parse or fails its validate: tag answers 404. Its unexported fields of provided types are its dependencies, injected after the constructor runs, as a page-backed island's are, so a field users *users.Service needs no NewT, and its other unexported fields, its tracked state among them, are left as the constructor made them; a dependency field the URL would bind too (named as a path parameter, tagged url:) is E0419 at start-up. A page whose pointer has Title() string titles the document: ui.PageTitle reads it in the layouts. One with Crumbs() []ui.Crumb gives them its breadcrumbs, read with ui.PageCrumbs. A stateless page's Title and Crumbs are read after its mount hook; a Live page's once its fields are bound, before its mount hook, which its session runs as it renders. On Get, the constructor's struct is a stateless page, a ui.Node: the mount hook its LiveSchema declares (ui.MountPage), if any, runs once its fields are bound, and the page renders. LayoutOf implies PageRoute. On a function handler (a folder's method func, GET in [id].vuka) a scalar parameter bound from a path segment that doesn't parse answers 404 too.

func Receiver

func Receiver(ctor any) RouteOption

Receiver builds the receiver of a method handler for each request with ctor, a page's constructor, binding the route's path parameters and injecting its dependency fields as PageRoute does a page's (404 when a parameter doesn't parse):

web.Routes.Post("/employers/{employer}/edit", (*edit.Edit).Post, web.Receiver(edit.NewEdit))

The handler's other parameters bind as any handler's.

func ReloadLayout added in v2.1.0

func ReloadLayout[T any]() RouteOption

ReloadLayout has the layout T of a route's LayoutOf load again on every in-app navigation, what vuka's folder routes register for a layout whose Load or markup reads the request — web.Request, its URL or query. Other layouts persist: navigating over a page's live connection to another page under the same layouts, their path parameters the same, keeps them as they are — their Load doesn't run again, and the elements, scroll, client state and live components they render stay — and renders the page below them. Their markup renders again for the new page, the page getting only what changed of it: a header of ui.PageTitle and ui.PageCrumbs read there follows the page, where one Load stored stays the first page's (vuka registers a layout reading them in Load with the option). A layout reloading persists all the same in the page, its Load running again. A layout whose Load takes the *http.Request reloads without the option. Guards (GuardOf) run on every navigation.

func UseOf

func UseOf(mw any) RouteOption

UseOf wraps the route in mw, HTTP middleware: a func(next http.Handler) http.Handler, or its constructor, func(deps…) web.Middleware, its dependencies provided types, resolved when the app is built; what vuka's folder routes register for a folder's Middleware. Several wrap in the order given, the first outermost, and they wrap the whole route — outside its Use middleware, its guards and layouts, and its binding, so a path parameter's 404 happens inside them. Not calling next answers the request itself. On a Live page it wraps the GET and the join, never the socket's events.

func View

func View(f vuka.File, name ...string) RouteOption

View renders the route's result with a view, as Template does.

func Wrap

func Wrap(mw ...Middleware) RouteOption

Wrap adds middleware to the route, as Use does.

type Router

type Router interface {
	http.Handler
	Handle(method, pattern string, h http.Handler)
	Param(r *http.Request, name string) string
}

Router matches requests to routes. Patterns use the canonical syntax: "/pets/{id}" binds a segment, "/files/{path...}" the rest of the path, and a pattern matches its path exactly. Param returns a bound value for a request the router dispatched.

type SecurityHeaders

type SecurityHeaders struct {
	// ContentTypeOptions: "nosniff" by default, so a browser never runs
	// an upload or a JSON answer as a script or a page.
	ContentTypeOptions string
	// ReferrerPolicy: "strict-origin-when-cross-origin" by default, so a
	// URL's path and query stay on the site.
	ReferrerPolicy string
	// FrameOptions: "DENY" by default, or "SAMEORIGIN": who may show the
	// app's pages in a frame. The CSP's frame-ancestors follows it.
	FrameOptions string
	// CSP is the Content-Security-Policy: "frame-ancestors 'none'" by
	// default ('self' with FrameOptions "SAMEORIGIN"; left out with "-").
	CSP string
	// HSTS is the Strict-Transport-Security max-age sent on HTTPS requests
	// (r.TLS, or X-Forwarded-Proto: https from a trusted proxy); 365 days
	// by default. HSTSSubdomains adds includeSubDomains.
	HSTS           time.Duration
	HSTSSubdomains bool
}

SecurityHeaders are the headers every response of an app carries unless its handler sets them itself. The zero value of a field is its default; "-" (or, for HSTS, a negative duration) leaves the header out.

type ServeMux

type ServeMux struct {
	// contains filtered or unexported fields
}

ServeMux is the default Router: net/http's ServeMux with Go 1.22 patterns.

func NewServeMux

func NewServeMux() *ServeMux

func (*ServeMux) Handle

func (m *ServeMux) Handle(method, pattern string, h http.Handler)

func (*ServeMux) Param

func (m *ServeMux) Param(r *http.Request, name string) string

func (*ServeMux) ServeHTTP

func (m *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Source

type Source[T any] interface {
	Count(ctx context.Context) (int, error)
	Slice(ctx context.Context, limit, offset int) ([]T, error)
}

Source is where a paginator reads: the total number of rows a query matches, and one window of them. It has no dependency on any ORM, so web does not import one; an orm.QuerySet[T] is adapted by Paginate (it satisfies Query), and any store can implement Source directly.

type Starter

type Starter interface{ Start(context.Context) error }

Starter is a provided value with work to do when the app starts: Run calls Start in dependency order.

type Statement

type Statement struct {
	SQL      string
	Args     []any
	DB       string // the database's name
	Start    time.Time
	Duration time.Duration
	Rows     int64 // rows affected; -1 for a query
	Err      error
	// Refused is a statement the database layer refused before sending it;
	// Err says why.
	Refused bool
	// Origin and Field are what ran it, as StatementOrigin marked its
	// context: a GraphQL field's path and name; NoteStatement fills them.
	Origin, Field string
}

Statement is a statement for the toolbar's SQL panel: what a database layer passes NoteStatement. orm's ormweb reports the ORM's statements by itself.

type Stopper

type Stopper interface{ Stop(context.Context) error }

Stopper is a provided value with work to do when the app stops: Stop is called in the reverse order of construction.

type Templates

type Templates func(*vuka.Decl)

Templates is what Template returns: a declarer giving a route its view, and an option doing so.

func Template

func Template(f vuka.File, name ...string) Templates

Template renders a route's result with a view: a .templ file of the same package (its one exported component, or the one named) or an html/template file (its main template, or the one named). The order of @web.Template and the route's method decorator doesn't matter; from Go it is an option: app.Get("/pets/{id}", ShowPet, web.Template(petView)).

func (Templates) Attrs

func (t Templates) Attrs() []any

Attrs makes the view a route's option.

type URLParams

type URLParams struct{}

URLParams, embedded in a struct, makes it the parameters a Live page's Update method takes when the page is patched — moved to another URL of its route, the component staying mounted:

type ShelfParams struct {
	web.URLParams
	Page int // ?page=
}

func (s *Shelf) Update(p ShelfParams) { s.load(p.Page) }

Its fields bind from the new URL as the component's props do (path segment, else query parameter, of their name); the component's props are bound again too. Update runs before the patch renders. Mount, not Update, sees the first URL.

Only a page with such an Update stays mounted across a patch, whatever the browser asks: any other mounts again, Mount seeing the new URL. So Update must make every check Mount makes of what the URL names — that the visitor may see order {id}, say. An error it returns fails the patch: nothing of the page stays mounted, and the browser loads the URL, the route answering as for any request.

An Update taking URLParams itself keeps the page mounted taking nothing of the URL: for a page whose components keep their state in it (@ui.URL fields, Query), which fill from the new URL as they render.

type Uses

type Uses func(*vuka.Decl)

Uses is what Use returns: a declarer adding middleware to one route, a route's option doing so, and an Option adding it to the whole app.

func Use

func Use(mw ...Middleware) Uses

Use adds HTTP middleware, the first given outermost: @web.Use(mw) on a route, or web.New(web.Use(mw)) for every request of the app.

func (Uses) Attrs

func (u Uses) Attrs() []any

Attrs makes the middleware a route's option.

Directories

Path Synopsis
Package auth signs users in to a web app and guards its routes, live pages and live events.
Package auth signs users in to a web app and guards its routes, live pages and live events.
authtest
Package authtest helps test apps using auth: requests and live sessions signed in as anyone, without a login, and Users kept in memory.
Package authtest helps test apps using auth: requests and live sessions signed in as anyone, without a login, and Users kept in memory.
Package cron parses cron schedules and computes their next run: five fields (minute hour day-of-month month day-of-week), the descriptors @yearly, @monthly, @weekly, @daily, @hourly and @every <duration>, and a CRON_TZ= prefix naming the time zone the fields are read in.
Package cron parses cron schedules and computes their next run: five fields (minute hour day-of-month month day-of-week), the descriptors @yearly, @monthly, @weekly, @daily, @hourly and @every <duration>, and a CRON_TZ= prefix naming the time zone the fields are read in.
Package crontest tests an app's crons: Run runs one by name with its dependencies, and Clock is a virtual clock to test schedules with — time stands still until the test moves it.
Package crontest tests an app's crons: Run runs one by name with its dependencies, and Clock is a virtual clock to test schedules with — time stands still until the test moves it.
examples
auth/accounts
Package accounts is the app's users: who can sign in, and as whom.
Package accounts is the app's users: who can sign in, and as whom.
auth/notices
Package notices is the board's notices, shared by every page and the API.
Package notices is the board's notices, shared by every page and the API.
islands/registry
Package registry is the applicants' store, provided to the app.
Package registry is the applicants' store, provided to the app.
live/library
Package library is the books the shelf and the book pages show.
Package library is the books the shelf and the book pages show.
live/pages/books
Package books is the shelf, /books, and in [id] a page per book, /books/{id}, all in a layout of their own.
Package books is the shelf, /books, and in [id] a page per book, /books/{id}, all in a layout of their own.
live/rooms
Package rooms is the chat's: what it broadcasts, and each room's history.
Package rooms is the chat's: what it broadcasts, and each room's history.
pets/model
Package model holds the pets app's data types, shared by its handlers and its views.
Package model holds the pets app's data types, shared by its handlers and its views.
pets/pages/signup
Package signup is /signup: the form, its POST, and the page after it.
Package signup is /signup: the form, its POST, and the page after it.
Package graphql serves GraphQL: a schema of Go values, the validation and execution of operations against it, introspection, the schema's SDL, and the GraphQL-over-HTTP transport.
Package graphql serves GraphQL: a schema of Go values, the validation and execution of operations against it, introspection, the schema's SDL, and the GraphQL-over-HTTP transport.
language
Package language reads and writes GraphQL documents: a lexer and a parser for executable documents (operations and fragments) and type-system documents (schema definitions, the SDL), the syntax tree both produce, and a printer.
Package language reads and writes GraphQL documents: a lexer and a parser for executable documents (operations and fragments) and type-system documents (schema definitions, the SDL), the syntax tree both produce, and a printer.
internal
chrome
Package chrome drives a headless Chrome over the DevTools protocol, for the tests of live pages: open pages, click, type, press keys and evaluate JavaScript.
Package chrome drives a headless Chrome over the DevTools protocol, for the tests of live pages: open pages, click, type, press keys and evaluate JavaScript.
liveproto
Package liveproto is the seam between the web framework's live transport and the Vuka live session it drives: a Session speaks the protocol version its page joined with (v1: HTML, v2: render trees) and answers each client message with the message to send back.
Package liveproto is the seam between the web framework's live transport and the Vuka live session it drives: a Session speaks the protocol version its page joined with (v1: HTML, v2: render trees) and answers each client message with the message to send back.
livetree
Package livetree is a stand-in for the Vuka live session's protocol v2 until the session speaks it: it runs a v1 session and turns the HTML of each changed instance into render trees and their changes, in the format of Vuka's "Protocol v2: render trees" (docs/features/components.md).
Package livetree is a stand-in for the Vuka live session's protocol v2 until the session speaks it: it runs a v1 session and turns the HTML of each changed instance into render trees and their changes, in the format of Vuka's "Protocol v2: render trees" (docs/features/components.md).
testenv
Package testenv decides what a test does when a tool it needs is missing.
Package testenv decides what a test does when a tool it needs is missing.
Package mail sends email from a web app: a Mailer service apps inject, configured from the environment (or .env), with a log driver for development and tests and an SMTP driver for production.
Package mail sends email from a web app: a Mailer service apps inject, configured from the environment (or .env), with a log driver for development and tests and an SMTP driver for production.
mailtest
Package mailtest tests the mail an app sends: a quiet log-driver Mailer to provide, and helpers reading what it kept.
Package mailtest tests the mail an app sends: a quiet log-driver Mailer to provide, and helpers reading what it kept.
Package viewtest helps test pages: it gives pages that read template variables the values a test chooses instead of what the app's context processors (web.ContextProcessors) compute, and sets how a page's deferred parts render.
Package viewtest helps test pages: it gives pages that read template variables the values a test chooses instead of what the app's context processors (web.ContextProcessors) compute, and sets how a page's deferred parts render.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL