Files
kjol/go/cmd/examples/go-wasm-web/README.md

5.9 KiB

go-wasm-web — example app for the gowasm engine

A runnable example of kjol's gowasm engine: author UI components in pure Go, compiled to WebAssembly, with SSR + hydration, Next.js-style server components, layouts, the kjol/webui component kit, Tailwind CSS (compiled by kjol's own engine), and a flash-free, state-preserving hot reload. No custom markup, no JSX — just Go. The engine lives in top-level kjol packages (kjol/go/{vdom,wasmruntime,rsc,wasmdevserver,webui}); this directory is only the app that consumes them.

Run it

cd cmd/examples/go-wasm-web
go run ./server            # codegen + SSR + hot reload at http://localhost:8085

Open http://localhost:8085. / and /about use the light public layout; /chart, /server, /data (client-side fetching), and /kit (a UI-kit "kitchen-sink" demo of the webui components) use the dark app layout. Edit any .go file and the browser hot-swaps the new wasm without a full reload or a flash, preserving page state; a build failure shows the Go compiler output as an overlay.

Data fetching (/data) shows both directions of kjol/httputil: the server answers /api/quotes with httputil.RespondGob([]Quote) and the client decodes it straight back into []Quote with httputil.FetchGob (the same Go type on both ends — no JSON); and a user-entered GitHub repo (owner/name) is fetched with httputil.FetchJSON into a tagged Go struct. The client HTTP transport is wasmruntime.FetchBytes, installed by the runtime itself (override it with httputil.SetClientTransport for auth headers or a base URL). /data is a static route, and fetching only exists on the client, so the fetches no-op during SSR: the server pre-renders the page's spinner, and the client runs them for real after hydration.

Styling is Tailwind: the build runs kjol/cmd/twcss, which scans the Go markup + the webui kit for utility classes and compiles css/app.csswwwroot/app.css with kjol's native Tailwind v4 engine (kjol/tw). There is no Bootstrap and no hand-written CSS.

Saving a .css file recompiles only Tailwind and swaps the stylesheet into the live page — no wasm rebuild, no reload, no lost state. Saving a .go file does the full rebuild and hot-swaps the wasm.

Building

The build is Go, not a shell script — buildsteps/ holds the four steps (codegen → Tailwind → wasm → wasm_exec.js shim), and both the one-off build and the dev server's watch loop call the same functions, so they cannot drift apart.

go run ./build      # one-off: codegen + Tailwind + wasm + shim
go run ./server     # dev server: does the same build, then watches and hot-reloads

In VS Code these are the gowasm: build and gowasm: dev server (hot reload) tasks; both run through gowasm: prebuild (codegen + Tailwind), which is also the preLaunchTask of the debug configs — under the debugger the binary is built by Delve, so nothing else would generate app/*.gen.go.

This is a separate module

go.mod here declares its own module (gowasmweb) with replace kjol => ../../.., so the app's go-chart dependency (and freetype / x/image) stays out of kjol — the engine packages (vdom, wasmruntime, rsc, wasmdevserver) are stdlib-only. go build ./... at the kjol root does not descend into this nested module; build it from this directory.

Layout

app/                the application — neutral, standalone functions (no central struct)
  pages.go            Deps + Shell + App/Public layouts + nav + Counter + pages
  chart.go            Chart page (go-chart, renders on both sides)
  kit.go              /kit — UI-kit demo page showcasing kjol/webui components
  data.go             /data — client fetch: gob from /api/quotes + third-party JSON
  server_counter.go   //gowasm:server component (server-only; clicks-over-time chart)
  *.gen.go            GENERATED by kjol/cmd/wasmgen (routes, layout dispatch, stubs)
css/app.css         Tailwind entry (@import "tailwindcss" + @theme tokens)
wasm/               the js/wasm client entry point (main_native.go is a host stub)
server/             the dev-server main: injects Build/Render/Document into wasmdevserver
wwwroot/            wasmboot.js (+ generated app.css, wasm_exec.js, app.wasm)

How it maps onto the engine (top-level kjol packages)

Engine package Role This app's use
kjol/vdom neutral virtual DOM (native + wasm): VNode, builders, Signal, RenderHTML pages build *VNode; server SSRs with vdom.RenderHTML
kjol/wasmruntime wasm client runtime: reconcile, Run/Hydrate, router, fetch, HMR state wasm/main.go calls Hydrate/Run
kjol/rsc stateless server components over HTTP (gob) //gowasm:server + the generated client stub
kjol/wasmdevserver reusable dev server: SSR, /rsc, hot reload, error overlay server/main.go fills a wasmdevserver.Config
kjol/httputil gob/JSON responders + typed client fetch (RespondGob, FetchGob, FetchJSON) the /data page + the /api/quotes handler
kjol/cmd/wasmgen directive codegen → app/*.gen.go run by buildWasm and //go:generate

The golden rule holds: wasmdevserver imports no app code. The app injects Build (how to compile the wasm), Render (SSR a route → HTML), and Document (wrap it in a page) via wasmdevserver.Config — the same coupling inversion kjol uses elsewhere.

Directives (expanded by wasmgen at build time)

//gowasm:page / static layout=public   // a route; `static` SSRs it, `layout=` wraps it
func HomePage(d Deps) func() *VNode { ... }

//gowasm:layout public                 // chrome for pages that opt into layout=public
func PublicLayout(d Deps, content *VNode) *VNode { ... }

//gowasm:server                        // runs on the server; calling it looks identical
func ServerCounter() func() *VNode { count := NewSignal(0); ... }