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