Sign in

    luna

    Fine-grained reactive UI library for Moonbit/JS

    luna
    ui
    signals
    Download zip
    Author
    Version
    0.3.5
    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/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/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

    AsyncState

    Async state representation - environment independent

    Attr

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

    ComponentRef

    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

    Context

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

    EffectRunner

    Effect wrapper with unique ID for deduplication

    EventHandler

    Event handler type - newtype wrapper for callback function

    MatchCase

    Match case for Switch - pairs a condition with content

    Node

    Virtual DOM node types

    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.
    test {
    let count = @signal.signal(0)
    inspect(count.get(), content="0")
    count.set(5)
    inspect(count.get(), content="5")
    }

    Trigger

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

    Hydration trigger type alias for external use

    TriggerType

    Hydration trigger types - when to hydrate a component

    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

    VIsland

    Virtual Island node - serializable hydration unit

    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, 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.
    test {
    let a = @signal.signal(true)
    let b = @signal.signal(true)
    let c = @signal.signal(false)
    let all_true = @signal.all([a, b, c])
    inspect(all_true(), content="false")
    c.set(true)
    inspect(all_true(), content="true")
    a.set(false)
    inspect(all_true(), content="false")
    }

    any

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

    Create a signal that is true when any input signal is true.
    test {
    let a = @signal.signal(false)
    let b = @signal.signal(false)
    let any_true = @signal.any([a, b])
    inspect(any_true(), content="false")
    a.set(true)
    inspect(any_true(), content="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

    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.
    test {
    let a = @signal.signal(0)
    let b = @signal.signal(0)
    let runs : Ref[Int] = { val: 0 }
    let _ = @signal.render_effect(fn() {
    let _ = a.get() + b.get()
    runs.val = runs.val + 1
    })
    inspect(runs.val, content="1") // Initial run
    @signal.batch(fn() {
    a.set(1)
    b.set(2)
    })
    // Only one additional run, not two
    inspect(runs.val, content="2")
    }

    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.
    test {
    let first = @signal.signal("Hello")
    let last = @signal.signal("World")
    let full = @signal.combine2(first, last, fn(f, l) { f + " " + l })
    inspect(full(), content="Hello World")
    first.set("Hi")
    inspect(full(), content="Hi World")
    }

    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

    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.
    test {
    let count = @signal.signal(5)
    let doubled = @signal.computed(fn() { count.get() * 2 })
    inspect(doubled(), content="10")
    count.set(7)
    inspect(doubled(), content="14")
    }

    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.
    test {
    let count = @signal.signal(0)
    let log : Array[Int] = []
    let result = @signal.create_root(fn(dispose) {
    let _ = @signal.render_effect(fn() { log.push(count.get()) })
    dispose // Return dispose function
    })
    count.set(1)
    result() // Dispose the root
    count.set(2) // Won't trigger effect
    inspect(log, content="[0, 1]")
    }

    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.
    // Note: effect() uses microtasks, so testing requires async handling
    let count = @signal.signal(0)
    let _ = @signal.effect(fn() {
    println("count changed to: " + count.get().to_string())
    })
    count.set(1) // Effect will run after current task completes

    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.
    test {
    let count = @signal.signal(0)
    let runs : Ref[Int] = { val: 0 }
    @signal.effect_once(fn() {
    let _ = count.get()
    runs.val = runs.val + 1
    })
    count.set(1)
    count.set(2)
    inspect(runs.val, content="2")
    }

    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.
    test {
    let enabled = @signal.signal(false)
    let count = @signal.signal(0)
    let log : Array[Int] = []
    let _ = @signal.effect_when(fn() { enabled.get() }, fn() {
    log.push(count.get())
    })
    count.set(1) // Won't run, enabled is false
    enabled.set(true)
    count.set(2)
    inspect(log, content="[1, 2]")
    }

    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

    Flatten a signal of signals. Unwraps nested Signal[Signal[T]] to a getter for T.
    test {
    let inner1 = @signal.signal(10)
    let inner2 = @signal.signal(20)
    let outer = @signal.signal(inner1)
    let flat = @signal.flatten(outer)
    inspect(flat(), content="10")
    outer.set(inner2)
    inspect(flat(), content="20")
    inner2.set(30)
    inspect(flat(), content="30")
    }

    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

    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.
    test {
    let a = @signal.signal(2)
    let b = @signal.signal(3)
    let compute_count : Ref[Int] = { val: 0 }
    let sum = @signal.memo(fn() {
    compute_count.val = compute_count.val + 1
    a.get() + b.get()
    })
    inspect(sum(), content="5")
    inspect(sum(), content="5") // Cached, no recompute
    inspect(compute_count.val, content="1")
    a.set(10)
    inspect(sum(), content="13") // Recomputes
    inspect(compute_count.val, content="2")
    }
    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.
    test {
    let count = @signal.signal(0)
    let values : Array[Int] = []
    let unsub = @signal.on(count, fn(v) { values.push(v) })
    count.set(1)
    count.set(2)
    unsub()
    count.set(3) // This won't be captured
    inspect(values, content="[1, 2]")
    }

    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)
    test {
    let count = @signal.signal(0)
    let log : Array[String] = []
    let dispose = @signal.render_effect(fn() {
    let v = count.get()
    log.push("run:" + v.to_string())
    @signal.on_cleanup(fn() { log.push("cleanup:" + v.to_string()) })
    })
    count.set(1)
    dispose()
    inspect(
    log,
    content=(
    #|["run:0", "cleanup:0", "run:1", "cleanup:1"]
    ),
    )
    }

    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.
    test {
    let count = @signal.signal(10)
    let values : Array[Int] = []
    let unsub = @signal.on_immediate(count, fn(v) { values.push(v) })
    count.set(20)
    unsub()
    // First value (10) was captured immediately, then 20 on update
    inspect(values, content="[10, 20]")
    }

    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.
    test {
    let count = @signal.signal(0)
    let runs : Ref[Int] = { val: 0 }
    @signal.on_mount(fn() {
    let _ = count.get() // This read won't create subscription
    runs.val = runs.val + 1
    })
    count.set(1)
    count.set(2)
    // on_mount only ran once, signal changes don't trigger re-run
    inspect(runs.val, content="1")
    }
    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).
    test {
    let count = @signal.signal(1)
    let prev = @signal.previous(count)
    inspect(prev(), content="None") // No previous value yet
    count.set(2)
    inspect(prev(), content="Some(1)")
    count.set(3)
    inspect(prev(), content="Some(2)")
    }

    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?.
    test {
    let count = @signal.signal(10)
    let prev = @signal.previous_with_initial(count, 0)
    inspect(prev(), content="0") // Initial value
    count.set(20)
    inspect(prev(), content="10")
    count.set(30)
    inspect(prev(), content="20")
    }

    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. Example:
    let theme_ctx = create_context("light") provide(theme_ctx, "dark", fn() { // All components here will see "dark" my_component() })

    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)

    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.
    test {
    let items = @signal.signal(["a", "b", "c"])
    let idx = @signal.signal(1)
    let selected = @signal.select(items, idx)
    inspect(
    selected(),
    content=(
    #|Some("b")
    ),
    )
    idx.set(0)
    inspect(
    selected(),
    content=(
    #|Some("a")
    ),
    )
    idx.set(10) // Out of bounds
    inspect(selected(), content="None")
    }

    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.
    test {
    let count = @signal.signal(100)
    inspect(count.get(), content="100")
    }

    switch_

    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

    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.
    test {
    let count = @signal.signal(0)
    let runs : Ref[Int] = { val: 0 }
    let _ = @signal.render_effect(fn() {
    @signal.untracked(fn() {
    let _ = count.get() // Won't create dependency
    runs.val = runs.val + 1
    })
    })
    count.set(1)
    count.set(2)
    // Effect only ran once (initial), not on updates
    inspect(runs.val, content="1")
    }

    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 Example:
    let theme = use_context(theme_ctx) // Returns "dark" or default

    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).
    test {
    let count = @signal.signal(0)
    let changes : Array[(Int, Int)] = []
    let dispose = @signal.watch(fn() { count.get() }, fn(new_val, old_val) {
    changes.push((new_val, old_val))
    })
    count.set(1)
    count.set(2)
    count.set(2) // Same value, no callback
    count.set(3)
    dispose()
    inspect(changes, content="[(1, 0), (2, 1), (3, 2)]")
    }

    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.
    test {
    let count = @signal.signal(5)
    let log : Array[String] = []
    let dispose = @signal.watch_immediate(fn() { count.get() }, fn(
    new_val,
    old_val,
    ) {
    match old_val {
    Some(old) => log.push(old.to_string() + "->" + new_val.to_string())
    None => log.push("init:" + new_val.to_string())
    }
    })
    count.set(10)
    dispose()
    inspect(
    log,
    content=(
    #|["init:5", "5->10"]
    ),
    )
    }

    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