The first component slice is implemented for SPA build output.
Implemented today:
- Explicit or discovered
.cmp.gwdkbuild inputs withcomponent Name. - Optional
props { name string }declarations, including scalar default literals such asprops { count int = 0 }. - Inline scalar props with
string,int,float, andbooltypes. - Component-local Go imports using normal module import paths, such as
import ui "github.com/acme/app/ui". - Typed props contracts that reference imported Go structs, such as
props ui.CounterProps. - Typed state contracts that reference imported Go structs and a no-argument
init function, such as
state ui.CounterState = ui.NewCounterState(). - Typed exports metadata blocks such as
exports { selectedID string }. - Typed emits metadata blocks such as
emits { select(id string) }. - One
view {}block per component. - Self-closing component calls such as
<Hero title="GOWDK" />. - Wrapper component calls such as
<Panel>...</Panel>with child content rendered into<slot />in the component view. - Named slots with caller-side
<template g:slot="name">...</template>and component-side<slot name="name">fallback</slot>. - Scalar scoped slots with component-side slot props and caller-side
let:bindings, for example<slot name="item" label={Title} />consumed by<template g:slot="item" let:label>...</template>. - Page-level cross-package component imports with
use ui "components"and qualified calls such as<ui.Hero />. - Component-scoped cross-package component imports with local aliases such as
use icons "icons"and<icons.Badge />inside the declaring component. - Escaped
{prop}text and attribute interpolation inside component views. - Component prop values can interpolate page build data, such as a route param
from literal
paths {}. - Slot children render in the caller scope, so page build data and route params used inside the child content are resolved before being inserted into the component slot. Scoped slot values are injected into that caller scope for the slot body.
- A component with
state ...renders initial state at build time and emits a generated JavaScript island by default when called withoutg:island. - Component-local
client { func Name(...) { ... } }handlers can group the current safe typed expression subset and be called fromg:on:*with scalar expressions, such asAdd(Count + 1). The olderfn Name(...)spelling remains accepted. - Components can dispatch declared events from
client {}handlers withemit name(Field). Parent components can listen on component calls withg:on:name={...}and receive typed event fields through the compiler-ownedeventscope. - Component-local
computed Name Type { return expr }values can derive read-only browser state from props, state, and other computed values. Computed values may also use one Go-styleifreturn followed by a fallback return. The compiler orders computed values by dependency and rejects cycles. - Pages can declare first-slice page-scoped stores with
store cart ui.CartState = ui.NewCartState(). Componentclient {}blocks can declare explicit dependencies withuse cart; the compiler validates store type/init contracts and rejects unknown store uses. Runtime shared-state subscriptions are still planned. - A component can declare
wasm ./browser/counterto compile a browser-side Go package and make normal calls to that component use the WASM island runtime by default. The package is built withGOOS=js GOARCH=wasm, must produce a real browser WASM module, and must not import server/process/network packages such asnet/http,os/exec,database/sql, rawsyscall,plugin, orunsafe. - A component call can explicitly request the WASM island artifact with
<Counter g:island="wasm" />when a call-site override is needed. If the component has nowasmpackage, GOWDK still emits the first-slice placeholder module plus loader shape for that explicit call. - Duplicate component names are rejected within the same GOWDK package during
compiler validation. Components in different packages may share a name and
must be referenced through the calling package's
usealias. - Redundant component implementations are rejected during compiler validation
with
redundant_component_implementation, even when their names differ. - Component
cssdeclarations are parsed, lowered into compiler metadata, emitted as scoped component CSS, linked from generated pages, content-hashed, manifest-mapped, and served with immutable cache headers. Componentassetdeclarations are emitted as content-hashed files underassets/gowdk/components/<package>/<component>/, manifest-mapped, and served with immutable cache headers. - Component
js "./file.js"andjs "./file.ts"declarations emit scoped browser module files and link them only from pages that call that component. Inlinejs {}blocks are supported for small cases, but path-based modules are preferred. GOWDK transforms TypeScript without type-checking and does not yet bundle or follow JavaScript imports. - Component files are compiler inputs, not Go imports. A page can call a
same-package component by name, such as
<Hero />, when that component file is part of the same build/module input set. Cross-package page calls must use a GOWDK source import and qualified component tag:
package pages
use ui "components"
view {
<main><ui.Hero title="GOWDK" /></main>
}
The quoted use target is a discovered .gwdk package name, not a Go import
path. Imported components can call sibling components in their own package by
bare name inside the component body. Components can also declare their own
scoped use aliases for qualified child component calls:
package marketing
use icons "icons"
component Hero
view {
<section><icons.Badge /></section>
}
Default slot:
component Card
view {
<section><slot /></section>
}
Named slot:
component Panel
view {
<section>
<header><slot name="actions"><span>No actions</span></slot></header>
<slot />
</section>
}
Scoped slot:
component Row
props {
label string
count int = 0
active bool = false
}
view {
<slot name="item" value={label} />
}
view {
<Row label="Ada">
<template g:slot="item" let:value>
<strong>{value}</strong>
</template>
</Row>
}
Typed emits:
component Option
props {
ID string
}
emits {
select(id string)
}
client {
fn Pick() {
emit select(ID)
}
}
Bindable child state is not a stable public contract. Use typed emits plus parent-owned state instead:
view {
<Option g:on:select={SelectedID = event.id} />
}
Typed exports declare local component values that a parent can observe through
the generated exports event:
exports {
SelectedID string
}
view {
<button g:on:click={SelectedID = "first"}>{SelectedID}</button>
}
Parent components can listen with g:on:exports:
view {
<Picker g:on:exports={CurrentID = event.SelectedID} />
}
Stores are explicit page-scoped UI state:
route "/cart"
guard public
store cart ui.CartState = ui.NewCartState()
component CartButton
client {
use cart
}
WASM islands are declared on the component:
component Counter
wasm ./browser/counter
view {
<button>Count</button>
}
See examples/components/wasm/ for a runnable component-level WASM island ABI
example with the required browser Go exports.
view {
<Counter />
}
Component files are GOWDK compiler inputs. They are not imported by Go code and
must not import generated app output. Go import declarations inside .gwdk
files import normal Go packages for typed contracts and build-time helpers.
GOWDK use declarations import discovered .gwdk source packages; today that
contract is implemented for qualified component calls.
Props are caller-provided inputs. Inline props {} declarations support scalar
string, int, float, and bool types. Parent calls pass quoted string
props, scalar literal expressions for numbers and booleans, or expression
values from the implemented build-data subset. Props are read-only to
client {} code; mutable browser state belongs in state or in an explicit
page store.
Imported Go structs are the stable typed prop path for richer contracts.
Inline props can declare static scalar defaults with name type = literal.
Defaults are used when a caller omits the prop and are overridden by explicit
caller values.
Advanced prop forwarding stays inside the typed compiler contract:
{...props}may be used inside a component that declares props. It forwards only same-named props that the child component also declares; it does not expose an arbitrary prop bag or global lookup.target:sourcemaps a differently named caller prop into a declared child prop. Without a value,target:sourceforwards{source}. With a value, such astarget:source={Expr}ortarget:source="literal", the value is used fortargetwhilesourcenames the caller-side source for diagnostics.- Explicit props, spreads, and renames cannot provide the same target prop more than once. Unknown target props and unsupported spread sources fail before output is written.
State is component-local UI state. A state Type = Init() declaration runs the
no-argument Go init function at build time for SPA/static output and serializes
the JSON-compatible initial value into the component island. State is visible to
the browser and must not carry secrets, trusted authorization state, database
state, or server validation results that the server still needs to enforce.
Bindable child state is supported on component calls with
g:bind:<ExportedState>={ParentState}. The target must be a child state field,
must be declared in exports, and must have a scalar type compatible with the
parent state field. Generated JavaScript sends the parent value down through
reactive props and listens for the child's typed exports event to write the
new child value back to parent state. A single component call may bind several
exported fields at once; the generated assignments share the one exports
listener and run as ordered statements. A component may not export a field named
active, which the exports payload reserves for the mount-state flag. Bound
state is still local UI state: it is not trusted input, server state, auth state,
validation, business logic, route truth, or cache policy.
Computed values are read-only derived state. They can depend on props, state, and other computed values. The compiler builds a dependency graph for declared computed values, emits them in dependency order, and rejects cycles before generated JavaScript is written.
Stores are page-scoped UI state. A page store declaration names the store,
the Go type, and the build-time init function. A component client { use name }
declares that the island may use that store. Generated browser store state is a
page-local enhancement contract; it is not global application authority and it
does not replace server-side routing, auth, validation, persistence, or action
behavior.
Store use is explicit. Same-page stores use client { use cart }; stores from
another discovered .gwdk package require a GOWDK use alias and a qualified
client store reference such as client { use stores.cart }. Cross-package
stores are validated by alias and store name, not discovered globally.
App-global stores are deferred.
A use can carry the store's Go type so the component can reference the store's
fields directly, without redeclaring a matching state shape:
component CartBadge
client {
use cart ui.CartState
computed Label string { return string(Count) }
}
view {
<span>{Label}</span>
}
use cart ui.CartState binds CartState's fields (here Count) into the
component's client scope. The type is resolved against the component's own
imports, so a reusable component stays self-describing even when different pages
declare a same-named store; the annotated type is the component's contract for
the store's shape, exactly as a local state declaration was. The island seeds
those fields with the type's zero values for SSR and adopts the store's real
(init or persisted) value on mount; keep a local state <Type> = <init> if you
need the store's init value reflected in the server-rendered HTML.
A page store can opt into browser persistence with a persist modifier:
store cart ui.CartState = ui.NewCartState() persist "local"
store prefs ui.UIPrefs = ui.DefaultPrefs() persist "session"
persist "local" keeps the store in localStorage (survives a browser
restart); persist "session" keeps it in sessionStorage (survives reload and
SPA navigation, cleared when the tab closes). Persistence is keyed by store name
(gowdk:store:<name>), so the same persisted store keeps its value across routes
on the origin. Only the store's own declared fields are persisted — never
component state, props, or computed values. The compiler embeds a hash of the
store's struct shape; when the shape changes, stale persisted state is discarded
rather than restored, so a struct change never crashes on old data. Because
browser storage is readable by any script on the origin, persisting a field whose
name resembles a secret (token, password, secret, auth, …) is a warning —
including a nested field such as Profile.Token, because persistence writes the
whole value of each top-level field: keep credentials and trusted authorization
state server-side. An unknown scope is rejected — see
gowdk explain page_store_persist_scope_invalid.
persist "local" stores also sync across tabs: when one tab writes, other tabs
on the origin mirror the value through the browser storage event. persist "session" stores are deliberately tab-local — sessionStorage is partitioned
per top-level tab, so session-scoped stores do not (and cannot) sync across tabs.
To drop a persisted store (for example after checkout or logout), use the
bounded clear <store> statement inside a client function, mount, destroy, or
effect block:
client {
use cart
func Checkout() {
clear cart
}
}
clear cart lowers to window.__gowdkStores.clear("cart"), which removes the
stored copy and resets the store to its build-time init value, notifying every
island that uses it. A component may only clear a store it uses; clearing an
unused store is a compile error. If two pages persist a store with the
same name but different shapes, they share one storage key and discard each
other's data on navigation; the compiler warns with
page_store_persist_key_conflict. If they share the same shape but declare
different local/session scopes, the runtime keeps whichever scope initialized
first and the compiler warns with page_store_persist_scope_conflict. A store
first reached on a route that does not persist it still adopts persistence when a
later route declares it, restoring the saved value regardless of navigation order.
Persistence survives SPA navigation: when the client runtime swaps page content it re-scans store seeds, so a store first declared on a later client-side route hydrates without a full page load, and a store already in memory keeps its value.
WASM islands participate in page stores too. The host loader merges every used
store's current (and persisted) value into the mount/handle/destroy payload's
state, writes back any store values an export returns in the extended
{ patches, stores } result shape, and re-invokes the island when another island
changes a used store. Surfacing serialized state from the Go uint32 export
contract — so a Go island can return that stores map — is the remaining Go-side
ABI work; see examples/components/wasm/README.md.
Current limits. Invalid persist scopes are reported but not auto-fixed, because
choosing local vs session is a deliberate decision.
Exports must reference a declared prop, state field, or computed value and the
declared type must match that local symbol. Generated JavaScript islands emit
an exports event with event.active == true after mount and updates, plus a
gowdk:exports DOM event for direct integrations. Before unmount, the runtime
emits the same events with event.active == false and exported values set to
null, so parent code can clear local handles. Exports are local UI handles;
they are not server state, trusted input, or a replacement for backend actions.
Slots are the reusable-markup primitive. A default slot uses <slot />, named
slots use <slot name="name">, and scoped slots pass scalar values through
slot props plus caller-side let: bindings. GOWDK does not currently have a
separate snippet/render value model.
Recursive component rendering is rejected to prevent unbounded build-time
rendering; direct and transitive cycles fail before output is written. Dynamic
component selection is rejected; component calls must name a known component
directly or through an explicit use alias.
client {} is a compiler-owned UI language, not arbitrary JavaScript. The
supported handlers, helpers, lifecycle blocks, effects, refs, list built-ins,
bindings, conditionals, computed values, and scalar expressions are defined in
syntax.md. Generated island JavaScript interprets that bounded
subset instead of evaluating arbitrary user JavaScript source.
Client handlers run in source order. The generated runtime batches state
updates, recomputes computed values in dependency order, runs cleanup before
effects rerun or unload, and then updates DOM bindings. Async client handlers
are limited to compiler-owned async helpers such as validated
await fetchJSON[T](urlExpr) assignments; they cannot return values and do not
change the ownership boundary.
Generated browser runtime behavior is scoped to the island or page enhancement that requested it. JavaScript may update text, attributes, classes, styles, form bindings, list rows, local state, page stores, partial responses, and SPA link transitions. It must not own route existence, auth, business rules, database access, trusted server validation, action behavior, or cache/loading policy. Real routes, direct browser refresh, and server behavior stay owned by the compiler manifest, generated Go, and user Go handlers.
Production WASM islands are declared with component-level wasm. Normal calls
to that component use the WASM island runtime by default. The referenced package
is browser-side Go compiled for GOOS=js GOARCH=wasm with server and process
packages rejected. GOWDK validates the required component-scoped ABI entrypoints,
ships Go's browser wasm_exec.js runtime asset for declared Go WASM packages,
passes gowdk-wasm-island-v1 payloads to component WASM exports, and keeps DOM
mutation in the generated host loader. WASM islands are not a replacement for
backend handlers.
The runnable ABI example in examples/components/wasm/ builds a component
WASM asset, per-component host loader, and wasm_exec.js from a browser Go
package with the required GOWDKMount<Component>,
GOWDKHandle<Component>, and GOWDKDestroy<Component> exports.
Not implemented yet:
- Supported recursive component rendering and supported dynamic component selection.
- Full runtime validation for user browser logic in WASM islands beyond required export, browser import, and patch-operation checks.
- Wiring generated Go component packages into the generated app layout.
- Cross-package store and asset use syntax.
- Emitting and rewriting component-scoped CSS and component-level assets from
the existing component
cssandassetmetadata.
Component design rules:
- Components must stay portable and must not derive route identity from folder location.
- Component resolution should be explicit enough for diagnostics, editor navigation, and generated code.
- Generated component output must escape untrusted text by default.
- Public generated-runtime contracts belong under
runtime/.