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; tgc/emit 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, resumed paths). |
| 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/, storybook/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. |
| Component format | tgc/ — abi/ (the frozen preamble), rt/ (the runtime module), emit/ (the card compiler), host/, policy/, persist/ | Loading a WebAssembly module from anywhere into a running app. Core wasm plus the GC proposal and nothing else: one file that carries its own manifest, and an instance a component can hold in its own state. See tgc/SPEC.md and tgc/SECURITY.md. |
| Demos & docs | demo/, playground/, storybook/, tutucard/ | The ported examples (storybook/examples/), browser/wasm demo hosts, an in-browser playground, the compiler-free card playground, and a compiled storybook gallery. |
moon run --target native cmd/tutuca -- gen 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/spec">
state Counter { label: String, count: Int, history: Array[Int] }
handle Counter {
message { 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(Unknown(_, _)) | None => Unhandled
}
// `.count = default` and `.label = e.value` are writes the view performs
// itself, so they raise no name and there is no case here for them.| 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(
initial=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 # 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 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() -> BoolInstall
Download zipMoonBit port of the tutuca UI framework (value language, templates, vdom, components, app runtime, lint, CLI)
Dependencies