Sign in

    luna

    Fine-grained reactive UI library for Moonbit/JS

    luna
    ui
    signals
    Download zip
    Author
    Version
    0.6.0
    License
    MIT
    Last updated
    9 months ago
    Downloads
    20K
    Warning: This is a Proof of Concept (PoC)

    This project is experimental and under active development. APIs may change without notice. Not recommended for production use.

    #Luna UI

    Docs: https://luna.mizchi.workers.dev/

    A reactive UI library written in MoonBit with Island Architecture and Fine-Grained Reactivity inspired by Solid.js and Qwik.

    • Client-side: Compiles to JavaScript for browser DOM rendering and hydration
    • Server-side: Runs on native backend for high-performance SSR
    • JavaScript API: Use from JavaScript/TypeScript via @luna_ui/luna npm package

    #Features

    • Fine-Grained Reactivity - Signal-based reactive primitives (Signal, effect, memo) with automatic dependency tracking
    • Island Architecture - Partial hydration with selective component loading
    • SSR (Server-Side Rendering) - Render to HTML string with streaming support
    • Hydration - Restore interactivity on SSR-rendered content with multiple trigger strategies (load, idle, visible, media)
    • JSX/TSX Support - Use familiar JSX syntax via @luna_ui/luna npm package
    • Multi-target Support - Core signals work on js, native, wasm, wasm-gc

    #Installation

    #MoonBit

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

    #npm (for JSX/TSX)

    npm install @luna_ui/luna

    #Target Support

    Packagejsnativewasmwasm-gc
    mizchi/luna (core)✅✅✅✅
    mizchi/luna/core/render✅✅✅✅
    mizchi/luna/dom✅---

    • Core (mizchi/luna): Signals, VNode, reactive primitives - works on all targets
    • Render (mizchi/luna/core/render): HTML string rendering - works on all targets
    • DOM (mizchi/luna/dom): Browser DOM rendering and hydration - JavaScript only

    #Usage

    #Signals - Reactive Primitives

    // Create a signal
    let count = @signal.signal(0)

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

    // Create an effect that auto-tracks dependencies
    let _ = @signal.effect(fn() {
    println("Count: " + count.get().to_string())
    @signal.on_cleanup(fn() { println("Cleaning up") })
    })

    // Update triggers the effect
    count.set(1) // prints: Count: 1
    count.update(fn(n) { n + 1 }) // prints: Count: 2

    #DOM Rendering (JavaScript target)

    fn counter_component() -> @luna_dom.DomNode {
    let count = @signal.signal(0)
    let doubled = @signal.memo(fn() { count.get() * 2 })

    @luna_dom.div(class="counter", [
    @luna_dom.h2([@luna_dom.text("Counter")]),
    // Dynamic text updates automatically
    @luna_dom.p([@luna_dom.text_dyn(fn() { "Count: " + count.get().to_string() })]),
    @luna_dom.p([@luna_dom.text_dyn(fn() { "Doubled: " + doubled().to_string() })]),
    @luna_dom.div(class="buttons", [
    @luna_dom.button(
    on=@luna_dom.events().click(fn(_) { count.update(fn(n) { n - 1 }) }),
    [@luna_dom.text("-")],
    ),
    @luna_dom.button(
    on=@luna_dom.events().click(fn(_) { count.update(fn(n) { n + 1 }) }),
    [@luna_dom.text("+")],
    ),
    @luna_dom.button(
    on=@luna_dom.events().click(fn(_) { count.set(0) }),
    [@luna_dom.text("Reset")],
    ),
    ]),
    ])
    }

    fn main {
    let doc = @dom.document()
    match doc.getElementById("app") {
    Some(el) => {
    let app = counter_component()
    @luna_dom.render(el |> @luna_dom.DomElement::from_dom, app)
    }
    None => ()
    }
    }

    #Conditional Rendering

    fn conditional_example() -> @luna_dom.DomNode {
    let show = @signal.signal(true)

    @luna_dom.div([
    @luna_dom.button(
    on=@luna_dom.events().click(fn(_) { show.update(fn(b) { not(b) }) }),
    [@luna_dom.text_dyn(fn() { if show.get() { "Hide" } else { "Show" } })],
    ),
    @luna_dom.show(fn() { show.get() }, fn() {
    @luna_dom.div([@luna_dom.text("Conditionally rendered content")])
    }),
    ])
    }

    #List Rendering

    fn list_example() -> @luna_dom.DomNode {
    let items : @signal.Signal[Array[String]] = @signal.signal(["A", "B", "C"])

    @luna_dom.ul([
    @luna_dom.for_each(fn() { items.get() }, fn(item, index) {
    @luna_dom.li([@luna_dom.text(item)])
    }),
    ])
    }

    #Development

    Requires just command runner.

    # Install dependencies pnpm install # Run all CI checks (recommended before PR) just ci # Type check just check # Run all tests just test # Run MoonBit tests only just test-moonbit # Run browser tests just test-browser # Run E2E tests just test-e2e # Build just build # Format code just fmt # Show bundle sizes just size # Watch mode just watch

    #Project Structure

    src/ ├── core/ # Target-independent │ ├── signal/ # Signal primitives │ ├── vnode.mbt # VNode definition │ ├── render/ # HTML string rendering │ ├── routes/ # Route matching │ └── serialize/ # State serialization ├── platform/ # Platform-specific │ ├── dom/ # Browser DOM API │ │ ├── element/ # Low-level DOM operations (render, diff, reconcile) │ │ └── router/ # Client-side router │ ├── js/ # JS-specific │ │ └── api/ # Public API for JS (@luna_ui/luna) │ └── server_dom/ # Server-side SSR helpers ├── stella/ # Shard/Island embedding ├── sol/ # SSR framework (CLI + runtime) ├── examples/ # Example applications └── tests/ # Test fixtures js/ ├── luna/ # npm package (@luna_ui/luna) └── loader/ # Island hydration loader (@luna_ui/luna-loader) e2e/ # Playwright E2E tests

    #Island Architecture

    Luna supports partial hydration through Island Architecture. Components are rendered on the server and selectively hydrated on the client based on triggers:

    TriggerDescription
    loadHydrate immediately on page load
    idleHydrate during browser idle time (requestIdleCallback)
    visibleHydrate when element becomes visible (IntersectionObserver)
    mediaHydrate when media query matches
    noneManual hydration via __LN_HYDRATE__

    #Sol - SSR Framework

    Sol is a built-in SSR framework that integrates with Hono for server-side rendering. Create new projects with:

    just sol new myapp

    #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

    Context

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

    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

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

    Attr

    pub enum Attr[E] {
    VStatic(String)
    VDynamic(() -> String)
    VHandler(EventHandler[E])
    VAction(String)
    }

    Attribute value that can be static or dynamic (signal-based)

    ComponentRef

    pub(all) struct ComponentRef[T] {
    url : String
    props : T
    wc : Bool
    trigger : TriggerType
    }

    Component reference for type-safe Island embedding Represents a client-side component 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)
    • wc: Whether this is a Web Components based Island
    • trigger: When to hydrate the component

    EventHandler

    pub struct EventHandler[E] {
    callback : (E) -> Unit
    }

    Event handler type - newtype wrapper for callback function

    EventHandler::get_callback

    fn[E] EventHandler::get_callback(self : EventHandler[E]) -> ((E) -> Unit)

    Get the callback function

    MatchCase

    pub struct MatchCase[E] {
    when : () -> Bool
    render : () -> Node[E]
    }

    Match case for Switch - pairs a condition with content

    Node

    pub enum Node[E] {
    Element(VElement[E])
    Text(String)
    DynamicText(() -> String)
    Fragment(Array[Node[E]])
    Show(() -> Bool, () -> Node[E])
    For(() -> Array[Node[E]])
    Component(() -> Node[E])
    Island(VIsland[E])
    WcIsland(VWcIsland[E])
    Async(VAsync[E])
    ErrorBoundary(VErrorBoundary[E])
    Switch(VSwitch[E])
    InternalRef(VInternalRef[E])
    RawHtml(String)
    }

    Virtual DOM node types

    TriggerType

    #alias(Trigger)
    pub(all) enum TriggerType {
    Load
    Idle
    Visible
    Media(String)
    None
    }

    Hydration trigger types - when to hydrate a component

    TriggerType::parse

    fn TriggerType::parse(s : String) -> TriggerType

    Parse TriggerType from string.
    test {
    // Round-trip test: parse then to_string
    inspect(@luna.TriggerType::parse("load").to_string(), content="load")
    inspect(@luna.TriggerType::parse("idle").to_string(), content="idle")
    inspect(@luna.TriggerType::parse("visible").to_string(), content="visible")
    inspect(@luna.TriggerType::parse("none").to_string(), content="none")
    inspect(
    @luna.TriggerType::parse("media:(min-width: 1024px)").to_string(),
    content="media:(min-width: 1024px)",
    )
    }

    TriggerType::to_string

    fn TriggerType::to_string(self : TriggerType) -> String

    Convert TriggerType to attribute value.
    test {
    inspect(@luna.Load.to_string(), content="load")
    inspect(@luna.Idle.to_string(), content="idle")
    inspect(@luna.Visible.to_string(), content="visible")
    inspect(
    @luna.Media("(max-width: 768px)").to_string(),
    content="media:(max-width: 768px)",
    )
    inspect(@luna.None.to_string(), content="none")
    }

    VAsync

    pub struct VAsync[E] {
    render : async () -> Node[E]
    fallback : () -> Node[E]
    on_error : (Error) -> Node[E]?
    }

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

    VElement

    pub struct VElement[E] {
    tag : String
    attrs : Array[(String, Attr[E])]
    children : Array[Node[E]]
    }

    Virtual element node

    VErrorBoundary

    pub struct VErrorBoundary[E] {
    children : () -> Node[E] raise
    fallback : (Error, () -> Unit) -> Node[E] raise
    }

    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

    pub struct VInternalRef[E] {
    url : String
    state : String
    trigger : TriggerType
    wc : Bool
    styles : String
    children : Array[Node[E]]
    }

    Virtual Internal Reference node for type-safe Island embedding

    VIsland

    pub struct VIsland[E] {
    id : String
    url : String
    state : String
    trigger : TriggerType
    children : Array[Node[E]]
    }

    Virtual Island node - serializable hydration unit

    VSwitch

    pub struct VSwitch[E] {
    cases : Array[MatchCase[E]]
    fallback : () -> Node[E]?
    }

    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

    pub struct VWcIsland[E] {
    name : String
    url : String
    styles : String
    state : String
    trigger : TriggerType
    children : Array[Node[E]]
    }

    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, A : Show] action(a : A) -> Attr[E]

    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) @luna.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] async_(render~ : async () -> Node[E], fallback~ : () -> Node[E], on_error? : (Error) -> Node[E]?) -> Node[E]

    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] attr_dynamic(getter : () -> String) -> Attr[E]

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

    attr_dynamic_style

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

    Create a dynamic style attribute value

    attr_handler

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

    Create a handler attribute value

    attr_static

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

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

    attr_style

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

    Create a style attribute value (string form, e.g. "color: red; margin: 10px")

    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] component(render : () -> Node[E]) -> Node[E]

    Create a component VNode. Wraps a render function as a component boundary.
    test {
    let node : @luna.Node[Unit] = @luna.component(fn() {
    @luna.h("div", [], [@luna.text("Component")])
    })
    guard node is @luna.Component(render~) else { fail("expected Component") }
    guard render() is @luna.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 regular Island component

    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] error_boundary(children~ : () -> Node[E] raise, fallback~ : (Error, () -> Unit) -> Node[E] raise) -> Node[E]

    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] for_each(items : () -> Array[Node[E]]) -> Node[E]

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

    fragment

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

    Create a fragment VNode. Groups multiple nodes without a wrapper element.
    test {
    let node : @luna.Node[Unit] = @luna.fragment([
    @luna.text("Hello"),
    @luna.text(" "),
    @luna.text("World"),
    ])
    guard node is @luna.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] h(tag : String, attrs : Array[(String, Attr[E])], children : Array[Node[E]]) -> Node[E]

    Create a VNode element. The main building block for creating virtual DOM elements.
    test {
    let node : @luna.Node[Unit] = @luna.h(
    "div",
    [("class", @luna.attr_static("container"))],
    [@luna.text("Hello")],
    )
    guard node is @luna.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 : @luna.EventHandler[Int] = @luna.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] has_dynamic_content(attrs : Array[(String, Attr[E])]) -> 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] internal_ref(url : String, state : String, trigger? : TriggerType, wc? : Bool, styles? : String, children? : Array[Node[E]]) -> Node[E]

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

    island

    fn[E] island(id : String, url : String, state : String, children : Array[Node[E]], trigger? : TriggerType) -> Node[E]

    Create an island VNode for partial hydration

    match_case

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

    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] raw_html(content : String) -> Node[E]

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

    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] show(when : () -> Bool, child : () -> Node[E]) -> Node[E]

    Create a conditional VNode. Only renders the child when the condition is true.
    test {
    let visible = @signal.signal(true)
    let node : @luna.Node[Unit] = @luna.show(fn() { visible.get() }, fn() {
    @luna.text("Visible!")
    })
    guard node is @luna.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] switch_(cases~ : Array[MatchCase[E]], fallback? : () -> Node[E]?) -> Node[E]

    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] text(content : String) -> Node[E]

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

    text_dyn

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

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

    text_of

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

    Create a text VNode from a signal. Shorthand for text_dyn(fn() { sig.get().to_string() }).
    test {
    let name = @signal.signal("Alice")
    let node : @luna.Node[Unit] = @luna.text_of(name)
    guard node is @luna.DynamicText(getter) else { fail("expected DynamicText") }
    inspect(getter(), content="Alice")
    name.set("Bob")
    inspect(getter(), content="Bob")
    }

    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]

    Create a ComponentRef for a Web Components Island

    wc_island

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

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

    Source Files