v0.152.0
 1from __future__ import annotations
 2
 3import os
 4from collections.abc import Iterator
 5from pathlib import Path
 6
 7from plain.packages import packages_registry
 8from plain.runtime import APP_PATH
 9
10_APP_ASSETS_DIR = APP_PATH / "assets"
11
12_SKIP_ASSETS = (".DS_Store", ".gitignore")
13
14# A top-level `src/` in any asset dir holds build inputs — a build tool's entry
15# points and the modules they import. They are consumed by the build, never served.
16_BUILD_INPUT_DIR = "src"
17
18# A top-level `dist/` holds build output that is already content-hashed by the
19# build tool. Plain serves it immutable without re-fingerprinting (skips its md5).
20_BUILD_OUTPUT_DIR = "dist"
21
22
23def is_build_output(url_path: str) -> bool:
24    """Whether a url_path lives under the top-level `dist/` build-output dir."""
25    return url_path.startswith(_BUILD_OUTPUT_DIR + os.sep)
26
27
28class Asset:
29    def __init__(self, *, url_path: str, absolute_path: str):
30        self.url_path = url_path
31        self.absolute_path = absolute_path
32
33    def __str__(self) -> str:
34        return self.url_path
35
36
37def _iter_assets() -> Iterator[Asset]:
38    """
39    Iterate all valid asset files found in the installed
40    packages and the app itself.
41    """
42
43    def __iter_assets_dir(path: str | Path) -> Iterator[tuple[str, str]]:
44        at_root = True
45        for root, dirs, files in os.walk(path):
46            if at_root:
47                # Prune the top-level `src/` build-input dir — only here at the
48                # root (os.walk yields it first), never a nested `foo/src/`.
49                dirs[:] = [d for d in dirs if d != _BUILD_INPUT_DIR]
50                at_root = False
51            for f in files:
52                if f in _SKIP_ASSETS:
53                    continue
54                abs_path = os.path.join(root, f)
55                url_path = os.path.relpath(abs_path, path)
56                yield url_path, abs_path
57
58    for asset_dir in _iter_asset_dirs():
59        for url_path, abs_path in __iter_assets_dir(asset_dir):
60            yield Asset(url_path=url_path, absolute_path=abs_path)
61
62
63def _iter_asset_dirs() -> Iterator[str | Path]:
64    """
65    Iterate all directories containing assets, from installed
66    packages and from app/assets.
67    """
68    # Iterate the installed package assets, in order
69    for pkg in packages_registry.get_package_configs():
70        asset_dir = os.path.join(pkg.path, "assets")
71        if os.path.exists(asset_dir):
72            yield asset_dir
73
74    # The app/assets take priority over everything
75    if _APP_ASSETS_DIR.exists():
76        yield _APP_ASSETS_DIR