Source code for next.static.backends

"""Pluggable backend contract and Django-staticfiles default implementation.

A static backend turns co-located asset paths into public URLs and
renders them through one of its named renderer methods. The default
backend delegates URL resolution to Django staticfiles, so manifest
hashing, S3 storage, and CDN configuration from Django settings apply
automatically.

The abstract `StaticBackend` only mandates `register_file`. Renderer
methods are concrete on the default backend and selected per asset by
`KindRegistry.renderer(kind)`. Custom backends extend the surface by
adding more named methods such as `render_babel_script_tag` and
registering kinds that point to them.

A small factory builds backend instances from
`NEXT_FRAMEWORK['DEFAULT_STATIC_BACKENDS']` entries. The factory also
emits the `backend_loaded` signal so user code may react to backend
construction.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, ClassVar

from django.contrib.staticfiles.storage import staticfiles_storage

from next.conf import import_class_cached

from .assets import StaticNamespace
from .signals import backend_loaded


if TYPE_CHECKING:
    from collections.abc import Mapping
    from pathlib import Path

    from django.http import HttpRequest


[docs] class StaticBackend(ABC): """Pluggable strategy for resolving asset files to URLs and rendering tags. The constructor accepts the full backend entry from `DEFAULT_STATIC_BACKENDS`, which has the shape `{"BACKEND": "...", "OPTIONS": {...}}`. The base class stores the mapping on the `config` property. Subclasses are free to read any keys they expose to users. The only abstract requirement is `register_file`. Renderer methods are added by subclasses and selected per asset through `KindRegistry.renderer(kind)`. The default backend below ships `render_link_tag` and `render_script_tag` for the built-in `css` and `js` kinds. Custom backends register additional kinds and expose matching methods. """
[docs] def __init__(self, config: Mapping[str, Any] | None = None) -> None: """Store the raw config mapping for subclasses to read.""" self._config: Mapping[str, Any] = config or {}
@property def config(self) -> Mapping[str, Any]: """Return the backend entry supplied at construction time.""" return self._config
[docs] @abstractmethod def register_file( self, source_path: Path, logical_name: str, kind: str, ) -> str: """Register a co-located asset file and return its public URL. The `source_path` argument is the absolute path to the source file on disk. The `logical_name` argument is the path without an extension, for example `"about"` or `"components/card"`. The `kind` argument must be a kind registered in the default kind registry. The method raises `RuntimeError` when the asset cannot be resolved to a URL. """
[docs] class StaticFilesBackend(StaticBackend): """Resolve co-located asset URLs through Django staticfiles. Assets live in the `next/` staticfiles namespace so manifest storage, S3 storage, and CDN settings apply automatically. Two option keys are recognised in the backend entry. The `css_tag` key sets a format string for `<link>` tags. It must contain the `{url}` placeholder. Extra attributes such as `crossorigin` or `integrity` can be baked directly into the template. The `js_tag` key sets a format string for `<script>` tags. The same placeholder rules apply. Attributes such as `defer` or `async` are added by writing them into the template. """ _DEFAULT_CSS_TAG: ClassVar[str] = '<link rel="stylesheet" href="{url}">' _DEFAULT_JS_TAG: ClassVar[str] = '<script src="{url}"></script>' _DEFAULT_MODULE_TAG: ClassVar[str] = '<script type="module" src="{url}"></script>'
[docs] def __init__(self, config: Mapping[str, Any] | None = None) -> None: """Read tag templates from the OPTIONS mapping and prime caches.""" super().__init__(config) opts = dict(self._config.get("OPTIONS") or {}) self._css_tag = str(opts.get("css_tag") or self._DEFAULT_CSS_TAG) self._js_tag = str(opts.get("js_tag") or self._DEFAULT_JS_TAG) self._module_tag = str(opts.get("module_tag") or self._DEFAULT_MODULE_TAG) self._url_cache: dict[tuple[str, str], str] = {}
def _logical_static_path(self, logical_name: str, suffix: str) -> str: return f"{StaticNamespace.NEXT}/{logical_name}{suffix}"
[docs] def register_file( self, source_path: Path, logical_name: str, kind: str, # noqa: ARG002 ) -> str: """Return the staticfiles URL for `next/<logical_name><suffix>`. The suffix is taken from `source_path.suffix`, so a single kind can serve multiple file extensions if a custom backend wishes to. Result is cached per `(logical_name, suffix)`. Missing entries in the staticfiles manifest are reported as `RuntimeError` with a hint about running `collectstatic`. """ suffix = source_path.suffix cache_key = (logical_name, suffix) cached = self._url_cache.get(cache_key) if cached is not None: return cached path = self._logical_static_path(logical_name, suffix) try: url = str(staticfiles_storage.url(path)) except ValueError as e: msg = ( f"Static asset {path!r} is missing from Django staticfiles " "manifest. Run collectstatic and ensure the next static " "finder is enabled." ) raise RuntimeError(msg) from e self._url_cache[cache_key] = url return url
[docs] def render_script_tag( self, url: str, *, request: HttpRequest | None = None, # noqa: ARG002 ) -> str: """Return a script tag built from the configured js_tag template. The `request` argument is accepted for contract compatibility and ignored by the default backend. """ return self._js_tag.format(url=url)
[docs] def render_module_tag( self, url: str, *, request: HttpRequest | None = None, # noqa: ARG002 ) -> str: """Return a module script tag built from the configured module_tag template. The `request` argument is accepted for contract compatibility and ignored by the default backend. """ return self._module_tag.format(url=url)
[docs] class StaticsFactory: """Build static backend instances from configuration dicts.""" DEFAULT_BACKEND: ClassVar[str] = "next.static.StaticFilesBackend"
[docs] @classmethod def create_backend(cls, config: Mapping[str, Any]) -> StaticBackend: """Instantiate the backend class named by `config['BACKEND']`. Raises `TypeError` when the configured class is not a subclass of `StaticBackend`. """ backend_path = config.get("BACKEND", cls.DEFAULT_BACKEND) backend_class = import_class_cached(backend_path) if not isinstance(backend_class, type) or not issubclass( backend_class, StaticBackend ): msg = f"Backend {backend_path!r} is not a StaticBackend subclass" raise TypeError(msg) instance: StaticBackend = backend_class(config) backend_loaded.send( sender=backend_class, config=dict(config), instance=instance ) return instance