Source code for litestar_vite.plugin._static

"""Server-neutral static-serving contract.

Describes where a Litestar Vite app's production static assets should be served
from: either directly by the web server (``NATIVE``) or by Litestar's static
router over ASGI (``ASGI``). The result is a plain dataclass with an explicit
``placement`` discriminator so an external consumer such as litestar-granian can
compare structurally (``config.placement == "native"``) without importing this
package. This module deliberately carries no litestar-granian import.
"""

from dataclasses import dataclass, fields
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar

if TYPE_CHECKING:
    from collections.abc import Sequence

    from litestar.datastructures import CacheControlHeader
    from litestar.openapi.spec import SecurityRequirement
    from litestar.types import (
        AfterRequestHookHandler,  # pyright: ignore[reportUnknownVariableType]
        AfterResponseHookHandler,  # pyright: ignore[reportUnknownVariableType]
        BeforeRequestHookHandler,  # pyright: ignore[reportUnknownVariableType]
        ExceptionHandlersMap,
        Guard,  # pyright: ignore[reportUnknownVariableType]
        Middleware,
    )


[docs] class StaticPlacement(str, Enum): """Where static files are served from. Subclassing ``str`` is deliberate: consumers compare structurally against the literal value (``config.placement == "native"``) without importing this package. """ NATIVE = "native" # eligible: the web server may serve mounts directly ASGI = "asgi" # Litestar's static router must serve
[docs] @dataclass(frozen=True, slots=True) class StaticServerMount: """Describe one static directory exposed to a native server.""" route: str directory: Path directory_index: str | None = None
[docs] @dataclass(frozen=True, slots=True) class StaticServerConfig: """Describe where static files should be served from. ``reason`` is diagnostic detail that is meaningful only when ``placement`` is ``ASGI``; it explains why Litestar's static router must serve (development mode, framework/SSR routing, manifest/build-state problems, and similar). """ placement: StaticPlacement = StaticPlacement.ASGI mounts: tuple[StaticServerMount, ...] = () reason: str | None = None
[docs] def __post_init__(self) -> None: """Enforce the placement invariants. Raises: ValueError: If ``NATIVE`` has no mounts, ``NATIVE`` carries a reason, or ``ASGI`` lacks a non-empty reason. """ if self.placement is StaticPlacement.NATIVE: if not self.mounts: msg = "StaticServerConfig with NATIVE placement requires at least one mount." raise ValueError(msg) if self.reason is not None: msg = "StaticServerConfig with NATIVE placement must not carry a reason; reason is ASGI-only detail." raise ValueError(msg) elif not self.reason: msg = "StaticServerConfig with ASGI placement requires a non-empty reason." raise ValueError(msg)
[docs] @dataclass class StaticFilesConfig: """Configuration for static file serving. Field names must match keyword parameters of Litestar's ``create_static_files_router``; :meth:`as_router_kwargs` forwards the set fields to it. """ after_request: "AfterRequestHookHandler | None" = None after_response: "AfterResponseHookHandler | None" = None before_request: "BeforeRequestHookHandler | None" = None cache_control: "CacheControlHeader | None" = None exception_handlers: "ExceptionHandlersMap | None" = None guards: "list[Guard] | None" = None # pyright: ignore[reportUnknownVariableType] middleware: "Sequence[Middleware] | None" = None opt: "dict[str, Any] | None" = None security: "Sequence[SecurityRequirement] | None" = None tags: "Sequence[str] | None" = None _NOT_ROUTER_KWARGS: "ClassVar[frozenset[str]]" = frozenset({"opt"})
[docs] def as_router_kwargs(self) -> "dict[str, Any]": """Return the explicitly-set fields as ``create_static_files_router`` keyword arguments. ``opt`` is excluded because the plugin merges it with its own options. Unset fields are omitted so Litestar's defaults apply. Returns: Keyword arguments for ``create_static_files_router``. """ kwargs: "dict[str, Any]" = {} for field_info in fields(self): if field_info.name in self._NOT_ROUTER_KWARGS: continue value: "Any" = getattr(self, field_info.name) if value is not None: kwargs[field_info.name] = value return kwargs