Sign in

    sol

    Thin Mars wrapper for file-based routing, SSR, and asset loading

    sol
    mars
    moonbit
    ssg
    ssr
    islands
    routing
    Download zip
    Author
    Version
    0.12.0
    License
    MIT
    Last updated
    7 months ago
    Downloads
    3K

    #Sol - SSR-first Web Framework for MoonBit

    Sol is an SSR-first web framework implemented in MoonBit. It adopts Island Architecture, combining server-side rendering with partial client-side hydration.

    #Playground

    examples/sol_app/ is the development playground for Sol framework. Used for prototyping new features, integration testing, and implementation verification.

    # Start development server in playground cd examples/sol_app pnpm install just sol dev

    #Runtime Asset Sync (Contributors)

    Canonical runtime assets live under src/ssg/assets/scripts/. When you update Luna loader runtime, sync in this order:

    # 1) Sync from luna.mbt dist -> Sol canonical assets -> examples/static just sync-luna-assets ../luna.mbt # 2) Check sync status only (CI-friendly) just check-luna-assets ../luna.mbt # 3) Re-sync examples/static from canonical assets just sync-example-assets

    #Features

    • Hono Integration: Server based on lightweight Hono
    • Island Architecture: Hydrate only necessary parts
    • Type Safety: Safe routing with MoonBit's type system
    • CLI Tools: Consistent workflow from project creation to build
    • Streaming SSR: Streaming support for async content
    • CSR Navigation: SPA-like page transitions with data-sol-link attribute
    • Middleware: Railway Oriented Programming based middleware system
    • Server Actions: Server-side functions with CSRF protection
    • Nested Layouts: Support for hierarchical layout structures

    #Mars-first Responsibilities

    Sol is organized around Mars as the server foundation, with these primary responsibilities:

    • File-based routing assignment: Route mapping from files/config to handlers
    • SSR orchestration: HTML/fragment rendering and response composition
    • Asset loading: Static assets and hydration loader wiring
    • Wasm entrypoint orchestration (optional): .mbtx / .wasm mount manifest generation

    Migration guide for existing Mars users: docs/migrate-from-mars.md Documentation guide: docs/README.md Onboarding guide: docs/onboarding.md Quickstart guide: docs/quickstart.md Deploy guide: docs/deploy.md Runbook: docs/runbook.md Troubleshooting guide: docs/troubleshooting.md

    #Quick Start

    # Create new project sol new myapp --user yourname cd myapp # Install dependencies pnpm install # Start development server pnpm dev # Build and deploy preflight (Cloudflare Workers default) sol build sol deploy --dry-run

    #Configuration

    sol.config.ts is the recommended configuration file. It should default-export a plain object.

    export default { islands: ["app/client"], routes: "app/server", output: "app/__gen__", runtime: "node", client_auto_exports: false, wasmEntryPoints: [ { id: "users_show", route: "/users/:id", source: "app/entries/users_show.mbtx", runtime: "wagi", // "wagi" | "wasi-cli" | "component" method: ["GET"], }, ], }

    If you prefer JSON, sol.config.json is supported. For schema validation in this repo, add:

    { "$schema": "./schemas/sol.config.schema.json" }

    #Project Structure

    Project structure generated by sol new (app server routes live in app/server):

    myapp/ ├── moon.mod.json # MoonBit module definition ├── package.json # npm package definition ├── sol.config.json # Sol config file ├── app/ │ ├── server/ # Server components │ │ ├── moon.pkg │ │ └── routes.mbt # routes() + config() + page functions │ ├── client/ # Client components (Islands) │ │ ├── moon.pkg │ │ └── counter.mbt # render + hydrate functions │ └── __gen__/ # Auto-generated (sol generate) │ ├── client/ # Client exports │ └── server/ # Server entry point └── static/ └── loader.js # Island loader

    The scaffold generated by sol new is defined in src/cli/templates.mbt and matches the layout above.

    #CLI Commands

    #sol new <name> --user <namespace>

    Create a new project.

    sol new myapp --user mizchi # Create mizchi/myapp package sol new myapp --user mizchi --dev # Use local luna path (for development)

    #sol dev

    Start development server. Auto-executes:
    1. sol generate --mode dev - Code generation (outputs to .sol/dev/)
    2. moon build - MoonBit build
    3. rolldown - Client bundle (if manifest exists)
    4. Start server

    sol dev # Default port 7777 sol dev --port 8080 # Specify port sol dev --clean # Clear cache and build

    #sol build

    Production build. Outputs to .sol/prod/.

    sol build # JS target (default) sol build --target wasm # WASM target sol build --skip-bundle # Skip rolldown sol build --skip-generate # Skip generation sol build --clean # Clear cache and build sol build --wasm-entrypoints --runtime wagi --emit-spin-fragment

    #sol serve

    Serve production build. Requires prior sol build.

    sol serve # Default port 7777 sol serve --port 8080 # Specify port

    #sol deploy

    Deployment preflight and command guidance (cloudflare-workers default).

    sol deploy # Dry-run guidance sol deploy --provider cloudflare-workers --project my-worker sol deploy --execute # Execute wrangler command

    #sol generate

    Auto-generate code based on sol.config.ts (recommended) or sol.config.json. If both exist, sol.config.ts takes priority.

    Note: Usually sol dev and sol build call this internally, so explicit execution is not needed.

    sol generate # Use sol.config.ts or sol.config.json (default: prod) sol generate --mode dev # Development mode (outputs to .sol/dev/) sol generate --mode prod # Production mode (outputs to .sol/prod/)

    #sol clean

    Delete generated files and cache.

    sol clean # Delete .sol/, app/__gen__/, _build/

    #SolRoutes Definition

    Declarative route definition using @router.SolRoutes:

    // app/server/routes.mbt

    pub fn routes() -> Array[@router.SolRoutes] {
    [
    // Page route
    @router.SolRoutes::Page(
    path="/",
    handler=@router.PageHandler(home_page),
    title="Home",
    meta=[],
    revalidate=None,
    cache=None,
    ),
    // API route (GET)
    @router.SolRoutes::Get(
    path="/api/health",
    handler=@router.ApiHandler(api_health),
    ),
    // API route (POST)
    @router.SolRoutes::Post(
    path="/api/submit",
    handler=@router.ApiHandler(api_submit),
    ),
    // Nested layout
    @router.SolRoutes::Layout(
    segment="/admin",
    layout=admin_layout,
    children=[
    @router.SolRoutes::Page(path="/", handler=@router.PageHandler(admin_dashboard), title="Admin", meta=[], revalidate=None, cache=None),
    @router.SolRoutes::Page(path="/users", handler=@router.PageHandler(admin_users), title="Users", meta=[], revalidate=None, cache=None),
    ],
    ),
    // Apply middleware
    @router.SolRoutes::WithMiddleware(
    middleware=[@middleware.cors(), @middleware.logger()],
    children=[
    @router.SolRoutes::Get(path="/api/data", handler=@router.ApiHandler(api_data)),
    ],
    ),
    ]
    }

    pub fn config() -> @router.RouterConfig {
    @router.RouterConfig::default()
    .with_default_head(head())
    .with_loader_url("/static/loader.js")

    #SolRoutes Variants

    VariantDescription
    PagePage route (HTML response)
    GetGET API route (JSON response)
    PostPOST API route (JSON response)
    LayoutNested layout group
    WithMiddlewareRoute group with middleware applied

    #Router API Selection

    • @router.register_routes / @router.register_server_routes
      • 入力: @luna/core/routes.Routes
      • 目的: file based routing の割り当てを薄く行う
      • Layout は path prefix のグルーピングとして扱う(layout 関数合成はしない)
    • @router.register_sol_routes
      • 入力: @router.SolRoutes
      • 目的: ServerNode ベースの SSR と layout 合成を含めて扱う
      • SolRoutes::Layout を実際に適用する

    仕様の単一ソース: docs/routing.md Layout の適用ルールと source_path の動的パラメータ形式はこのドキュメントを参照してください。

    Hot Reload 共通設計: docs/hot-reload.md Benchmark 手順と SOL_BENCH_MODE 仕様: docs/benchmarking.md

    #Middleware

    Railway Oriented Programming based middleware system.

    #Basic Usage

    // Middleware composition
    let middleware = @middleware.logger()
    .then(@middleware.cors())
    .then(@middleware.security_headers())

    // Apply to routes
    @router.SolRoutes::WithMiddleware(
    middleware=[middleware],
    children=[...],
    )

    #Built-in Middleware

    MiddlewareDescription
    logger()Request logging
    cors()CORS headers
    csrf()CSRF protection
    security_headers()Security headers
    nosniff()X-Content-Type-Options
    frame_options(value)X-Frame-Options

    #CORS Configuration

    @middleware.cors_with_config(
    @middleware.CorsConfig::default()
    .with_origin_single("https://example.com")
    .with_methods(["GET", "POST"])
    .with_credentials()
    )

    #Security Headers

    @middleware.security_headers_with_config(
    @middleware.SecurityHeadersConfig::default()
    .with_csp("default-src 'self'")
    .with_frame_options("DENY")
    )

    #Middleware Composition

    // Sequential execution (m1 → m2)
    let combined = @middleware.then_(m1, m2)
    // or
    let combined = m1.then(m2)

    // Compose from array
    let pipeline = @middleware.pipeline([m1, m2, m3])

    #Server Actions

    Server-side functions with CSRF protection. See Server Actions README for details.

    #Basic Usage

    // Define action handler
    let submit_handler = @action.ActionHandler(async fn(ctx) {
    let body = ctx.body
    // ... processing
    @action.ActionResult::ok(@js.any({ "success": true }))
    })

    // Register to registry
    pub fn action_registry() -> @action.ActionRegistry {
    @action.ActionRegistry::new(allowed_origins=["http://localhost:7777"])
    .register(@action.ActionDef::new("submit-form", submit_handler))
    }

    #ActionResult Types

    TypeDescription
    Success(data)Success, returns JSON data
    Redirect(url)Client-side redirect (returns JSON with redirect instruction)
    HttpRedirect(url)HTTP redirect (returns 302 with Location header)
    ClientError(status, msg)Client error (4xx)
    ServerError(msg)Server error (5xx)

    #Island Architecture

    #Island Components

    Island is a component shared between SSR and client:

    // app/client/counter.mbt

    pub fn counter(count : @signal.Signal[Int]) -> @luna.Node[CounterAction] {
    div(class="counter", [
    span(class="count-display", [text_of(count)]),
    button(onclick=@luna.action(Increment), [text("+")]),
    button(onclick=@luna.action(Decrement), [text("-")]),
    ])
    }

    #Embedding Islands (Server-side)

    Use ComponentRef-based API for type-safe island embedding. sol generate auto-generates factory functions from client Props types.

    // Auto-generated: app/__gen__/types/types.mbt
    pub struct CounterProps { initial_count : Int } derive(ToJson, FromJson)
    pub fn counter(props : CounterProps, trigger~ : @luna.Trigger) -> @luna.ComponentRef[CounterProps]

    Recommended: @sol.island() with ComponentRef

    // app/server/home.mbt
    let counter_props : @types.CounterProps = { initial_count: 42 }

    // Type-safe island embedding
    @sol.island(
    @types.counter(counter_props),
    [div([button([text("Count: 42")])])], // SSR fallback children
    )

    Alternative: @server_dom.client() (equivalent)

    @server_dom.client(
    @types.counter(counter_props),
    [div([text("Loading...")])],
    )

    Low-level: @sol.island_raw() (string-based, not recommended)

    @sol.island_raw("counter", "/static/counter.js", props_json, children)

    #Hydration Triggers

    TriggerDescription
    LoadImmediately on page load
    IdleOn requestIdleCallback
    VisibleOn IntersectionObserver detection
    Media(query)On media query match
    NoneManual trigger

    #Hydration Module Origin Policy

    loader.js only allows same-origin module URLs by default. Cross-origin hydration modules are blocked unless host/origin is explicitly allowed.

    <script> window.__LUNA_ALLOWED_HOSTS__ = [ "127.0.0.1:3456", "https://cdn.example.com" ]; </script>

    You can also update this allowlist at runtime, then re-scan islands:

    window.__LUNA_SET_ALLOWED_HOSTS__?.(["127.0.0.1"]); window.__LUNA_SCAN__?.();

    #CSR Navigation

    Links with data-sol-link attribute are processed as CSR. Use the sol_link helper:

    sol_link(href="/about", [text("About")])

    Behavior on click:
    1. sol-nav.js intercepts the click
    2. fetch gets new page HTML
    3. Replace <main id="main-content"> content
    4. Update browser history with History API
    5. Hydrate Islands on the new page

    #Streaming SSR

    Streaming async content using ServerNode::async_:

    @server_dom.ServerNode::async_(async fn() {
    let data = fetch_data() // async function call
    div([text(data)])
    })

    register_sol_routes でストリーミング応答を使う場合は、RouterConfig で有効化します(デフォルトは無効)。

    pub fn config() -> @router.RouterConfig {
    @router.RouterConfig::default()
    .with_default_head(head())
    .with_loader_url("/static/loader.js")
    .with_streaming_ssr()
    }

    Note: streaming response uses root_template when __LUNA_MAIN__ exists; otherwise it falls back to built-in page shell.

    #Module Structure

    src/sol/ ├── runtime.mbt # Core API ├── compiler.mbt # Rolldown bindings ├── router/ # Hono router integration │ ├── router.mbt # Route registration │ ├── sol_routes.mbt # SolRoutes definition │ └── fragment.mbt # Fragment response (CSR) ├── middleware/ # Middleware system │ ├── types.mbt # MwContext, MwRequest, MwResponse │ ├── compose.mbt # Composition functions (then, pipeline) │ ├── logger.mbt # Logger middleware │ ├── cors.mbt # CORS middleware │ ├── csrf.mbt # CSRF middleware │ └── security_headers.mbt # Security headers ├── action/ # Server Actions │ ├── action.mbt # ActionHandler, ActionResult │ └── router.mbt # register_actions └── cli/ # CLI tools ├── main.mbt # Entry point ├── new.mbt # sol new ├── dev.mbt # sol dev ├── build.mbt # sol build ├── serve.mbt # sol serve ├── generate.mbt # sol generate └── clean.mbt # sol clean

    #References

    #Sol - SSR-first Web Framework for MoonBit

    Sol is an SSR-first web framework implemented in MoonBit. It adopts Island Architecture, combining server-side rendering with partial client-side hydration.

    #Playground

    examples/sol_app/ is the development playground for Sol framework. Used for prototyping new features, integration testing, and implementation verification.

    # Start development server in playground cd examples/sol_app npm install just sol dev

    #Features

    • Hono Integration: Server based on lightweight Hono
    • Island Architecture: Hydrate only necessary parts
    • Type Safety: Safe routing with MoonBit's type system
    • CLI Tools: Consistent workflow from project creation to build
    • Streaming SSR: Streaming support for async content
    • CSR Navigation: SPA-like page transitions with data-sol-link attribute
    • Middleware: Railway Oriented Programming based middleware system
    • Server Actions: Server-side functions with CSRF protection
    • Nested Layouts: Support for hierarchical layout structures

    #Quick Start

    # Create new project sol new myapp --user yourname cd myapp # Install dependencies npm install # Start development server npm run dev

    #Configuration

    sol.config.ts is the recommended configuration file. It should default-export a plain object.

    export default { islands: ["app/client"], routes: "app/server", output: "app/__gen__", runtime: "node", client_auto_exports: false, }

    If you prefer JSON, sol.config.json is supported. For schema validation in this repo, add:

    { "$schema": "./schemas/sol.config.schema.json" }

    #Project Structure

    Project structure generated by sol new (app server routes live in app/server):

    myapp/ ├── moon.mod.json # MoonBit module definition ├── package.json # npm package definition ├── sol.config.json # Sol config file ├── app/ │ ├── server/ # Server components │ │ ├── moon.pkg │ │ └── routes.mbt # routes() + config() + page functions │ ├── client/ # Client components (Islands) │ │ ├── moon.pkg │ │ └── counter.mbt # render + hydrate functions │ └── __gen__/ # Auto-generated (sol generate) │ ├── client/ # Client exports │ └── server/ # Server entry point └── static/ └── loader.js # Island loader

    The scaffold generated by sol new is defined in src/cli/templates.mbt and matches the layout above.

    #CLI Commands

    #sol new <name> --user <namespace>

    Create a new project.

    sol new myapp --user mizchi # Create mizchi/myapp package sol new myapp --user mizchi --dev # Use local luna path (for development)

    #sol dev

    Start development server. Auto-executes:
    1. sol generate --mode dev - Code generation (outputs to .sol/dev/)
    2. moon build - MoonBit build
    3. rolldown - Client bundle (if manifest exists)
    4. Start server

    sol dev # Default port 7777 sol dev --port 8080 # Specify port sol dev --clean # Clear cache and build

    #sol build

    Production build. Outputs to .sol/prod/.

    sol build # JS target (default) sol build --target wasm # WASM target sol build --skip-bundle # Skip rolldown sol build --skip-generate # Skip generation sol build --clean # Clear cache and build

    #sol serve

    Serve production build. Requires prior sol build.

    sol serve # Default port 7777 sol serve --port 8080 # Specify port

    #sol generate

    Auto-generate code based on sol.config.ts (recommended) or sol.config.json. If both exist, sol.config.ts takes priority.

    Note: Usually sol dev and sol build call this internally, so explicit execution is not needed.

    sol generate # Use sol.config.ts or sol.config.json (default: prod) sol generate --mode dev # Development mode (outputs to .sol/dev/) sol generate --mode prod # Production mode (outputs to .sol/prod/)

    #sol clean

    Delete generated files and cache.

    sol clean # Delete .sol/, app/__gen__/, _build/

    #SolRoutes Definition

    Declarative route definition using @router.SolRoutes:

    // app/server/routes.mbt

    pub fn routes() -> Array[@router.SolRoutes] {
    [
    // Page route
    @router.SolRoutes::Page(
    path="/",
    handler=@router.PageHandler(home_page),
    title="Home",
    meta=[],
    revalidate=None,
    cache=None,
    ),
    // API route (GET)
    @router.SolRoutes::Get(
    path="/api/health",
    handler=@router.ApiHandler(api_health),
    ),
    // API route (POST)
    @router.SolRoutes::Post(
    path="/api/submit",
    handler=@router.ApiHandler(api_submit),
    ),
    // Nested layout
    @router.SolRoutes::Layout(
    segment="/admin",
    layout=admin_layout,
    children=[
    @router.SolRoutes::Page(path="/", handler=@router.PageHandler(admin_dashboard), title="Admin", meta=[], revalidate=None, cache=None),
    @router.SolRoutes::Page(path="/users", handler=@router.PageHandler(admin_users), title="Users", meta=[], revalidate=None, cache=None),
    ],
    ),
    // Apply middleware
    @router.SolRoutes::WithMiddleware(
    middleware=[@middleware.cors(), @middleware.logger()],
    children=[
    @router.SolRoutes::Get(path="/api/data", handler=@router.ApiHandler(api_data)),
    ],
    ),
    ]
    }

    pub fn config() -> @router.RouterConfig {
    @router.RouterConfig::default()
    .with_default_head(head())
    .with_loader_url("/static/loader.js")

    #SolRoutes Variants

    VariantDescription
    PagePage route (HTML response)
    GetGET API route (JSON response)
    PostPOST API route (JSON response)
    LayoutNested layout group
    WithMiddlewareRoute group with middleware applied

    #Router API Selection

    • @router.register_routes / @router.register_server_routes
      • Input: @luna/core/routes.Routes
      • Purpose: Thin file-based routing assignment
      • Layout is treated as path prefix grouping (no layout function composition)
    • @router.register_sol_routes
      • Input: @router.SolRoutes
      • Purpose: Handles ServerNode-based SSR including layout composition
      • Actually applies SolRoutes::Layout

    Details: docs/router-layout-support.md

    #Nested Layouts

    Support for hierarchical layout structures:

    // Admin section layout
    using @server_dom {
    h1,
    nav,
    div,
    text,
    sol_link,
    }

    fn admin_layout(
    props : @router.PageProps,
    content : @server_dom.ServerNode,
    ) -> @server_dom.ServerNode raise {
    @server_dom.ServerNode::sync(@luna.fragment([
    h1([text("Admin Panel")]),
    nav([
    sol_link(href="/admin", [text("Dashboard")]),
    sol_link(href="/admin/users", [text("Users")]),
    ]),
    div(class="admin-content", [content.to_vnode()]),
    ]))
    }

    // Route definition
    // segment="/admin" + path="/" => /admin
    // segment="/admin" + path="/users" => /admin/users
    @router.SolRoutes::Layout(
    segment="/admin", // URL prefix
    layout=admin_layout, // Layout function
    children=[
    // /admin
    @router.SolRoutes::Page(path="/", handler=@router.PageHandler(admin_dashboard), title="Admin", meta=[], revalidate=None, cache=None),
    // /admin/users
    @router.SolRoutes::Page(path="/users", handler=@router.PageHandler(admin_users), title="Users", meta=[], revalidate=None, cache=None),
    ],
    )

    #Middleware

    Railway Oriented Programming based middleware system.

    #Basic Usage

    // Middleware composition
    let middleware = @middleware.logger()
    .then(@middleware.cors())
    .then(@middleware.security_headers())

    // Apply to routes
    @router.SolRoutes::WithMiddleware(
    middleware=[middleware],
    children=[...],
    )

    #Built-in Middleware

    MiddlewareDescription
    logger()Request logging
    cors()CORS headers
    csrf()CSRF protection
    security_headers()Security headers
    nosniff()X-Content-Type-Options
    frame_options(value)X-Frame-Options

    #CORS Configuration

    @middleware.cors_with_config(
    @middleware.CorsConfig::default()
    .with_origin_single("https://example.com")
    .with_methods(["GET", "POST"])
    .with_credentials()
    )

    #Security Headers

    @middleware.security_headers_with_config(
    @middleware.SecurityHeadersConfig::default()
    .with_csp("default-src 'self'")
    .with_frame_options("DENY")
    )

    #Middleware Composition

    // Sequential execution (m1 → m2)
    let combined = @middleware.then_(m1, m2)
    // or
    let combined = m1.then(m2)

    // Compose from array
    let pipeline = @middleware.pipeline([m1, m2, m3])

    #Server Actions

    Server-side functions with CSRF protection. See Server Actions README for details.

    #Basic Usage

    // Define action handler
    let submit_handler = @action.ActionHandler(async fn(ctx) {
    let body = ctx.body
    // ... processing
    @action.ActionResult::ok(@js.any({ "success": true }))
    })

    // Register to registry
    pub fn action_registry() -> @action.ActionRegistry {
    @action.ActionRegistry::new(allowed_origins=["http://localhost:7777"])
    .register(@action.ActionDef::new("submit-form", submit_handler))
    }

    #ActionResult Types

    TypeDescription
    Success(data)Success, returns JSON data
    Redirect(url)Client-side redirect (returns JSON with redirect instruction)
    HttpRedirect(url)HTTP redirect (returns 302 with Location header)
    ClientError(status, msg)Client error (4xx)
    ServerError(msg)Server error (5xx)

    #Island Architecture

    #Island Components

    Island is a component shared between SSR and client:

    // app/client/counter.mbt

    pub fn counter(count : @signal.Signal[Int]) -> @luna.Node[CounterAction] {
    div(class="counter", [
    span(class="count-display", [text_of(count)]),
    button(onclick=@luna.action(Increment), [text("+")]),
    button(onclick=@luna.action(Decrement), [text("-")]),
    ])
    }

    #Embedding Islands (Server-side)

    Use ComponentRef-based API for type-safe island embedding. sol generate auto-generates factory functions from client Props types.

    // Auto-generated: app/__gen__/types/types.mbt
    pub struct CounterProps { initial_count : Int } derive(ToJson, FromJson)
    pub fn counter(props : CounterProps, trigger~ : @luna.Trigger) -> @luna.ComponentRef[CounterProps]

    Recommended: @sol.island() with ComponentRef

    // app/server/home.mbt
    let counter_props : @types.CounterProps = { initial_count: 42 }

    @sol.island(
    @types.counter(counter_props),
    [div([button([text("Count: 42")])])], // SSR fallback children
    )

    Alternative: @server_dom.client() (equivalent)

    @server_dom.client(
    @types.counter(counter_props),
    [div([text("Loading...")])],
    )

    Low-level: @sol.island_raw() (string-based, not recommended)

    @sol.island_raw("counter", "/static/counter.js", props_json, children)

    #Hydration Triggers

    TriggerDescription
    LoadImmediately on page load
    IdleOn requestIdleCallback
    VisibleOn IntersectionObserver detection
    Media(query)On media query match
    NoneManual trigger

    #CSR Navigation

    Links with data-sol-link attribute are processed as CSR. Use the sol_link helper:

    sol_link(href="/about", [text("About")])

    Behavior on click:
    1. sol-nav.js intercepts the click
    2. fetch gets new page HTML
    3. Replace <main id="main-content"> content
    4. Update browser history with History API
    5. Hydrate Islands on the new page

    #Streaming SSR

    Streaming async content using ServerNode::async_:

    @server_dom.ServerNode::async_(async fn() {
    let data = fetch_data() // async function call
    div([text(data)])
    })

    To use streaming responses with register_sol_routes, enable it in RouterConfig (disabled by default).

    pub fn config() -> @router.RouterConfig {
    @router.RouterConfig::default()
    .with_default_head(head())
    .with_loader_url("/static/loader.js")
    .with_streaming_ssr()
    }

    Note: streaming response uses root_template when __LUNA_MAIN__ exists; otherwise it falls back to built-in page shell.

    #Module Structure

    src/sol/ ├── runtime.mbt # Core API ├── compiler.mbt # Rolldown bindings ├── router/ # Hono router integration │ ├── router.mbt # Route registration │ ├── sol_routes.mbt # SolRoutes definition │ └── fragment.mbt # Fragment response (CSR) ├── middleware/ # Middleware system │ ├── types.mbt # MwContext, MwRequest, MwResponse │ ├── compose.mbt # Composition functions (then, pipeline) │ ├── logger.mbt # Logger middleware │ ├── cors.mbt # CORS middleware │ ├── csrf.mbt # CSRF middleware │ └── security_headers.mbt # Security headers ├── action/ # Server Actions │ ├── action.mbt # ActionHandler, ActionResult │ └── router.mbt # register_actions └── cli/ # CLI tools ├── main.mbt # Entry point ├── new.mbt # sol new ├── dev.mbt # sol dev ├── build.mbt # sol build ├── serve.mbt # sol serve ├── generate.mbt # sol generate └── clean.mbt # sol clean

    #References

    BundleConfig

    pub struct BundleConfig {
    input : Array[(String, String)]
    output_dir : String
    format : OutputFormat
    entry_file_names : String
    chunk_file_names : String
    minify : Bool
    sourcemap : Bool
    }

    Bundle configuration

    BundleConfig::new

    fn BundleConfig::new(input : Array[(String, String)], output_dir : String) -> BundleConfig

    Create default bundle configuration

    BundleConfig::with_format

    fn BundleConfig::with_format(self : BundleConfig, format : OutputFormat) -> BundleConfig

    Builder: set output format

    BundleConfig::with_minify

    fn BundleConfig::with_minify(self : BundleConfig, minify : Bool) -> BundleConfig

    Builder: set minification

    BundleConfig::with_sourcemap

    fn BundleConfig::with_sourcemap(self : BundleConfig, sourcemap : Bool) -> BundleConfig

    Builder: set source map generation

    BundleResult

    pub struct BundleResult {
    success : Bool
    output : Array[String]
    errors : Array[String]
    }

    Bundle result with success/failure info

    IslandConfig

    pub struct IslandConfig[E] {
    id : String
    script_url : String
    trigger :
    TriggerType

    state :
    StateConfig

    content :
    Node
    [E, String]?
    ssr_html : String?
    }

    Island configuration

    IslandConfig::new

    fn[E] IslandConfig::new(id : String, script_url : String) -> IslandConfig[E]

    Create a new island configuration

    IslandConfig::with_content

    fn[E] IslandConfig::with_content(self : IslandConfig[E], node :
    Node
    [E, String]) -> IslandConfig[E]

    Builder: set SSR content from VNode

    IslandConfig::with_ssr_html

    fn[E] IslandConfig::with_ssr_html(self : IslandConfig[E], html : String) -> IslandConfig[E]

    Builder: set SSR content from raw HTML string

    IslandConfig::with_state

    fn[E] IslandConfig::with_state(self : IslandConfig[E], json : String) -> IslandConfig[E]

    Builder: set inline state from JSON

    IslandConfig::with_state_ref

    fn[E] IslandConfig::with_state_ref(self : IslandConfig[E], script_id : String) -> IslandConfig[E]

    Builder: set state from script reference

    IslandConfig::with_state_url

    fn[E] IslandConfig::with_state_url(self : IslandConfig[E], url : String) -> IslandConfig[E]

    Builder: set state from URL

    IslandConfig::with_trigger

    fn[E] IslandConfig::with_trigger(self : IslandConfig[E], trigger :
    TriggerType
    ) -> IslandConfig[E]

    Builder: set trigger type

    IslandManifest

    pub struct IslandManifest {
    islands : Array[IslandManifestEntry]
    output_dir : String
    }

    Island manifest (written by generate, read by compiler)

    IslandManifestEntry

    pub struct IslandManifestEntry {
    name : String
    entry_path : String
    }

    Island manifest entry

    OutputFormat

    pub enum OutputFormat {
    Esm
    }

    Output format for bundled code

    OutputFormat::to_string

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

    RolldownChunk

    #external
    pub type RolldownChunk

    Rolldown output chunk

    RolldownChunk::as_any

    RolldownChunk::code

    fn RolldownChunk::code(self : RolldownChunk) -> String

    Get the chunk code

    RolldownChunk::file_name

    fn RolldownChunk::file_name(self : RolldownChunk) -> String

    Get the file name

    RolldownChunk::is_entry

    fn RolldownChunk::is_entry(self : RolldownChunk) -> Bool

    Check if this is an entry chunk

    RolldownOutput

    #external
    pub type RolldownOutput

    Rolldown build output

    RolldownOutput::as_any

    RolldownOutput::output

    Get output chunks from build result

    StaticDirServeConfig

    pub(all) struct StaticDirServeConfig {
    path_prefix : String
    local_dir : String
    }

    Configuration for serving a static directory (SSG output)

    StaticDirServeConfig::new

    fn StaticDirServeConfig::new(path_prefix : String, _source_dir : String) -> StaticDirServeConfig

    Create default StaticDirServeConfig

    StaticFileConfig

    pub struct StaticFileConfig {
    path_prefix : String
    local_dir : String
    mappings : Array[(String, String)]
    cache_control : String
    }

    Static file configuration

    StaticFileConfig::default

    Create default static file config for Luna apps

    StaticFileConfig::dev

    Create static file config for development (no caching)

    StaticFileConfig::with_mapping

    fn StaticFileConfig::with_mapping(self : StaticFileConfig, url_filename : String, local_path : String) -> StaticFileConfig

    Builder: add file mapping

    build

    async fn build(config : BundleConfig) -> RolldownOutput

    Build with rolldown (async)

    build_from_manifest

    async fn build_from_manifest(manifest : IslandManifest) -> RolldownOutput

    Build islands from manifest

    build_from_manifest_file

    fn build_from_manifest_file(manifest_path : String, cwd : String, on_success : (Array[String]) -> Unit, on_error : (String) -> Unit) -> Unit

    Build islands from manifest file path (convenience for CLI) Returns a promise that resolves when build is complete cwd: working directory where rolldown should be resolved from

    bundle_islands

    async fn bundle_islands(islands : Array[(String, String)], output_dir : String) -> RolldownOutput

    Build islands from configuration Takes a list of (island_name, entry_path) pairs and outputs to static directory

    bundle_islands_prod

    async fn bundle_islands_prod(islands : Array[(String, String)], output_dir : String) -> RolldownOutput

    Build islands with minification for production

    bundle_islands_result

    async fn bundle_islands_result(islands : Array[(String, String)], output_dir : String) -> BundleResult

    Build islands and return simplified result

    default_loader_url

    let default_loader_url : String

    Default loader script URL

    default_wc_loader_url

    let default_wc_loader_url : String

    Default wc-loader script URL

    generate_island_manifest

    fn generate_island_manifest(islands : Array[(String, String)], output_dir : String) -> String

    Generate island manifest JSON

    generate_state_script

    fn generate_state_script(id : String, json : String) -> String

    Generate state script tag for external state

    island

    fn[T : ToJson, E] island(cref :
    ComponentRef
    [T], children : Array[
    Node
    [E, String]]) ->
    Node
    [E, String]

    Create an Island VNode from a ComponentRef (type-safe, recommended)

    Example:
    @sol.island(
    @types.counter({ initial_count: 42 }),
    [div([button([text("Count: 42")])])],
    )

    island_page

    fn[E] island_page(app :
    Server
    , path : String, render_fn : (
    Context
    ) -> Array[IslandConfig[E]], title? : String, head? : String, loader_url? : String) -> Unit

    Register an island page route

    island_raw

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

    Create an Island VNode from raw strings (low-level API) Prefer island(cref, children) with ComponentRef for type safety

    island_with

    fn[T : ToJson, E] island_with(cref :
    ComponentRef
    [T], render : () ->
    Node
    [E, String]) ->
    Node
    [E, String]

    Create an Island VNode from a ComponentRef with a render function for children

    island_with_raw

    fn[E] island_with_raw(id : String, url : String, state : String, render : () ->
    Node
    [E, String], trigger? :
    TriggerType
    ) ->
    Node
    [E, String]

    Create an Island VNode with a render function (low-level API) Prefer island_with(cref, render) with ComponentRef for type safety

    json_array_to_any

    fn json_array_to_any(arr : Array[Json]) ->
    Any

    Convert MoonBit Json array to JavaScript Any value

    json_obj

    fn json_obj(pairs : Array[(String,
    Any
    )]) ->
    Any

    Create a JSON object for API responses

    json_to_any

    fn json_to_any(json : Json) ->
    Any

    Convert MoonBit Json to JavaScript Any value

    page

    fn[E] page(app :
    Server
    , path : String, render_fn : (
    Context
    ) ->
    Node
    [E, String], title? : String, head? : String, hydration? : Bool, loader_url? : String) -> Unit

    Register a page route that renders a VNode Use hydration=true for pages with Islands (adds loader script and hydration markers)

    parse_island_manifest

    fn parse_island_manifest(json_str : String) -> IslandManifest?

    Parse island manifest from JSON string

    render_fragment

    fn[E] render_fragment(node :
    Node
    [E, String]) -> String

    Render a VNode to HTML string without document wrapper (for partial/fragment responses)

    render_island

    fn[E] render_island(config : IslandConfig[E]) -> String

    Render an island to HTML string

    render_island_page

    fn render_island_page(islands : Array[String], title? : String, head? : String, loader_url? : String) -> String

    Render a page with islands Includes the loader script automatically

    render_island_with_state_script

    fn[E] render_island_with_state_script(config : IslandConfig[E], state_json : String) -> String

    Render island with separate state script (for large state)

    render_node

    fn[E] render_node(node :
    Node
    [E, String], hydration? : Bool) -> String

    Render a VNode to HTML string Use hydration=true to enable hydration markers

    render_page

    fn[E] render_page(node :
    Node
    [E, String], title? : String, head? : String, hydration? : Bool) -> String

    Render a VNode to HTML string with full document wrapper Use hydration=true to enable hydration markers (for pages with Islands)

    render_page_with_preloads

    fn[E] render_page_with_preloads(node :
    Node
    [E, String], title? : String, head? : String, hydration? : Bool) -> String

    Render a page with automatic modulepreload tags for islands

    render_streaming_page

    fn render_streaming_page(shell :
    Node
    [Unit, String], title? : String, head? : String, loader_url? : String) ->
    Response

    Render a page with streaming SSR and async boundaries (VAsync nodes) Returns a ReadableStream for HTTP response VAsync nodes in the tree will be rendered with fallback first, then resolved content will be streamed as replacement scripts

    render_with_island_preloads

    fn[E] render_with_island_preloads(node :
    Node
    [E, String]) ->
    SSRResult

    run

    fn run(configure : (
    Server
    ) -> Unit, port? : Int) -> Unit

    Run a Sol application with standard configuration This is the recommended way to start a Sol server.

    Example:
    fn main {
    @sol.run(fn(app) { @sol.page(app, "/", home_page) |> @sol.serve_static() })
    }

    run_app

    fn run_app(create_and_configure : () ->
    Server
    , port? : Int) -> Unit

    Run a Sol application that creates its own app instance This is used for generated configure_app() functions that take no parameters.

    Example:
    fn main {
    @sol.run_app(configure_app)
    }

    serve_static

    fn serve_static(app :
    Server
    , config? : StaticFileConfig) -> Unit

    Register static file serving middleware

    serve_static_dir

    fn serve_static_dir(app :
    Server
    , config : StaticDirServeConfig) -> Unit

    Register a static directory for serving SSG output This serves HTML files from .sol/prod/static/{path_prefix}

    streaming_page

    fn streaming_page(app :
    Server
    , path : String, render_fn : (
    Context
    ) ->
    Node
    [Unit, String], title? : String, head? : String, loader_url? : String) -> Unit

    Register a streaming page route with async support VAsync nodes in the rendered tree will be handled automatically Note: This uses ReadableStream internally for proper streaming support

    wc_island

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

    Create a Web Components Island VNode Uses Declarative Shadow DOM for SSR and wc-loader for hydration

    wc_island_with

    fn[E] wc_island_with(name : String, url : String, styles : String, state : String, render : () ->
    Node
    [E, String], trigger? :
    TriggerType
    ) ->
    Node
    [E, String]

    Create a Web Components Island VNode with a render function for children

    wc_page

    fn[E] wc_page(app :
    Server
    , path : String, render_fn : (
    Context
    ) ->
    Node
    [E, String], title? : String, head? : String, wc_loader_url? : String) -> Unit

    Register a page route with Web Components islands Uses wc-loader for hydration