Source code for next.static.finders

"""Django staticfiles finder that exposes next-dj co-located assets.

The finder surfaces every `template.css`, `layout.js`, and
`component.css` plus any stems registered on the stem registry under
the `next/` staticfiles namespace. The usual `{% static "next/about.css" %}`
call works without the user configuring anything.

The logical-path and source-file mapping is computed by the
co-located asset discovery helper below, which shares the same
`PathResolver` used at request-time discovery. The two layers agree on
every URL.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Literal, overload

from django.contrib.staticfiles.finders import BaseFinder
from django.contrib.staticfiles.utils import matches_patterns
from django.core.files import File
from django.core.files.storage import Storage

from next.pages.registry import (
    get_layout_djx_paths_for_watch,
    get_template_djx_paths_for_watch,
)
from next.pages.watch import get_pages_directories_for_watch

from .assets import StaticNamespace, default_kinds
from .discovery import PathResolver, default_stems


if TYPE_CHECKING:
    from collections.abc import Iterable, Iterator
    from pathlib import Path

    from .discovery import StemRegistry


def _collect_stem_static_files(
    out: dict[str, Path],
    directory: Path,
    logical_name: str,
    role: str,
    stems: StemRegistry,
) -> None:
    """Add `{stem}.<kind>` files found in the directory to the output map.

    Every registered kind is probed for each stem of the given role.
    """
    kinds = default_kinds.kinds()
    for stem in stems.stems(role):
        for kind in kinds:
            suffix = default_kinds.extension(kind)
            candidate = directory / f"{stem}{suffix}"
            if not candidate.exists():  # pragma: no cover
                continue
            static_path = f"{StaticNamespace.NEXT}/{logical_name}{suffix}"
            out.setdefault(static_path, candidate.resolve())


[docs] def discover_colocated_static_assets() -> dict[str, Path]: """Map staticfiles logical paths to absolute source files on disk. The helper scans every configured page-backend tree plus registered components. It honors the process-wide stem and kind registries, so custom stems registered during `AppConfig.ready` are picked up. """ out: dict[str, Path] = {} page_roots = tuple(root.resolve() for root in get_pages_directories_for_watch()) resolver = PathResolver(lambda: page_roots) for template_path in get_template_djx_paths_for_watch(): page_root = resolver.find_page_root(template_path) if page_root is None: continue template_dir = template_path.parent.resolve() logical_name = resolver.logical_name_for_template(template_dir, page_root) _collect_stem_static_files( out, template_dir, logical_name, "template", default_stems ) for layout_path in get_layout_djx_paths_for_watch(): page_root = resolver.find_page_root(layout_path) if page_root is None: # pragma: no cover continue layout_dir = layout_path.parent.resolve() logical_name = resolver.logical_name_for_layout(layout_dir, page_root) _collect_stem_static_files( out, layout_dir, logical_name, "layout", default_stems ) # next.components relies on the Django app registry being ready. A top-level # import would load it before AppConfig.ready() completes, so we defer here. from next.components import get_component_paths_for_watch # noqa: PLC0415 seen_component_dirs: set[Path] = set() for component_source in get_component_paths_for_watch(): # pragma: no cover component_dir = component_source.parent.resolve() if component_dir in seen_component_dirs: continue seen_component_dirs.add(component_dir) logical_name = f"components/{component_dir.name}" _collect_stem_static_files( out, component_dir, logical_name, "component", default_stems ) return out
class _MappedSourceStorage(Storage): """Storage wrapper that serves files from an explicit path mapping.""" def __init__(self, mapping: dict[str, Path]) -> None: """Store the explicit logical-path to absolute-path mapping.""" self._mapping = mapping def _resolve(self, name: str) -> Path: if name not in self._mapping: msg = f"Unknown static file: {name}" raise FileNotFoundError(msg) return self._mapping[name] def exists(self, name: str) -> bool: """Return True when the logical name has a mapping and the file exists.""" try: return self._resolve(name).exists() except FileNotFoundError: return False def open(self, name: str, mode: str = "rb") -> File: """Open the file behind the logical name for reading.""" path = self._resolve(name) return File(path.open(mode)) def path(self, name: str) -> str: """Return the absolute filesystem path behind the logical name.""" return str(self._resolve(name))
[docs] class NextStaticFilesFinder(BaseFinder): """Expose next-dj co-located assets under the `next/` staticfiles namespace."""
[docs] def __init__(self) -> None: """Initialise an empty mapping and storage, populated lazily on first lookup.""" self._mapping: dict[str, Path] = {} self._storage: _MappedSourceStorage = _MappedSourceStorage({})
def _refresh(self) -> None: self._mapping = discover_colocated_static_assets() self._storage = _MappedSourceStorage(self._mapping) @overload def find( self, path: str, find_all: Literal[False] = ... ) -> str | None: ... # pragma: no cover @overload def find( self, path: str, find_all: Literal[True] ) -> list[str]: ... # pragma: no cover
[docs] def find( self, path: str, find_all: bool = False, # noqa: FBT001, FBT002 ) -> str | list[str] | None: """Resolve the logical path to an absolute filesystem path or list.""" self._refresh() source = self._mapping.get(path) if source is None: return [] if find_all else None resolved = str(source) return [resolved] if find_all else resolved
[docs] def list( self, ignore_patterns: Iterable[str] | None, ) -> Iterator[tuple[str, Storage]]: """Yield logical-path and storage pairs for `collectstatic`.""" patterns = list(ignore_patterns) if ignore_patterns is not None else [] self._refresh() for logical_path in sorted(self._mapping): if matches_patterns(logical_path, patterns): continue yield logical_path, self._storage