=====================
Starlette Integration
=====================

Overview
========

The Starlette bridge provides a lightweight way to serve wilco components from
pure Starlette applications. It returns a list of Route objects that can be
mounted on any Starlette application.

Installation
============

Install wilco with Starlette support using the optional extra:

.. code-block:: bash

    pip install wilco[starlette]

This installs wilco with Starlette (>= 0.40.0). For development, you'll also
want uvicorn:

.. code-block:: bash

    pip install wilco[starlette] uvicorn

Quick start
===========

Here's a minimal example that serves components from a directory:

.. code-block:: python

    from pathlib import Path
    from starlette.applications import Starlette
    from starlette.routing import Mount
    from wilco import ComponentRegistry
    from wilco.bridges.starlette import create_routes

    # Create a registry pointing to your components
    registry = ComponentRegistry(Path("./components"))

    # Create wilco routes
    routes = create_routes(registry)

    # Mount on your Starlette app
    app = Starlette(routes=[
        Mount("/api", routes=routes),
    ])

    if __name__ == "__main__":
        import uvicorn
        uvicorn.run(app, host="0.0.0.0", port=8400)

This creates three endpoints:

- ``GET /api/bundles`` - List available components
- ``GET /api/bundles/{name}.js`` - Get bundled JavaScript
- ``GET /api/bundles/{name}/metadata`` - Get component metadata

API reference
=============

create_routes
-------------

.. code-block:: python

    from wilco.bridges.starlette import create_routes

    def create_routes(
        registry: ComponentRegistry,
        build_dir: Path | None = None,
    ) -> list[Route]:
        """Create Starlette routes for component serving.

        Args:
            registry: The component registry to serve components from.
            build_dir: Optional path to pre-built bundles directory.
                When provided, serves pre-built bundles from the manifest
                and falls back to live bundling for missing components.

        Returns:
            A list of Starlette Route objects that can be mounted on any app.
        """

The returned routes provide the following endpoints:

GET /bundles
^^^^^^^^^^^^

List all available component bundles.

**Response:**

.. code-block:: json

    [
        {"name": "counter"},
        {"name": "product_card"},
        {"name": "ui.button"}
    ]

GET /bundles/{name}.js
^^^^^^^^^^^^^^^^^^^^^^

Get the bundled JavaScript for a component.

**Parameters:**

- ``name``: Component name (e.g., ``counter``, ``ui.button``)

**Response:**

- Content-Type: ``application/javascript``
- Cache-Control: ``public, max-age=31536000, immutable``

The response contains the bundled ESM JavaScript with inline source maps.

**Errors:**

- ``404``: Component not found
- ``422``: Invalid component name
- ``500``: Bundling failed (esbuild error)

GET /bundles/{name}/metadata
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Get metadata for a component, including its JSON schema and content hash.

**Parameters:**

- ``name``: Component name

**Response:**

.. code-block:: json

    {
        "title": "Counter",
        "description": "A simple counter component",
        "version": "1.0.0",
        "properties": {
            "initialValue": {"type": "number", "default": 0},
            "step": {"type": "number", "default": 1}
        },
        "hash": "abc123def456"
    }

The ``hash`` field can be used for cache busting on the frontend.

**Errors:**

- ``404``: Component not found
- ``422``: Invalid component name

Component registry
==================

Multiple sources
----------------

You can register components from multiple directories:

.. code-block:: python

    registry = ComponentRegistry()

    # Add components from multiple sources
    registry.add_source(Path("./shared_components"))
    registry.add_source(Path("./app_components"), prefix="app")

    routes = create_routes(registry)

    app = Starlette(routes=[
        Mount("/api", routes=routes),
    ])

With a prefix, components are namespaced:

- ``./shared_components/button/`` becomes ``button``
- ``./app_components/header/`` becomes ``app:header``

Caching
=======

The Starlette bridge returns long-lived cache headers for component bundles:

.. code-block:: text

    Cache-Control: public, max-age=31536000, immutable

To enable cache busting:

1. Fetch the metadata endpoint to get the ``hash``
2. Append the hash as a query parameter: ``/api/bundles/counter.js?abc123``

The hash changes whenever the component source changes.

Full example
============

Here's a complete example with CORS middleware:

.. code-block:: python

    from pathlib import Path
    from starlette.applications import Starlette
    from starlette.middleware import Middleware
    from starlette.middleware.cors import CORSMiddleware
    from starlette.routing import Mount, Route
    from starlette.responses import JSONResponse
    from wilco import ComponentRegistry
    from wilco.bridges.starlette import create_routes

    # Set up component registry
    registry = ComponentRegistry()
    registry.add_source(Path("./components"))

    # Create wilco routes
    wilco_routes = create_routes(registry)

    def homepage(request):
        return JSONResponse({"message": "Component server running"})

    # Configure CORS for frontend development
    middleware = [
        Middleware(
            CORSMiddleware,
            allow_origins=["http://localhost:5173"],  # Vite dev server
            allow_methods=["GET"],
            allow_headers=["*"],
        )
    ]

    app = Starlette(
        routes=[
            Route("/", homepage),
            Mount("/api", routes=wilco_routes),
        ],
        middleware=middleware,
    )

    if __name__ == "__main__":
        import uvicorn
        uvicorn.run(app, host="0.0.0.0", port=8400, reload=True)

