The vuka Command
Introduction
The vuka command creates apps, grows them a piece at a time, and builds, runs, tests and vets them through the go command. It also formats .vuka files, explains what the compiler makes of them, and serves your editor.
Command Overview
These are the subcommands and their flags:
vuka new <dir> [module path] [--ui=… --db=… --mail=… --auth=… --frontend=… --name=… --yes --force]
vuka add module|service|component|island|page|entity … [--dry-run] [--force]
vuka build|run|test|vet|install [go flags] [packages]
vuka run -watch [-debug] [go flags] [package] [args]
vuka mod tidy|why|vendor|graph|… [args]
vuka build-islands [-watch]
vuka gen [-check] [-o build]
vuka gen -inplace [-check] [dir | dir/...]
vuka eject [-rm] [file.vuka… | dir | dir/...]
vuka explain [-full] file.vuka
vuka explain E0201
vuka fmt [-l] [-w] [-d] [-check] [-rewrite] [paths…]
vuka lsp [-gopls path] [-log file] [-shared=false]
vuka version
vuka upgrade [-n] [-no-test] [version]
vuka release [-yes] [-no-test] [-security] vX.Y.Z (maintainers)Creating an App
vuka new starts an app you can grow: pages, sign-in, a database with migrations, mail, styles and tests, wired together and working before you write a line. Getting started walks through what it makes.
You can name the module path and answer questions with flags:
vuka new shop # asks five questions
vuka new shop example.com/shop # a module path other than the directory's name
vuka new shop --db=postgres --frontend=react --yesAnswering the Questions
It asks, with the arrow keys (or the item's number) and Enter:
| Question | Choices (first is the default) | Flag |
|---|---|---|
| UI | Live pages + Tailwind · templ views + Tailwind · API only | --ui=live|templ|api |
| Database | SQLite · PostgreSQL · MySQL · none | --db=sqlite|postgres|mysql|none |
| logged in development, SMTP in production · none | --mail=yes|no | |
| Authentication | yes, with a login page · no | --auth=yes|no |
| Frontend app | none · React · Vue · Svelte | --frontend=none|react|vue|svelte |
A flag answers its question; --yes (-y) takes the default for every question no flag answers. Without a terminal — a pipe, CI — nothing is asked: the flags and the defaults are used, and the command never waits for input. --name is the app's name as people read it (APP_NAME; the directory's name, capitalised, by default).
What It Sets Up
- Sign-in needs a database. It keeps its users, sessions and tokens there, so sign-ins survive restarts and replicas share them. With
--db=nonethe question isn't asked and the app has none;--db=none --auth=yesis refused, saying why. - The first admin is created when the app first starts, from
ADMIN_EMAILandADMIN_PASSWORDin.env.vuka newfills in a strong random password and prints it once..envis in.gitignore;.env.example, committed, has placeholders. The app never asks for a password at start-up. - SQLite needs no setup: the file is made under
data/on the first run, pure Go, no CGO. PostgreSQL and MySQL get aDATABASE_URLto fill in and their driver's import; the generated tests run on SQLite either way. - A frontend is a Vite project in
frontend/, served at/app(web.FrontendAt), withAPP_NAMEpassed to it (web.FrontendEnv).vuka runstarts its dev server,vuka buildbuilds it into the binary. - A directory that isn't empty is refused;
--forcewrites the app beside what is there, keeping every file it finds (and saying which).
After writing the files it runs vuka mod tidy, then prints the next steps. vuka test ./... passes in the new app as it is.
Adding to an App
Adding a Module
vuka add module <name> grows an app vuka new made, a module at a time:
vuka add module invoicesThis writes modules/invoices/ — an Invoice model, an Invoices service, a page at /invoices linked in the navigation (or /api/invoices routes in an API app), a test of the service — and migrations/000N_invoices.go making its table, after the latest migration. It adds the module's import to main.vuka, which is all a module needs to be part of the app: its routes, pages and services declare themselves.
The app applies the migration when it next starts. The name is a plural noun in lower case; an existing module, or an app without a database, is refused.
It is add, not a form of new: new makes a project, add grows the one you are in, and a project directory named module stays possible.
Adding Services, Components, Islands, Pages and Entities
The other kinds add one piece each:
vuka add service Mailer [--module notes] | a service in services/ (or modules/notes/; notes/Mailer is the same) with its constructor NewMailer and a test, registered in main.vuka's services with app.Service(notes.NewMailer): pages, routes and hooks take a *notes.Mailer parameter |
vuka add component StatCard [--module m] | a stateless component, in components/ or the module: the struct of its props in stat_card.vuka, its markup in stat_card.vuka.html |
vuka add island Chart [--module m] [--vue|--react|--svelte] [--dir d] | web/islands/Chart.vue (.tsx, .svelte; web/islands/<m>/ with --module), its props typed for an import, and components/chart.vuka with chart.vuka.html (or in modules/<m>/) — a Chart component placing it, so pages write <components.Chart title="Sales" count={3} />; the framework's packages and Vite plugin are added when missing and installed, showing it is under way (--no-install prints the command instead) |
vuka add page /reports | a one-struct .vuka.html page under pages/; use [id] names for parameters and _layout.vuka.html for shared layout. When the app's main doesn't pass pages.Routes() yet (an app with templ views, an API, or none of pages/ so far), it adds the import and the option to web.New — see serving the pages |
vuka add page /board --island Board | a page shape under pages/ that imports its frontend component; layouts use _layout.vuka.html, hooks use decorators |
vuka add entity Comment --module posts body:text post:ref email:string:unique | an orm model in entities/ (or the module; posts/Comment is the same); --unmanaged --table t maps an existing table — no timestamps, test or migration, and no migrations/ needed. Otherwise it comes with ID, CreatedAt and UpdatedAt, a test, and its migration written by go tool orm makemigrations |
Entity Field Types
An entity's fields are name:type, ? after the type for an optional one (an Option), and :unique or :index after it:
| type | Go | column |
|---|---|---|
string | string | VARCHAR(255) (@orm.Char{Max: 255}) |
text | string | unbounded text (@orm.LongText) |
int, bigint | int64 | integer |
float | float64 | floating point |
decimal | float64 | NUMERIC(12,2) |
bool | bool | boolean, default false |
time, date | time.Time | a timestamp, a date |
json | orm.JSON[map[string]any] | JSON |
ref | orm.FK[T] | <name>_id, a foreign key to the model T the name names (user:ref or user_id:ref → User, in the module or another one); optional: ON DELETE SET NULL |
--table names the table (the plural of the name in snake_case otherwise).
Serving the Pages
A page under pages/ is served once the app's main passes pages.Routes() to web.New. An app made with live pages does from the start; for any other, the first vuka add page (or vuka add module writing a page) adds it:
import (
"github.com/vuka-lang/web/v2"
+
+ "example.com/shop/pages"
)
…
- web.New(web.Dev()).Run("")
+ web.New(pages.Routes(), web.Dev()).Run("")It edits the main that owns pages/ — the module root's, else the module's only package main — in main.go or main.vuka, keeping its comments and layout. When web.New spreads a list of options (web.New(options()...)), pages.Routes() goes at the end of that list. A main that passes it already is left as it is. When the module has several mains and none passes it, or web.New can't be found, nothing is edited: the command prints the line to add and the file it goes in. vuka fix routes makes the same change in an existing app, the fix for W0701.
Refusals and Dry Runs
Every kind refuses a name that isn't a Go identifier of the right case, a declaration or file that exists (--force overwrites it), and a module that doesn't; --dry-run lists what it would write and changes nothing. Files are formatted as vuka fmt formats them, and a package nothing imports yet is imported in main.vuka.
Building, Running and Testing
vuka build, run, test, vet and install are the go command's, with every .vuka file in the module transpiled into an overlay (go … -overlay): nothing is written among your sources. vuka build also refreshes build/. Flags and packages are passed to go as they are.
Incremental Transpiling
Only what changed is transpiled again. The Go each package became is kept in the user's cache directory (vuka/transpile), keyed by the package's sources, the API of the module's packages it imports, the go command, the build environment, go.mod/go.sum/go.work and this vuka: a build after a build transpiles the packages whose inputs changed, and a change to a function's body leaves its importers alone.
The island build is skipped the same way when nothing in the frontend root (its sources, config, lockfile, installed packages, @source paths) changed. VUKA_CACHE=off turns both off.
Under vuka run the files web.Frontend serves are left out of the binary (empty in the overlay; it reads them from disk in development), so they cost the linker nothing; -embed (with -watch) or VUKA_EMBED_STUB=off builds them in.
Vetting
vuka vet checks shape files, decorated hooks, field bindings, deferred loaders and typed handler arguments as well as the generated Go. Missing loaders, unfillable parameters and invalid markup are reported at source locations. It runs vukavet's checks too, as go vet's -vettool: the app's go tool vukavet when its go.mod declares it, else a vukavet beside vuka or on PATH, else one it builds at its own version (which); it says so when it can't. -vettool=… overrides it, and VUKA_VUKAVET=off runs go vet's own analyzers only.
Ejecting to Go
vuka eject turns .vuka files into Go:
vuka eject # the module: gen -inplace
vuka eject pages/orders.vuka
vuka eject -rm ./... # the Go becomes the sourceIt writes the Go generated for .vuka files beside them, as gen -inplace does: for the files named, or the packages (dir, dir/...; the module by default). With -rm the Go becomes the source: each .vuka file is deleted and its Go kept as orders.go (orders_vuka.go when orders.go is taken), without the generated-code header or line directives.
What's left is a plain Go module: go build, go vet and go test take it with no vuka command, and it behaves as the .vuka version did. Eject the whole module (./...) so the link files and the files embedding CSS and assets are written too; they stay generated files.
Rendering remains authored in shape files. Page registrations are generated from pages/, while services are explicitly registered on the app. Use vuka explain to inspect lowering rather than copying generated rendering or registration calls into application source.
Explaining Code and Diagnostics
vuka explain shows what Vuka makes of a file: each rewritten line beside the Go it becomes, then the code added after the source (decorator wrappers, statics), each labelled with the line it comes from. -full prints the whole generated file.
Given a diagnostic's code, it prints that code's page of the catalogue: what is wrong, why, and an example before and after the fix. A build error names the code to look up:
$ vuka build
error[E0203]: an attribute must start its own line
--> pages/users.vuka:12:18
|
12 | func helper() {} @doc("Lists the users")
| ^^^^
|
= help: put the attribute on its own line, above the declaration
For more information about this error, try `vuka explain E0203`.
$ vuka explain E0203Diagnostic Codes
Every diagnostic of the compiler, vuka vet, the editor and the libraries' checks at boot has a code: E for an error, W for a warning. On a terminal the frame is coloured (NO_COLOR turns it off).
vuka build, vet, test, run and install take -diag=json to print the diagnostics as a JSON array instead, each with its code, severity, message, span (file relative to the current directory, line, column), related spans, help, suggestion and the URL of its page.
Formatting Files
vuka fmt formats .vuka and .vuka.html files, as gofmt formats Go: it prints the formatted file, or with -w writes it back, -l lists the files whose formatting differs, and -d shows the diff. -check lists them and fails when there are any, writing nothing — the flag for CI. -rewrite writes a shape in the Go-header form in the <script lang="go"> form before formatting it.
Paths are files or directories (all .vuka and .vuka.html files below, skipping hidden directories, node_modules, vendor and build/ modules vuka made); no path means the current directory. A file that doesn't parse is reported and left alone.
Formatting Rules
- Go comes out exactly as gofmt would print it, alignment included. Vuka's own syntax is spaced the same way:
@attr(…)arguments and static fields' types and values are gofmt-formatted, a static'sstatic namelines up like a field name,matchand its cases are indented like aswitch, and an attribute always starts its own line, with the declaration below it. - Shape markup is laid out the way Prettier lays out markup. An element that fits in 80 columns (a tab counts 4) stays on one line, unless its children were written across lines and hold an element; otherwise its children go one per line, a tab deeper than the line the element starts on. Attributes that don't fit go one per line, with
>or/>on its own line under the<.<br/>becomes<br />;<p></p>stays as it is.{for},{if}and{match}blocks follow the same rule, their bodies a tab deeper and}}under the{; a match always takes several lines, its cases under the{. Go expressions in{…}are gofmt-formatted ({a+b}becomes{a + b}); one holding a comment is left as written. - Text never changes what it renders. Spaces that render (
<b>a</b> <i>b</i>,<p> x </p>) are kept, so elements they join stay on one line; lines of text keep their breaks and are re-indented; entities are left as written. Each formatted file is checked against the original, and an internal error is reported rather than a page that would render differently.
This shows a file before and after formatting:
// before
return <section className="pets" id="main" data-count={len(pets)} title="All our pets">
<h1>Pets ({len(pets)})</h1>
<table>{for _,p:=range pets {<PetRow pet={p}/>}}</table>
{if admin { <button disabled={len(pets)==0} >Add</button> } else { <i>read only</i> }}
</section>
// after
return <section
className="pets"
id="main"
data-count={len(pets)}
title="All our pets"
>
<h1>Pets ({len(pets)})</h1>
<table>{for _, p := range pets { <PetRow pet={p} /> }}</table>
{if admin {
<button disabled={len(pets) == 0}>Add</button>
} else {
<i>read only</i>
}}
</section>Formatting Shape Files
A .vuka.html file is formatted whole, in the form it is in:
- The Go in its
<script lang="go">(or its Go header) is formatted as a.vukafile, unindented. - The markup is indented two spaces a level, by its elements and by the blocks of its
if,forandswitchstatements. Statements and the Go in holes are gofmt-formatted ({a+b}becomes{a + b}). An opening tag that doesn't fit in the width goes one attribute a line, with>or/>on its own line under the<. Void elements and childless components close themselves:<br>becomes<br />,<ui.Badge></ui.Badge>becomes<ui.Badge />. Runs of blank lines become one. - Text stays on its lines. Only whitespace that doesn't render changes: line breaks are re-indented, and what
<pre>,<textarea>, a<script>and an inline<style>hold is left as written. Each file is checked against the original, and an internal error is reported rather than a page that would render differently. - A component's
<style>, after the markup, has its CSS formatted: one selector and one declaration a line, blocks indented two spaces. - Tailwind class order, when turned on, sorts each static
class="…"list the way Tailwind orders its rules: classes it doesn't know first, as written, then utilities by the property they set, then those under a variant (hover:,md:). Aclass={…}hole is never touched.
The project's vuka.toml, next to its go.mod, sets the width and the Tailwind order in its [fmt] table. Both are optional:
[fmt]
width = 100 # the column a tag's attributes wrap at
tailwind = true # sort class lists in Tailwind's order (off by default)The editor's format-on-save and range formatting use the same rules and the same file.
NOTE
Tailwind ordering is off by default: it reorders every class list of a project the first time it runs, which is a change worth making on purpose.
Converting Shapes
vuka fix shape-script rewrites every .vuka.html file of the module in the Go-header form in the <script lang="go"> form, leaving its Go and markup as written; a file it can't read as a shape is left alone and reported by the compiler. vuka fmt -rewrite -w does the same and formats the files.
Processor Variables
vuka fix ctx-vars rewrites, in the module's .vuka and .vuka.html files, the reads vuka vet reports as W0402: auth.Can(ctx, "x") and auth.Current(ctx).Can("x") become perms.Has("x"), auth.Current(ctx) becomes user, and web.Request(ctx).URL.Path, .URL.Query() and .PathValue("id") become request.Path, request.Query and request.Params["id"]. Imports of web and auth left unused go. A read whose variable main doesn't list, or whose name a local takes, is left alone.
The Language Server
vuka lsp is the language server. See Editors.
Checking Versions
vuka version prints the command's version and the set of vuka, ui and web it expects (from v0.50.0 one version for all four; see Versions and compatibility). Inside an app it also lists the versions the app's go.mod requires, with any replace, and warns when they aren't one set or aren't this command's set.
Upgrading an App
vuka upgrade moves an app to a set: the latest when no version is named (fetched with GOPROXY=direct, so a fresh tag counts). It go gets vuka, ui and web at that version — and, to this command's own set, the orm at the version vuka new pins, when the app requires an older one — (never go mod tidy), then runs vuka vet ./... and vuka test ./... (-no-test skips the tests), and prints a summary.
-n (or -dry-run) prints the plan and changes nothing. go.mod and go.sum as they were are kept in .vuka/upgrade/; a failed go get puts them back.
Releasing
For maintainers: vuka release releases vuka, ui, web, cmd/vuka and cmd/vukavet under one version. A dry run unless -yes. See Releasing.