Source code for next.components.watch

"""Read-only discovery of component filesystem paths for autoreload.

`get_component_paths_for_watch` combines page-tree and backend `DIRS`
scans so the dev reloader can register component files without
mutating the manager state.
"""

from __future__ import annotations

import itertools
import logging
from typing import TYPE_CHECKING, Any

from next.conf import next_framework_settings

from .backends import ComponentsFactory, FileComponentsBackend
from .info import _paths_from_component_info
from .loading import ModuleLoader
from .scanner import ComponentScanner, component_extra_roots_from_config


if TYPE_CHECKING:
    from pathlib import Path


logger = logging.getLogger(__name__)


def _collect_paths_for_one_pages_root(
    scanner: ComponentScanner,
    comp_name: str,
    root: Path,
) -> set[Path]:
    """Gather component paths under one pages tree root."""
    result: set[Path] = set()
    try:
        for path in root.glob(f"**/{comp_name}"):
            if not path.is_dir():
                continue
            try:
                rel_parent = path.parent.relative_to(root)
            except ValueError:
                continue
            scope_relative = "/".join(rel_parent.parts) if rel_parent.parts else ""
            for info in scanner.scan_directory(path, root, scope_relative):
                result |= _paths_from_component_info(info)
    except OSError as e:
        logger.debug(
            "Cannot scan %s for component dirs %s: %s",
            root,
            comp_name,
            e,
        )
    return result


def _collect_component_paths_under_page_trees() -> set[Path]:
    """Collect component paths from page backends without mutating registries."""
    from next.urls import RouterFactory  # noqa: PLC0415

    result: set[Path] = set()
    page_configs = next_framework_settings.DEFAULT_PAGE_BACKENDS
    if not isinstance(page_configs, list):
        return result
    for config in page_configs:
        if not isinstance(config, dict):
            continue
        try:
            backend = RouterFactory.create_backend(config)
        except Exception:
            logger.exception(
                "error creating page backend for component autoreload scan %s",
                config,
            )
            continue
        if not RouterFactory.is_filesystem_discovery_router(backend):
            continue
        fs_backend: Any = backend
        comp_name = str(fs_backend._components_folder_name)
        scanner = ComponentScanner(comp_name)
        for root in itertools.chain(
            (p.resolve() for p in fs_backend._get_root_pages_paths()),
            (
                a.resolve()
                for app_name in fs_backend._get_installed_apps()
                if (a := fs_backend._get_app_pages_path(app_name))
            ),
        ):
            result |= _collect_paths_for_one_pages_root(scanner, comp_name, root)
    return result


def _collect_component_paths_from_backend_dirs() -> set[Path]:
    """Collect paths from component backend `DIRS` entries only."""
    result: set[Path] = set()
    comp_configs = next_framework_settings.DEFAULT_COMPONENT_BACKENDS
    if not isinstance(comp_configs, list):
        return result
    for config in comp_configs:
        if not isinstance(config, dict):
            continue
        try:
            backend = ComponentsFactory.create_backend(config)
        except Exception:
            logger.exception(
                "error creating component backend for autoreload scan %s", config
            )
            continue
        if not isinstance(backend, FileComponentsBackend):
            continue
        scanner = ComponentScanner(
            backend.components_dir,
            module_loader=ModuleLoader(),
        )
        for root in component_extra_roots_from_config(config):
            try:
                for info in scanner.scan_directory(root, root, ""):
                    result |= _paths_from_component_info(info)
            except OSError as e:
                logger.debug(
                    "Cannot scan component root %s: %s",
                    root,
                    e,
                )
    return result


[docs] def get_component_paths_for_watch() -> set[Path]: """Return filesystem paths that matter for the dev component reloader. This performs a read-only scan. It does not mutate the components manager or router registration state. """ page_paths = _collect_component_paths_under_page_trees() extra_paths = _collect_component_paths_from_backend_dirs() return page_paths | extra_paths
__all__ = ["get_component_paths_for_watch"]