MoonBit port of the tutuca UI framework (value language, templates, vdom, components, app runtime, lint, CLI)
Dependencies
| Layer | Package(s) | What it does |
|---|---|---|
| Value language | core/ — marianoguerra/tutuca/core (value_*.mbt, path_*.mbt) | The value model and its evaluation, plus the reactive path/dispatch system (COW spine rebuild, handler dispatch, change sets). core never PARSES anything — one package by necessity, since value_* and path_* form a dependency cycle. |
| Expression language | tscript/ (+ check/, emit_mbt/, conformance/) | Reading the surface tutuca writes: the slot expressions in a view (.field, $method, @value) and the block language of <script type="tutuca/script">. Tokenizer, parser, checker, and a MoonBit emitter for the ahead-of-time path. There was an interpreter here too, for the card runtime; tutucard/wasm compiles a card now, so a card is mounted by instantiating a module rather than by running one where it stands. Above core rather than inside it, for the reason in the cell above. |
| Templates | anode/ (+ anode/sanitize) | Parses the HTML-ish view syntax into an AST: attributes, directives, x- ops, macros, whitespace handling, optimization. sanitize is the WHATWG Sanitizer API config model, applied statically to a view's literal names. |
| Virtual DOM | vdom/ (+ vdom/memdom, vdom/browser, vdom/wasm) | Builds and incrementally morphs a VDOM against any DOM implementing the DomNode trait. |
| Render-time filters | vdom/filter/ (+ url/, handler/, markup/, markdown/), markdown/, sinks/ | The half a static pass cannot decide: an attribute VALUE is only known once state has produced it. URL schemes, on* handlers, sanitized raw markup, and Markdown rendered straight into vdom nodes. markdown/ is a CommonMark+GFM parser vendored from mizchi/markdown.mbt — see markdown/UPSTREAM.md. sinks/ holds one four-bit type and imports nothing: which of these rules an element's attribute NAMES could concern, which render decides off the tree so the rules can skip what cannot concern them. |
| Render | render/ | Turns a parsed view + a value stack into a @vdom.Vdom tree (loops, scopes, event-path metas, teleport). |
| Components / App | component/, app/ (+ app/browser, app/wasm), transactor/ | Typed-state component definitions (a plain struct + one Dispatch update match), the app runtime, and the transactor that routes events at the root and settles state. |
| Styling | css/ | The one place stylesheets live: a Tailwind port plus embedded Tailwind and margaui bundles, so a host compiles its collected class names to CSS with no Node, no CDN and no checkout. |
| Tooling | lint/, inspector/, statedef/, viewfile/, viewgen/, cli/ | The linter (parse-issue rules + a WHATWG-tokenizer structural HTML linter), a schema inspector, the state schema language, the view-file splitter, the ahead-of-time view compiler, and the native tutuca CLI. |
| Testing | testing/harness | A reusable harness to mount and drive a ModuleDef on the in-memory DOM. |
| Universal core | dyncomp/ — wit/ (the tutuca:component contract), host/ (+ host/wasm), policy/, registry/, jsonschema/, persist/ (+ persist/wasm) | Loading a WebAssembly component from anywhere into a running app: the contract it implements, the host that wraps it as an ordinary &Obj, what a bundle from a stranger is allowed to do, the searchable catalog of everything loaded, and the schema ⇄ JSON Schema projection both a form and an agent's tool read. See dyncomp/DESIGN.md. |
| Universal UI | dyncomp/ui/ (+ dyncomp/ui/std, dyncomp/ui/wasm) | The page a person arranges out of that catalog. The component tree IS the layout — Universal, Stack, Grid, Tabs are ordinary tutuca components, and a loaded guest is decorated exactly like a standard one. The editor is backend-agnostic, so moon test drives the whole thing on the in-memory DOM; ui/wasm is the half that cannot be — the bundle bridge, the session store, and mount(). See docs/dynamic-components.md. |
| Demos & docs | demo/, playground/, storybook/, tutucard/, guests/ | 52 ported examples (storybook/examples/), browser/wasm demo hosts, an in-browser playground, the compiler-free card playground, a compiled storybook gallery, and nine sample tutuca:component guests (eight MoonBit, one Rust — the list is guests/guests.mjs). |
moon run --target native cmd/main -- gen-views demo/counterlib/counter.html --name Counter
# -> demo/counterlib/counter_view_gen.mbt (the view vocabulary as types)
# -> demo/counterlib/counter_view_ir_gen.mbt (the compiled views + the wrapper)
# both checked in; regenerate, never edit
<script type="tutuca/state">
state Counter { label: String, count: Int, history: Array[Int] }
receive Counter { ResetTo(Int) }
</script>update=(s : CounterState, msg, _ctx) => match CounterMsg::from_dispatch(msg) {
Some(Add(d)) => ... // `d` is a Double: `@on.click="add 1"`
Some(SetLabel(l)) => ... // `l` is a String: `@on.input="setLabel value"`
Some(ResetCount) => Unhandled
Some(Unknown(_, _)) | None => Unhandled
}| id | |
|---|---|
| (none) | the single unnamed component's main view |
| row | …its row view |
| Counter:main | the Counter component's main view |
| Counter | shorthand for Counter:main |
| macro:icon | a macro shared by every component in the file |
<template id="macro:icon" data-size="'24'" data-color="'currentColor'">
<svg :width="^size" :height="^size" :stroke="^color"><path :d="^path"></path></svg>
</template>
<template id="Gallery">
<x:icon :size=".size" :path=".heart"></x:icon>
</template>counter_component(
init=CounterState::fresh(),
update=(s, msg, _ctx) => ...,
)@anode.View::from_ir(
"main",
@anode.h("div", [@anode.attr("class", "stat")], [
@anode.h("button", [@anode.attr("class", "btn"), @anode.eid(0)], [
@anode.text("+"),
]),
@anode.dyn_text(Field("count")),
]),
[[@anode.on("click", Method("inc"))]],
)moon run --target native cmd/dev -- gen-views # generate + fmt
git diff --exit-code # drift checktutuca watch demo/counterlib # or a file, or bare for the whole project| Tab | |
|---|---|
| Component | the MoonBit you write |
| View | the .html its views live in (name the component with <!-- name: Counter -->) |
| Generated | read-only: what gen-views makes of the View tab, updating as you type |
///|
test "render a tree into memdom" {
let doc = @memdom.document()
let container = @vdom.DomNode::create_element(doc, "DIV", None, None)
let opts = @vdom.RenderOpts::new(doc)
let prev = @vdom.render(
@vdom.h("ul", attrs={ "className": Str("list") }, childs=[
@vdom.h("li", key="a", childs=[@vdom.text("one")]),
@vdom.h("li", key="b", childs=[@vdom.text("two")]),
]),
container,
opts,
)
// incremental re-render: morphs in place, preserves keyed nodes
let _ = @vdom.render(
@vdom.h("ul", attrs={ "className": Str("list") }, childs=[
@vdom.h("li", key="b", childs=[@vdom.text("two")]),
@vdom.h("li", key="a", childs=[@vdom.text("one!")]),
]),
container,
opts,
prev~,
)
inspect(
container.to_html(),
content=(
#|<div><ul class="list"><li>two</li><li>one!</li></ul></div>
),
)
}///|
let opts = @browser.window_opts()
///|
let container = @browser.BrowserNode::from_element(
@dom.window().document().getElementById("app").unwrap(),
)
///|
let prev = @vdom.render(view(state), container, opts)moon run --target native cmd/dev -- setup # npm install (happy-dom) + enable git hooks
moon run --target native cmd/dev -- check # moon check across wasm-gc, js, native
moon run --target native cmd/dev -- test # moon test across the three targets
moon run --target native cmd/dev -- build # moon build wasm-gc + native CLI + js
moon run --target native cmd/dev -- dist # assemble a self-contained dist/fn postcondition_failed(handler~ : String, pred~ : String, sentence? : String, state? : Value) -> Unitfn precondition_failed(handler~ : String, pred~ : String, sentence? : String, state? : Value) -> Unitfn refusing() -> BoolMoonBit port of the tutuca UI framework (value language, templates, vdom, components, app runtime, lint, CLI)
Dependencies