Frontend integration
====================

From the frontend, you can load components using the wilco loader:

.. code-block:: tsx

    import { useComponent } from '@wilcojs/react';

    function App() {
        const Counter = useComponent('counter');

        return (
            <Suspense fallback={<div>Loading...</div>}>
                <Counter initialValue={10} step={2} />
            </Suspense>
        );
    }

See the :doc:`/reference/components` documentation for more details
on component composition.

Template-based rendering
========================

For server-rendered applications using Jinja2 templates, you can embed wilco
components directly in your HTML templates using a widget helper:

.. code-block:: python

    from pathlib import Path
    from starlette.applications import Starlette
    from starlette.routing import Mount, Route
    from starlette.templating import Jinja2Templates
    from wilco import ComponentRegistry
    from wilco.bridges.starlette import create_routes

    # Widget helper for templates
    class WilcoComponentWidget:
        def __init__(self, component_name: str, props: dict = None, api_base: str = "/api"):
            self.component_name = component_name
            self.props = props or {}
            self.api_base = api_base

        def __html__(self):
            import html, json, uuid
            props_json = html.escape(json.dumps(self.props), quote=True)
            container_id = f"wilco-{uuid.uuid4().hex[:8]}"
            return f'''<div id="{container_id}"
                 data-wilco-component="{self.component_name}"
                 data-wilco-props="{props_json}"
                 data-wilco-api="{self.api_base}">
                Loading...
            </div>'''

    templates = Jinja2Templates(directory="templates")
    registry = ComponentRegistry(Path("./components"))

    async def product_list(request):
        widget = WilcoComponentWidget("store:product_list", props={
            "products": [{"name": "Widget", "price": 9.99}]
        })
        return templates.TemplateResponse(request, "products.html", {"widget": widget})

    app = Starlette(routes=[
        Route("/", product_list),
        Mount("/api", routes=create_routes(registry)),
    ])

In your template:

.. code-block:: html

    <!DOCTYPE html>
    <html>
    <body>
        {{ widget }}
        <script src="/static/wilco/loader.js" defer></script>
    </body>
    </html>

Example application
===================

A complete example application is available in the `examples/starlette/
<https://github.com/msqd/wilco/tree/main/examples/starlette>`_ directory.
It demonstrates:

- Jinja2 template-based rendering with wilco components
- Starlette-Admin for product management
- SQLAlchemy database models
- Shared components from ``examples/common/components/``

To run the example:

.. code-block:: bash

    cd examples/starlette
    make setup   # Install deps, create database, load fixtures
    make start   # Run development server

Visit http://localhost:8400 for the store, http://localhost:8400/admin for the admin.

Live preview in Starlette-Admin
===============================

The Starlette example includes live preview functionality that shows real-time
component updates as admin users edit forms. This enables WYSIWYG editing of
content that uses wilco components.

Architecture
------------

The live preview system consists of four parts:

1. **AdminPreviewMiddleware** - Injects preview scripts into admin pages
2. **admin-preview-inject.js** - Creates the two-column layout UI
3. **live-loader-starlette.js** - Handles form validation and preview updates
4. **Validation endpoints** - Server-side form validation returning component props

AdminPreviewMiddleware
----------------------

This ASGI middleware intercepts HTML responses for ``/admin`` routes and injects
the necessary scripts before ``</body>``:

.. code-block:: python

    from starlette.types import ASGIApp, Message, Receive, Scope, Send

    class AdminPreviewMiddleware:
        """Middleware to inject live preview scripts into Starlette-Admin pages."""

        INJECT_SCRIPTS = """
        <script src="/wilco-static/wilco/loader.js" defer></script>
        <script src="/static/wilco/admin-preview-inject.js" defer></script>
        <script src="/static/wilco/live-loader-starlette.js" defer></script>
        </body>"""

        def __init__(self, app: ASGIApp) -> None:
            self.app = app

        async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
            if scope["type"] != "http":
                await self.app(scope, receive, send)
                return

            path = scope.get("path", "")

            # Only inject into admin pages
            if not path.startswith("/admin"):
                await self.app(scope, receive, send)
                return

            # Buffer response and inject scripts before </body>
            # ... (full implementation in examples/starlette/app/main.py)

Wrap your application after mounting admin:

.. code-block:: python

    app = Starlette(routes=routes, middleware=middleware)
    admin = create_admin()
    admin.mount_to(app)

    # Wrap with preview middleware
    app = AdminPreviewMiddleware(app)

Validation endpoint
-------------------

Create a validation endpoint that receives form data and returns component props:

