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.22.1
    License
    MIT
    Last updated
    5 months ago
    Downloads
    3K

    #mizchi/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. Static-site generation (SSG) lives in the sibling mizchi/astra package and mounts on the same Mars Server.

    #Install

    Library:

    // moon.mod.json { "deps": { "mizchi/sol": "0.21.1", "mizchi/luna": "0.21.0" } }

    CLI (binary):

    moon install mizchi/sol/cmd/sol # → $MOON_HOME/bin/sol # or via npm pnpm add -g @luna_ui/sol # npm wrapper 0.20.0

    #Playground

    examples/sol_app/ is the development playground (run from the repo root after moon build --target js --release):

    cd sol/examples/sol_app pnpm install sol dev

    #Cloudflare Worker Composition

    When sol.config.json uses "runtime": "cloudflare", sol build emits .sol/prod/server/main.js as a Worker-safe module with a stable default export. Existing Workers can route some requests to Sol and keep their own API handlers in the same Worker. New apps can start with this wiring already in place:

    sol new myapp --user mizchi --cloudflare cd myapp pnpm install pnpm dev

    The generated worker.entry.mjs is the public composition point:

    import solApp from "./.sol/prod/server/main.js"; import apiWorker from "./worker-api.mjs"; export default { fetch(request, env, ctx) { const path = new URL(request.url).pathname; return path.startsWith("/api/") ? apiWorker.fetch(request, env, ctx) : solApp.fetch(request, env, ctx); }, };

    Keep Sol UI routes on solApp.fetch(...), and expose custom MoonBit Worker handlers behind a stable wrapper such as worker-api.mjs instead of importing Sol's private _build/js/... output or waiting on globalThis.__SOL_APP__.

    #Runtime Asset Sync (Contributors)

    Runtime assets (loader.js, wc-loader.js, sol-nav.js, lib.js) now live in the sibling mizchi/astra package under astra/src/assets/scripts/. Sol's runtime resolves them via .mooncakes/mizchi/astra/src/assets/scripts/<name> at request time.

    When you update the Luna loader runtime, sync into astra (not sol):

    just -f ../astra/justfile sync-luna-assets ../luna.mbt

    #Static Site Generation

    SSG capability moved to the dedicated mizchi/astra package in 0.16.0. To build a docs / blog site, install mizchi/astra and use astra dev / astra build. See astra/README.md and astra/examples/sol_docs/ for a working starter.

    #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 Worker auth guide: docs/worker-auth.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", // Defaults preserve the generated Sol pipeline. serverEntry: "auto", // "auto" | "generated" | "user" clientBundle: "auto", // "auto" | "external" 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"], }, ], }

    serverEntry and clientBundle split ownership of generated intermediates:

    • serverEntry: "auto" keeps the historical behavior: app/server/main.mbt means user-managed server entry; otherwise Sol generates app/__gen__/server.
    • serverEntry: "user" makes the server entry explicit and skips app/__gen__/server even if main.mbt is not present yet.
    • clientBundle: "external" makes island discovery and bundling user-owned. Sol skips app/__gen__/client, .sol/*/client, and .sol/*/manifest.json. It still generates app/__gen__/types when it finds Props structs, so the server can keep a typed contract to frontend props while explicitly passing URLs served by your own bundler.

    External bundles can still use SSR modulepreload without making Sol read or write the client build graph. Load the bundler manifest in your app entry and pass the JSON to RouterConfig:

    let client_manifest =
    #|{
    #| "app/client/counter.ts": {
    #| "file": "assets/counter-abc123.js",
    #| "imports": ["assets/runtime-def456.js"]
    #| }
    #|}

    pub fn config() -> @sol.RouterConfig {
    @sol.RouterConfig::default()
    .with_client_manifest_json(client_manifest, base_url="/static/")
    }

    The manifest may be keyed by source entry or emitted file, matching Vite/Rolldown-style output. Island refs should point at the emitted URL, such as /static/assets/counter-abc123.js; Sol only uses the supplied JSON to emit the shared chunk <link rel="modulepreload"> tags during SSR.

    For external bundles, generated type helpers use URL injection:

    @sol.island(
    @types.counter_at(
    "/static/assets/counter-abc123.js",
    { initial_count: 42 },
    ),
    [counter_ssr()],
    )

    This keeps CounterProps checked between the server and frontend without letting Sol own the client bundle output path. When generated contracts exist, Sol also writes app/__gen__/types/types.d.ts so TypeScript frontend code can import the same props, route path, route params, route query params, action payload/response, and API request/response contracts without depending on Sol's bundler.

    For TypeScript-owned frontend contracts, contractTs can feed the same generated type helpers directly from TS interfaces without writing an intermediate manifest file:

    export default { clientBundle: "external", contractTs: [ { input: "app/client/props.ts", props: "CounterProps", package: "app/client", clientUrl: "/static/assets/counter-abc123.js", }, ], }

    contractManifest is still supported for fully explicit JSON contracts; when both fields are present, the explicit manifest path takes precedence.

    contractManifest route entries may declare query parameters, and action/API entries may declare request/response shapes. Sol emits both MoonBit structs and TypeScript declarations from those shapes:

    { "routes": [ { "path": "/api/items/:id", "query": [ { "name": "token" }, { "name": "include", "optional": true } ] } ], "actions": [ { "id": "submit-contact", "request": { "name": "SubmitContactRequest", "fields": [{ "name": "email", "type": "String" }] }, "response": { "name": "SubmitContactResponse", "fields": [{ "name": "ok", "type": "Bool" }] } } ], "apis": [ { "method": "POST", "path": "/api/items/:id", "request": { "name": "CreateItemRequest", "fields": [{ "name": "title", "type": "String" }] }, "response": { "name": "ItemResponse", "fields": [{ "name": "id", "type": "String" }] } } ] }

    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 ├── worker.entry.mjs # Cloudflare starter compose point (--cloudflare) ├── wrangler.toml # Cloudflare Workers config (--cloudflare) ├── app/ │ ├── server/ # Server components │ │ ├── moon.pkg │ │ └── routes.mbt # routes() + config() + page functions │ ├── client/ # Client components (Islands) │ │ ├── moon.pkg │ │ ├── counter.mbt # render + hydrate functions │ │ └── api_tools.mbt # copy/status/format starter controls │ └── __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 --cloudflare 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; skipped with clientBundle: "external")
    4. Start server

    For runtime: "cloudflare", sol dev starts wrangler dev instead of the Node adapter. If the project has wrangler.dev.toml, that file is used; otherwise Sol writes a fallback .sol/dev/wrangler.toml that runs .sol/dev/server/main.js.

    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 doctor

    Diagnose local project wiring before build/serve/deploy. It checks sol.config.json, route files, generated outputs, and whether the selected runtime uses the matching adapter package (sol_adapter_node or sol_adapter_cloudflare).

    sol doctor # Print warnings/errors sol doctor --strict # Treat warnings as failures

    #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.

    For a user-owned server entry and external client bundle, sol generate writes the runtime server wrapper under .sol/<mode>/server/, built-in Sol assets, and app/__gen__/types when Props contracts exist. It does not create app/__gen__/server, app/__gen__/client, or client bundle manifests.

    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)
    RawGet / RawPost / RawPut / RawDelete / RawPatchRaw Web Response route
    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 only (no layout function composition)
    • @router.register_sol_routes
      • Input: @router.SolRoutes
      • Purpose: Handles ServerNode-based SSR including layout composition
      • Actually applies SolRoutes::Layout

    Single source of truth: docs/routing.md Refer to this document for Layout application rules, raw response routes, route/component asset attachment, and source_path dynamic parameter format.

    Route and component assets are first-class Sol metadata. Use RouterConfig.with_style(...) / .with_client_script(...) for app-wide assets, page(..., styles=[...], scripts=[...]) or assets(...) for route-scoped assets, and with_style(...) / with_assets(...) for component-local CSS and client modules. Local paths are emitted under /static/ and deduplicated deterministically.

    Hot Reload design: docs/hot-reload.md Benchmark procedures and SOL_BENCH_MODE specification: docs/benchmarking.md

    #Route Ownership And Errors

    Sol is allowed to own only part of a deployed Worker. In mixed apps, keep ownership explicit in the host entry. route_ownership_manifest(routes) gives host routers a typed Sol ownership contract for pages, JSON APIs, raw responses, static assets, and reserved framework paths.

    The same manifest can be used as an optional JSON API client source: manifest.api_client_manifest_json() returns method/path/param metadata, and manifest.api_client_typescript() emits browser/Worker fetch helpers with a typed SolApiResult<T> success/error shape.

    import solApp from "./.sol/prod/server/main.js"; export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { return apiFetch(request, env, ctx); } return solApp.fetch(request, env, ctx); }, };

    Recommended split:

    • Sol page routes own HTML responses, layout composition, fragments, and framework assets such as /__sol__/* and /static/*.
    • Sol API routes own JSON responses declared with api_get, api_post, api_put, api_delete, or api_patch.
    • Raw Sol routes own exact Web Response values declared with raw_get, raw_post, raw_put, raw_delete, or raw_patch.
    • Host Worker routes own platform-specific passthroughs such as service binding proxies and auth callbacks.

    Error handling follows the owning layer. Sol page handler/layout failures return HTML/text 500 responses. Sol API handler failures return JSON 500 responses. Raw handler failures return plain text 500; otherwise the raw Response body/status are not wrapped. Middleware response headers are merged into raw responses without overwriting headers that the raw Response already set. A route that is intentionally outside Sol should be matched before solApp.fetch; if neither the host nor Sol owns it, return the host fallback 404 from the Worker entry.

    #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")
    )

    #Route-Level Auth For Worker Apps

    Middleware can protect a route group and may read request headers through the Mars context. To halt, send a response from the middleware; to continue, return without sending.

    fn require_admin() -> @middleware.Middleware {
    @middleware.Middleware(@mars.Handler(async fn(ctx) {
    match ctx.header("Authorization") {
    Some(token) if token.has_prefix("Bearer ") => ()
    _ => ctx.json({ "error": "unauthorized" }.to_json(), status=401)
    }
    }))
    }

    pub fn routes() -> Array[@sol.SolRoutes] {
    [
    @sol.route("/", home),
    @sol.with_mw([require_admin()], [
    @sol.route("/admin", admin_page),
    @sol.api_post("/api/items", create_item),
    ]),
    ]
    }

    For Cloudflare bindings, keep the env-specific routing in the host Worker entry until a typed binding contract is available. Pass host-owned requests to the Worker API layer first, then fall back to Sol for UI routes.

    #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

    struct SubmitRequest {
    value : String
    } derive(FromJson)

    struct SubmitResponse {
    success : Bool
    } derive(ToJson)

    // Define action handler
    let submit_handler = @action.ActionHandler(async fn(ctx) {
    let req : SubmitRequest = match ctx.decode_json() {
    Some(req) => req
    None => return @action.ActionResult::bad_request("Invalid JSON payload")
    }
    // ... processing with req
    @action.ActionResult::json(SubmitResponse::{ success: true })
    })

    // Register to registry
    pub fn action_registry() -> @action.ActionRegistry {
    @action.ActionRegistry::new(allowed_origins=["http://localhost:7777"])
    .register(@action.ActionDef::from_key(@types.action_submit(), 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)])
    })

    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

    #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

    For runtime: "cloudflare", sol dev starts wrangler dev instead of the Node adapter. If the project has wrangler.dev.toml, that file is used; otherwise Sol writes a fallback .sol/dev/wrangler.toml that runs .sol/dev/server/main.js.

    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/routing.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

    struct SubmitFormRequest {
    value : String
    } derive(FromJson)

    struct SubmitFormResponse {
    success : Bool
    } derive(ToJson)

    // Define action handler
    let submit_form_handler = @action.ActionHandler(async fn(ctx) {
    let req : SubmitFormRequest = match ctx.decode_json() {
    Some(req) => req
    None => return @action.ActionResult::bad_request("Invalid JSON payload")
    }
    // ... processing with req
    @action.ActionResult::json(SubmitFormResponse::{ success: true })
    })

    // Register to registry
    pub fn action_registry() -> @action.ActionRegistry {
    @action.ActionRegistry::new(allowed_origins=["http://localhost:7777"])
    .register(
    @action.ActionDef::from_key(
    @types.action_submit_form(),
    submit_form_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

    ActionRegistry

    Registry for server actions

    ApiHandler

    API handler type - async function that receives PageProps and returns Json response. Uses MoonBit's built-in Json type for wasm/native compatibility.

    ApiResponse

    Type-safe API response enum

    Use with .to_json() to convert to Json for handler return.

    CacheStrategy

    Cache strategy for CSR navigation Controls how the client handles navigation to the same page

    PageHandler

    Page handler type - async function that receives PageProps and returns ServerNode

    PageProps

    Props passed to page components

    RawHandler

    Raw handler type - async function that returns a Web Response directly. Use this for streaming, redirects, binary bodies, and Worker pass-throughs.

    RouteManifestEntry

    RouteManifestMatchKind

    RouteManifestMethod

    RouteManifestOwner

    RouteOwnershipManifest

    RouterConfig

    Router configuration for customizing routing behavior

    SolRoutes

    Sol route definition with typed handlers

    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::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::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

    api_delete

    fn[T : ToJson] api_delete(path : String, handler : async (
    PageProps
    ) -> T) ->
    SolRoutes

    Create a DELETE API route

    api_get

    fn[T : ToJson] api_get(path : String, handler : async (
    PageProps
    ) -> T) ->
    SolRoutes

    Create a GET API route

    Example:
    api_get("/api/users/:id", get_user)

    api_patch

    fn[T : ToJson] api_patch(path : String, handler : async (
    PageProps
    ) -> T) ->
    SolRoutes

    Create a PATCH API route

    api_post

    fn[T : ToJson] api_post(path : String, handler : async (
    PageProps
    ) -> T) ->
    SolRoutes

    Create a POST API route

    Example:
    api_post("/api/users", create_user)

    api_put

    fn[T : ToJson] api_put(path : String, handler : async (
    PageProps
    ) -> T) ->
    SolRoutes

    Create a PUT API route

    assets

    fn assets(children : Array[
    SolRoutes
    ], styles? : Array[String], scripts? : Array[String]) ->
    SolRoutes

    Attach stylesheet and client script assets to a route group.

    bad_request

    fn bad_request(message : String) ->
    ApiResponse

    Create a bad request (400) response

    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

    error_response

    fn error_response(message : String, status? : Int) ->
    ApiResponse

    Create an error response

    forbidden

    fn forbidden(message? : String) ->
    ApiResponse

    Create a forbidden (403) response

    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

    nodes

    Create a sync ServerNode from an array of nodes

    Example:
    nodes([
    @element.h1([@element.text("Hello")]),
    @element.p([@element.text("World")]),
    ])

    not_found_response

    fn not_found_response(message? : String) ->
    ApiResponse

    Create a 404 not found response

    Create a JSON response from key-value pairs

    Example:
    ok([("status", "ok".to_json())]).to_json()

    optional

    fn optional(props :
    PageProps
    , name : String, default : String) -> String

    Extract an optional path parameter with default value (delegates to RouteParams)

    optional_query

    fn optional_query(props :
    PageProps
    , name : String, default : String) -> String

    Extract an optional query parameter with default value (delegates to RouteParams)

    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

    raw_delete

    Create a DELETE route that returns a Web Response directly.

    raw_get

    Create a GET route that returns a Web Response directly.

    raw_patch

    Create a PATCH route that returns a Web Response directly.

    raw_post

    Create a POST route that returns a Web Response directly.

    raw_put

    Create a PUT route that returns a Web Response directly.

    redirect_permanent_to

    fn redirect_permanent_to(url : String) ->
    ApiResponse

    Create a permanent redirect (301)

    redirect_to

    fn redirect_to(url : String, status? : Int) ->
    ApiResponse

    Create a redirect response (default 302)

    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

    require

    fn require(props :
    PageProps
    , name : String) -> Result[String,
    ApiResponse
    ]

    Extract a required path parameter (delegates to RouteParams)

    require_int

    fn require_int(props :
    PageProps
    , name : String) -> Result[Int,
    ApiResponse
    ]

    Extract and parse an integer parameter (delegates to RouteParams)

    require_query

    fn require_query(props :
    PageProps
    , name : String) -> Result[String,
    ApiResponse
    ]

    Extract a required query parameter (delegates to RouteParams)

    route

    fn route(path : String, handler : async (
    PageProps
    ) ->
    ServerNode
    , title? : String, meta? : Array[(String, String)], revalidate? : Int?, cache? :
    CacheStrategy
    ?, styles? : Array[String], scripts? : Array[String]) ->
    SolRoutes

    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_default_favicon

    fn serve_default_favicon(app :
    Server
    ) -> Unit

    serve_sol_assets

    fn serve_sol_assets(app :
    Server
    ) -> Unit

    Register /sol/* route for serving built-in framework assets. In prod mode, lazily preloads assets into memory on first request. In dev mode, resolves from source paths on each request.

    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}

    server_island

    fn[E] server_island(id~ : String, props? : Json?, fallback? :
    Node
    [E, String], revalidate? : Int?) ->
    Node
    [E, String]

    Create a Server Island VNode that renders a placeholder with luna:defer attribute. The client-side loader will fetch the actual content from /_sol/defer/:id.

    Example:
    @sol.server_island(
    id="user-profile",
    props=Json::Object({ "userId": Json::String("123") }),
    fallback=div([text("Loading...")]),
    )

    server_island_typed

    fn[T : ToJson, E] server_island_typed(id~ : String, props~ : T, fallback? :
    Node
    [E, String], revalidate? : Int?) ->
    Node
    [E, String]

    Create a Server Island VNode with typed props (convenience wrapper)

    server_nodes

    Create a ServerNode group from server components.

    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

    unauthorized

    fn unauthorized(message? : String) ->
    ApiResponse

    Create an unauthorized (401) response

    wc_island_raw

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

    Create a Web Components Island VNode from raw strings (low-level API) Prefer island(cref, children) with ComponentRef (wc=true) for type safety

    wc_island_with_raw

    fn[E] wc_island_with_raw(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 (low-level API) Prefer island_with(cref, render) with ComponentRef (wc=true) for type safety

    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

    with_assets

    fn with_assets(content :
    Node
    [Unit, String], styles? : Array[String], scripts? : Array[String]) ->
    ServerNode

    Attach component-local stylesheet and client script assets to a node.

    with_mw

    Apply middleware to a group of routes

    Example:
    with_mw([@mw.logger(), @mw.cors()], [
    api_get("/api/data", get_data),
    ])

    with_script

    fn with_script(script : String, content :
    Node
    [Unit, String]) ->
    ServerNode

    Attach a component-local client script to a node.

    with_style

    fn with_style(style : String, content :
    Node
    [Unit, String]) ->
    ServerNode

    Attach a component-local stylesheet to a node.

    wrap

    Create a layout wrapper

    Example:
    wrap("/admin", admin_layout, [
    page("/", admin_dashboard),
    page("/settings", admin_settings),
    ])