luna

    Fine-grained reactive UI library for Moonbit/JS

    luna
    ui
    signals
    Download zip
    Author
    Version
    0.24.1
    License
    MIT
    Last updated
    21 hours ago
    Downloads
    19K

    #mizchi/luna

    UI primitive for MoonBit / JS. Fine-grained reactive signals, VDOM, hydration, stream renderer, file-based routing core, and the Island runtime that powers mizchi/sol and mizchi/astra.

    // moon.mod.json { "deps": { "mizchi/luna": "0.19.2" } }

    For TypeScript consumers, the JS bindings ship as @luna_ui/luna (also at 0.19.x) — see ../js/luna/.

    #Layout

    • src/ — luna source (Moon packages: signals, render, routes, dom, x/css, x/stella, etc.). Headless + styled APG components live in the separate mizchi/luna_components mooncake (../luna_components/).
    • e2e/ — Playwright suites for the JS-side bindings
    • experiments/ — research code (css-factorize, view_transition, webcomponents_ssr, …)
    • spec/ — design notes
    • vite.config.ts, vitest.config.ts — JS-side dev / test runners

    #Development

    From the repo root:

    moon check --target js # workspace check (luna + sol + astra) moon test --target js # workspace test pnpm test:browser # vitest browser runner pnpm test:e2e # luna's Playwright suite

    #Sibling packages

    The monorepo overview is in ../README.md.

    #License

    MIT

    #Luna Core

    Core functionality of the Luna UI library. Platform-independent.

    #Module Structure

    SubmoduleResponsibility
    signal/Reactive primitives (Signal, Effect, Computed)
    render/VNode → HTML string rendering
    routes/Type-safe routing
    serialize/State serialization/deserialization
    vnode.mbtVNode type definitions

    #VNode

    Virtual DOM node. Type parameter E represents the event type.

    pub enum Node[E] {
    Element(VElement[E]) // HTML element
    Text(String) // Static text
    DynamicText(() -> String) // Dynamic text
    Fragment(Array[Node[E]]) // Fragment
    Show(...) // Conditional rendering
    For(...) // List rendering
    Island(VIsland[E]) // Hydration boundary
    WcIsland(VWcIsland[E]) // Web Components Island
    Async(VAsync[E]) // Async node
    // ...
    }

    #Signal

    Reactive value container.

    let count = @signal.signal(0)
    count.get() // 0
    count.set(1) // Set value
    count.update(fn(n) { n + 1 }) // Update function

    // Derived value
    let doubled = @signal.computed(fn() { count.get() * 2 })

    // Side effects
    @signal.effect(fn() {
    println(count.get())
    })

    #Attr

    Attribute values. Supports static/dynamic/event handlers.

    pub enum Attr[E] {
    VStatic(String) // Static value
    VDynamic(() -> String) // Signal-linked
    VHandler(EventHandler[E]) // Event handler
    VAction(String) // Declarative action
    }

    #TriggerType

    Hydration triggers.

    pub enum TriggerType {
    Load // On page load
    Idle // On requestIdleCallback
    Visible // On IntersectionObserver detection
    Media(String) // On media query match
    None // Manual trigger
    }

    #References

    AsyncState

    Async state representation - environment independent

    Attr

    using @mizchi/luna/core { type Attr }

    Attribute value that can be static or dynamic (signal-based) Type parameter A represents the attribute value type:
    • Web: A = String (HTML attributes are strings)
    • TUI: A = TuiAttrValue (typed values like Dimension, Color)

    ComponentRef

    Component reference for type-safe Island embedding Represents a client-side Web Components Island that can be embedded in server-rendered HTML
    • url: Path to the client JavaScript (e.g., "/static/counter.js")
    • props: Component props (must be ToJson serializable)
    • trigger: When to hydrate the component

    Context

    using @mizchi/signals { type Context }

    Context type with typed getter The current_getter is managed per-context and tracks the current value chain

    EventHandler

    Event handler type - newtype wrapper for callback function

    MatchCase

    Match case for Switch - pairs a condition with content

    Node

    using @mizchi/luna/core { type Node }

    Virtual DOM node types Type parameter A represents the attribute value type (see Attr[E, A])

    Owner

    using @mizchi/signals { type Owner }

    Owner - manages lifecycle of reactive computations (Solid.js style)

    Resource

    Resource - Async value container with Signal integration Environment-agnostic: uses callbacks instead of Promise/async directly

    Signal

    using @mizchi/signals { type Signal }

    Signal type - holds a value and notifies subscribers on change. A reactive primitive that stores a value and automatically tracks dependencies.

    Trigger

    using @mizchi/luna/core { type TriggerType as Trigger }

    Hydration trigger type alias for external use

    TriggerType

    Hydration trigger types - when to hydrate a component

    TrustedHtml

    Explicit wrapper for HTML that has already been trusted by the caller.

    VAsync

    using @mizchi/luna/core { type VAsync }

    Virtual Async node for async rendering with error handling Note: async functions implicitly raise Error, no need for raise? annotation

    VElement

    Virtual element node

    VErrorBoundary

    Virtual ErrorBoundary node for catching rendering errors Similar to Solid.js ErrorBoundary - catches errors during:
    • Child component rendering
    • Effects and memos within children Does NOT catch:
    • Event handler errors
    • Async errors outside render cycle

    VInternalRef

    Virtual Internal Reference node for type-safe Island embedding

    VSwitch

    using @mizchi/luna/core { type VSwitch }

    Virtual Switch node - renders first matching case Similar to Solid.js /
    • cases: array of condition-content pairs, evaluated in order
    • fallback: optional content when no case matches

    VWcIsland

    Virtual Web Components Island node - Web Components based hydration unit Uses Declarative Shadow DOM for SSR and DOM Parts for partial updates

    action

    fn[E, V, Act : Show] action(a : Act) ->
    Attr
    [E, V]

    Create an action attribute value - dispatches named action on event Used for declarative event handling with enum types: ("onclick", action(Increment)) // requires derive(Show)

    Example:
    pub enum MyAction { Increment; Decrement } derive(Show) h("button", [("onclick", action(Increment))], [...])

    all

    fn all(signals : Array[
    Signal
    [Bool]]) -> (() -> Bool)

    Create a signal that is true when all input signals are true.

    any

    fn any(signals : Array[
    Signal
    [Bool]]) -> (() -> Bool)

    Create a signal that is true when any input signal is true.

    async_

    fn[E, A] async_(render~ : async () ->
    Node
    [E, A], fallback~ : () ->
    Node
    [E, A], on_error? : (Error) ->
    Node
    [E, A]?) ->
    Node
    [E, A]

    Create an async VNode with fallback
    • render: async function that produces the content (may raise errors)
    • fallback: shown while loading or on error (if no on_error handler)
    • on_error: optional custom error UI handler

    attr_dynamic

    fn[E, A] attr_dynamic(getter : () -> A) ->
    Attr
    [E, A]

    Create a dynamic attribute value. The value is computed lazily on each render.
    test {
    let mut count = 0
    let attr : Attr[Unit, String] = attr_dynamic(fn() {
    "count-" + count.to_string()
    })
    guard attr is VDynamic(getter) else { fail("expected VDynamic") }
    inspect(getter(), content="count-0")
    count = 5
    inspect(getter(), content="count-5")
    }

    attr_dynamic_style

    fn[E] attr_dynamic_style(getter : () -> String) ->
    Attr
    [E, String]

    Create a dynamic style attribute value Note: For Web (A = String), the getter returns a style string

    attr_handler

    fn[E, A] attr_handler(handler :
    EventHandler
    [E]) ->
    Attr
    [E, A]

    Create a handler attribute value

    attr_static

    fn[E, A] attr_static(value : A) ->
    Attr
    [E, A]

    Create a static attribute value.
    test {
    let attr : Attr[Unit, String] = attr_static("my-class")
    guard attr is VStatic(v) else { fail("expected VStatic") }
    inspect(v, content="my-class")
    }

    attr_style

    fn[E] attr_style(style : String) ->
    Attr
    [E, String]

    Create a style attribute value (string form, e.g. "color: red; margin: 10px") Note: For Web (A = String), pass the style string directly

    batch

    fn[T] batch(f : () -> T) -> T

    Run a function in a batch - all signal updates are batched. Effects only run once after all updates complete.

    batch_end

    fn batch_end() -> Unit

    End a batch update - run all pending effects

    batch_start

    fn batch_start() -> Unit

    Start a batch update - effects won't run until batch ends

    combine2

    fn[A, B, R] combine2(a :
    Signal
    [A], b :
    Signal
    [B], f : (A, B) -> R) -> (() -> R)

    Combine two signals into one. The getter recomputes when either signal changes.

    combine3

    fn[A, B, C, R] combine3(a :
    Signal
    [A], b :
    Signal
    [B], c :
    Signal
    [C], f : (A, B, C) -> R) -> (() -> R)

    Combine three signals into one

    combine4

    fn[A, B, C, D, R] combine4(a :
    Signal
    [A], b :
    Signal
    [B], c :
    Signal
    [C], d :
    Signal
    [D], f : (A, B, C, D) -> R) -> (() -> R)

    Combine four signals into one

    component

    fn[E, A] component(render : () ->
    Node
    [E, A]) ->
    Node
    [E, A]

    Create a component VNode. Wraps a render function as a component boundary.
    test {
    let node : Node[Unit, String] = component(fn() {
    h("div", [], [text("Component")])
    })
    guard node is Component(render~) else { fail("expected Component") }
    guard render() is Element(el) else { fail("expected Element") }
    inspect(el.tag, content="div")
    }

    component_ref

    fn[T] component_ref(url : String, props : T, trigger? :
    TriggerType
    ) ->
    ComponentRef
    [T]

    Create a ComponentRef for a Web Components Island

    computed

    fn[T] computed(compute : () -> T) -> (() -> T)

    Create a computed signal (alias for memo with Signal-like API). Same as memo, but named to be more familiar for those coming from other reactive frameworks.

    create_context

    fn[T] create_context(default_value : T) ->
    Context
    [T]

    Create a new context with a default value Similar to Solid.js createContext

    create_root

    fn[T] create_root(f : (() -> Unit) -> T) -> T

    Create a new reactive root scope. The function receives a dispose callback that cleans up all effects. Returns the result of the function.

    create_root_with_dispose

    fn[T] create_root_with_dispose(f : () -> T) -> (T, () -> Unit)

    Create a reactive root and return both result and dispose function

    deferred

    fn[T] deferred() -> (
    Resource
    [T], (T) -> Unit, (String) -> Unit)

    Create a deferred Resource (starts pending, resolve/reject manually)

    effect

    fn effect(fn_ : () -> Unit) -> (() -> Unit)

    Create an effect that is deferred until after rendering completes. (Solid.js style createEffect - deferred execution via microtask) Returns a dispose function to stop the effect. Unlike render_effect, this runs asynchronously via microtask queue.

    effect_once

    fn effect_once(fn_ : () -> Unit) -> Unit

    Create a one-time effect that disposes itself after first run. Useful for initialization logic that should only run once.

    effect_when

    fn effect_when(condition : () -> Bool, fn_ : () -> Unit) -> (() -> Unit)

    Create an effect that only runs when condition is true. Uses render_effect for synchronous execution.

    error_boundary

    fn[E, A] error_boundary(children~ : () ->
    Node
    [E, A] raise, fallback~ : (Error, () -> Unit) ->
    Node
    [E, A] raise) ->
    Node
    [E, A]

    Create an error boundary VNode Catches errors during child rendering and displays fallback UI
    • children: lazy function that produces child content (may throw)
    • fallback: function receiving (error, reset) that produces fallback UI
      • error: the caught Error
      • reset: function to retry rendering children

    Example:
    error_boundary( children=fn() { risky_component() }, fallback=fn(err, reset) { h("div", [], [ text("Error: " + err.to_string()), h("button", [("onclick", handler(fn(_) { reset() }))], [text("Retry")]) ]) } )

    event_handler

    fn event_handler() ->
    EventHandler
    [Unit]

    Create a placeholder event handler for SSR (noop)

    flatten

    fn[T] flatten(outer :
    Signal
    [
    Signal
    [T]]) -> (() -> T)

    Flatten a signal of signals. Unwraps nested Signal[Signal[T]] to a getter for T.

    for_each

    fn[E, A] for_each(items : () -> Array[
    Node
    [E, A]]) ->
    Node
    [E, A]

    Create a list VNode. Renders a dynamic list of nodes.
    test {
    let items = ["a", "b", "c"]
    let node : Node[Unit, String] = for_each(fn() { items.map(fn(s) { text(s) }) })
    guard node is For(render~) else { fail("expected For") }
    inspect(render().length(), content="3")
    }

    fragment

    fn[E, A] fragment(children : Array[
    Node
    [E, A]]) ->
    Node
    [E, A]

    Create a fragment VNode. Groups multiple nodes without a wrapper element.
    test {
    let node : Node[Unit, String] = fragment([
    text("Hello"),
    text(" "),
    text("World"),
    ])
    guard node is Fragment(children) else { fail("expected Fragment") }
    inspect(children.length(), content="3")
    }

    get_owner

    fn get_owner() ->
    Owner
    ?

    Get the current owner (if any)
    fn[E, A] h(tag : String, attrs : Array[(String,
    Attr
    [E, A])], children : Array[
    Node
    [E, A]]) ->
    Node
    [E, A]

    Create a VNode element. The main building block for creating virtual DOM elements.
    test {
    let node : Node[Unit, String] = h(
    "div",
    [("class", attr_static("container"))],
    [text("Hello")],
    )
    guard node is Element(el) else { fail("expected Element") }
    inspect(el.tag, content="div")
    }

    handler

    fn[E] handler(f : (E) -> Unit) ->
    EventHandler
    [E]

    Create an event handler from a callback.
    test {
    let h : EventHandler[Int] = handler(fn(x) { let _ = x * 2 })
    inspect(h.get_callback()(5), content="()")
    }

    handler_from_callback

    fn handler_from_callback(f : () -> Unit) ->
    EventHandler
    [Unit]

    Create an event handler from a simple callback (ignores event, for SSR compatibility)

    has_dynamic_content

    fn[E, A] has_dynamic_content(attrs : Array[(String,
    Attr
    [E, A])]) -> Bool

    Check if element has dynamic content that needs hydration

    has_owner

    fn has_owner() -> Bool

    Check if currently inside an owner scope

    internal_ref

    fn[E, A] internal_ref(url : String, state : String, trigger? :
    TriggerType
    , styles? : String, children? : Array[
    Node
    [E, A]]) ->
    Node
    [E, A]

    Create an internal reference VNode (for server_dom.wc_island())

    match_case

    fn[E, A] match_case(when~ : () -> Bool, render~ : () ->
    Node
    [E, A]) ->
    MatchCase
    [E, A]

    Create a match case for Switch
    • when: condition function, evaluated lazily
    • render: content to render when condition is true

    memo

    fn[T] memo(compute : () -> T) -> (() -> T)

    Create a memoized computation. Returns a getter function that caches the result and recomputes only when dependencies change.
    fn[T] on(sig :
    Signal
    [T], callback : (T) -> Unit) -> (() -> Unit)

    Explicitly subscribe to a signal with a callback. Returns an unsubscribe function. Unlike effect(), this doesn't auto-track and only listens to this one signal. Callback is NOT called on initial subscription, only on subsequent changes.

    on_cleanup

    fn on_cleanup(cleanup : () -> Unit) -> Unit

    Register a cleanup function to run when the current scope disposes. Works in:
    • Effects: cleanup runs before effect re-runs or when effect disposes
    • Components: cleanup runs when component unmounts (Solid.js style - can be called directly in component body)

    on_immediate

    fn[T] on_immediate(sig :
    Signal
    [T], callback : (T) -> Unit) -> (() -> Unit)

    Subscribe and run immediately with current value. Like on, but also invokes the callback with the current value right away.

    on_mount

    fn on_mount(fn_ : () -> Unit) -> Unit

    Run a function once after mount (Solid.js style onMount). The function runs without tracking dependencies. Cleanup registered via on_cleanup inside will run when owner is disposed.
    fn[T] previous(sig :
    Signal
    [T]) -> (() -> T?)

    Create a signal that holds the previous value of another signal. Returns a getter function for the previous value (None on first read).

    previous_with_initial

    fn[T] previous_with_initial(sig :
    Signal
    [T], initial : T) -> (() -> T)

    Create a signal that holds the previous value with initial value. Unlike previous, this returns a plain T instead of T?.

    provide

    fn[T, R] provide(ctx :
    Context
    [T], value : T, f : () -> R) -> R

    Provide a context value for the current Owner and its descendants The value is associated with the current Owner and will be available to all effects and components created within this scope.

    raw_html

    fn[E, A] raw_html(content : String) ->
    Node
    [E, A]

    Create a raw HTML VNode (content is not escaped). Use with caution: content should be trusted or sanitized.
    test {
    let node : Node[Unit, String] = raw_html("<strong>Bold</strong>")
    guard node is RawHtml(html) else { fail("expected RawHtml") }
    inspect(html, content="<strong>Bold</strong>")
    }

    raw_trusted_html

    fn[E, A] raw_trusted_html(content :
    TrustedHtml
    ) ->
    Node
    [E, A]

    Create a raw HTML VNode from an explicit trusted wrapper.

    register_disposer

    fn register_disposer(disposer : () -> Unit) -> Unit

    Register a disposer with the current owner Called internally by effect() to register its dispose function

    register_owner_cleanup

    fn register_owner_cleanup(cleanup : () -> Unit) -> Unit

    Register a cleanup with the current owner (alternative to onCleanup in effect)

    render_effect

    fn render_effect(fn_ : () -> Unit) -> (() -> Unit)

    Create an effect that runs immediately and re-runs when dependencies change. (Solid.js style createRenderEffect - synchronous execution) Returns a dispose function to stop the effect. Supports on_cleanup() calls inside the effect.

    resource

    fn[T] resource(fetcher : ((T) -> Unit, (String) -> Unit) -> Unit) ->
    Resource
    [T]

    Create a Resource from a callback-based fetcher The fetcher receives (resolve, reject) callbacks This is environment-agnostic - the actual async execution is handled by the caller

    resource_rejected

    fn[T] resource_rejected(error : String) ->
    Resource
    [T]

    Create a Resource with initial error (already rejected)

    resource_resolved

    fn[T] resource_resolved(value : T) ->
    Resource
    [T]

    Create a Resource with initial value (already resolved)

    run_with_owner

    fn[T] run_with_owner(owner :
    Owner
    , f : () -> T) -> T

    Run a function with a specific owner as current

    select

    fn[T] select(items :
    Signal
    [Array[T]], index :
    Signal
    [Int]) -> (() -> T?)

    Select from an array signal by index signal. Returns None if index is out of bounds.

    show

    fn[E, A] show(when : () -> Bool, child : () ->
    Node
    [E, A]) ->
    Node
    [E, A]

    Create a conditional VNode. Only renders the child when the condition is true.
    test {
    let visible = true
    let node : Node[Unit, String] = show(fn() { visible }, fn() {
    text("Visible!")
    })
    guard node is Show(condition~, ..) else { fail("expected Show") }
    inspect(condition(), content="true")
    }

    signal

    fn[T] signal(initial : T) ->
    Signal
    [T]

    Convenience function to create a signal. Shorthand for Signal::new.

    switch_

    fn[E, A] switch_(cases~ : Array[
    MatchCase
    [E, A]], fallback? : () ->
    Node
    [E, A]?) ->
    Node
    [E, A]

    Create a switch VNode - renders first matching case Similar to Solid.js /

    Example:
    switch_( cases=[ match_case(when=fn() { state.get() == 1 }, render=fn() { text("One") }), match_case(when=fn() { state.get() == 2 }, render=fn() { text("Two") }), ], fallback=Some(fn() { text("Other") }) )

    text

    fn[E, A] text(content : String) ->
    Node
    [E, A]

    Create a text VNode.
    test {
    let node : Node[Unit, String] = text("Hello World")
    guard node is Text(s) else { fail("expected Text") }
    inspect(s, content="Hello World")
    }

    text_dyn

    fn[E, A] text_dyn(content : () -> String) ->
    Node
    [E, A]

    Create a dynamic text VNode. The content is computed lazily on each render.
    test {
    let mut count = 5
    let node : Node[Unit, String] = text_dyn(fn() { count.to_string() })
    guard node is DynamicText(getter) else { fail("expected DynamicText") }
    inspect(getter(), content="5")
    count = 10
    inspect(getter(), content="10")
    }

    text_of

    fn[E, A, T : Show] text_of(sig :
    Signal
    [T]) ->
    Node
    [E, A]

    Create a text VNode from a signal. Shorthand for text_dyn(fn() { sig.get().to_string() }).

    unsafe_trusted_html

    fn unsafe_trusted_html(content : String) ->
    TrustedHtml

    untracked

    fn[T] untracked(f : () -> T) -> T

    Run a function without tracking (useful for avoiding circular deps). Signal reads inside this function won't create subscriptions.

    use_context

    fn[T] use_context(ctx :
    Context
    [T]) -> T

    Use a context value - returns the current provided value or default Walks up the Owner chain to find the nearest provided value. Similar to Solid.js useContext

    watch

    fn[T : Eq] watch(source : () -> T, callback : (T, T) -> Unit) -> (() -> Unit)

    Watch a computed expression and run callback when it changes. Returns a dispose function. The callback receives (newValue, oldValue).

    watch_immediate

    fn[T : Eq] watch_immediate(source : () -> T, callback : (T, T?) -> Unit) -> (() -> Unit)

    Watch with immediate execution (runs callback on first value too). The callback receives (newValue, oldValue?) where oldValue is None on first run.

    wc_component_ref

    fn[T] wc_component_ref(url : String, props : T, trigger? :
    TriggerType
    ) ->
    ComponentRef
    [T]

    Alias for component_ref — kept for backward source compatibility within the workspace.

    wc_island

    fn[E, A] wc_island(name : String, url : String, styles : String, state : String, children : Array[
    Node
    [E, A]], trigger? :
    TriggerType
    ) ->
    Node
    [E, A]

    Create a Web Components island VNode for partial hydration Uses Declarative Shadow DOM for SSR

    Source Files