.. code-block:: python

    from starlette.requests import Request
    from starlette.responses import JSONResponse
    from starlette.routing import Route

    async def validate_preview(request: Request) -> JSONResponse:
        """Validate form data and return props for live preview."""
        if request.method != "POST":
            return JSONResponse({"error": "Method not allowed"}, status_code=405)

        # Get product ID from path if editing existing item
        product_id = request.path_params.get("id")
        product = None
        if product_id:
            product = get_product_by_id(product_id)  # Your DB lookup

        # Parse form data
        form_data = await request.form()
        data = {key: value for key, value in form_data.items()}

        # Validate
        errors = {}
        if not data.get("name"):
            errors["name"] = ["Name is required"]
        if not data.get("price"):
            errors["price"] = ["Price is required"]

        if errors:
            return JSONResponse({"success": False, "errors": errors})

        # Convert to component props
        props = {
            "name": data.get("name", ""),
            "price": float(data.get("price", 0)),
            "description": data.get("description", ""),
            "imageUrl": f"/media/{data.get('image')}" if data.get("image") else "/placeholder.jpg",
        }

        return JSONResponse({"success": True, "props": props})

    def get_preview_routes() -> list[Route]:
        """Get routes for preview validation endpoints."""
        return [
            Route("/admin/product/preview", validate_preview, methods=["POST"]),
            Route("/admin/product/{id:int}/preview", validate_preview, methods=["POST"]),
        ]

Preview UI injection
--------------------

The ``admin-preview-inject.js`` script automatically detects product edit pages
and creates a two-column layout with the form on the left and preview on the right.

Key features:

- **Resizable sidebar**: Drag the handle to resize the preview panel
- **Width persistence**: Remembers width per admin type (product, category, etc.)
- **Responsive layout**: Stacks vertically on small screens or when preview exceeds 50% width
- **Toggle button**: Switch between sidebar and full-width stacked modes

The preview container uses data attributes to configure the component:

.. code-block:: html

    <div id="wilco-preview-container"
         data-wilco-component="store:product_preview"
         data-wilco-props="{}"
         data-wilco-api="/api"
         data-wilco-live="true"
         data-wilco-validate-url="/admin/product/123/preview">
    </div>

Live form validation
--------------------

The ``live-loader-starlette.js`` script monitors form inputs and triggers
debounced validation requests (300ms delay). When the server returns new props,
the preview component updates automatically.

The script:

1. Listens for ``input`` and ``change`` events on form fields
2. Collects form data and POSTs to the validation URL
3. On success, updates component props via ``window.wilco.updateComponentProps``
4. On validation errors, displays them above the preview

Customizing for other models
----------------------------

To add live preview to other admin models:

1. Create a preview component (e.g., ``store:category_preview``)
2. Add validation routes for the model
3. The ``admin-preview-inject.js`` can be modified to detect other edit pages
4. Add preview width storage keys as needed (stored as ``wilco-preview-width-{type}``)

Static files setup
------------------

Ensure your static file mounts serve both the wilco loader and your preview scripts:

.. code-block:: python

    from starlette.staticfiles import StaticFiles

    from wilco.bridges.base import STATIC_DIR as WILCO_STATIC_DIR

    routes = [
        # Your routes...
        Mount("/static", StaticFiles(directory="resources/static"), name="static"),
        Mount("/wilco-static", StaticFiles(directory=str(WILCO_STATIC_DIR)), name="wilco_static"),
    ]

Production deployment
=====================

For production, you can pre-compile component bundles to avoid runtime esbuild
dependency and serve them as static files.

Pre-building bundles
--------------------

Use the ``wilco build`` CLI to pre-compile components:

.. code-block:: bash

    wilco build --output dist/wilco/ --components-dir ./components --prefix store

This generates:

- ``dist/wilco/bundles/{name}.{hash}.js`` — hashed bundle files
- ``dist/wilco/manifest.json`` — component-to-file mapping

Using pre-built bundles
-----------------------

Pass the ``build_dir`` parameter to ``create_routes``:

.. code-block:: python

    from pathlib import Path
    from wilco.bridges.starlette import create_routes

    build_dir = Path("./dist/wilco")

    # Serves pre-built bundles when available, falls back to live bundling
    routes = create_routes(registry, build_dir=build_dir)

    app = Starlette(routes=[
        Mount("/api", routes=routes),
    ])

You can also set the ``WILCO_BUILD_DIR`` environment variable and resolve it
programmatically:

.. code-block:: python

    from wilco.manifest import resolve_build_dir

    build_dir = resolve_build_dir(Path("./dist/wilco"))

In static mode, the API bundle endpoint returns 404 and clients load bundles
from static file URLs instead.

Comparison with FastAPI
=======================

The Starlette bridge is a lighter alternative to the FastAPI bridge:

- **Use Starlette bridge** when you want minimal dependencies and don't need
  FastAPI features like automatic OpenAPI docs or Pydantic validation.

- **Use FastAPI bridge** when you're already using FastAPI and want seamless
  integration with its dependency injection and automatic documentation.

Both bridges expose the same endpoints and behavior.
