simple-module-hosting 0.0.26__tar.gz → 0.0.27__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/.gitignore +1 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/PKG-INFO +3 -3
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/pyproject.toml +3 -3
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_inertia_setup.py +12 -7
- simple_module_hosting-0.0.27/simple_module_hosting/assets.py +160 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/host_cli.py +2 -2
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/manifest.py +34 -27
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_branding_head.py +19 -2
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_middleware_order.py +9 -1
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/LICENSE +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/README.md +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/__init__.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/__main__.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_error_handlers.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_host_services.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_hydrate_step.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_inertia_shared.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_observability.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_phase_helpers.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/app_builder.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/bootstrap_settings.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/health.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/host_settings.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/i18n_deps.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/i18n_middleware.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/inertia_deps.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/inertia_utils.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/logging.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/middleware.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/migrations.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/permissions.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/py.typed +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/redirects.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/settings.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/shared_props.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/static_files.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_app.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_check_migrations.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_design_packs_wiring.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_error_page_shared_props.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_health.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_host_cli.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_hosting_permissions.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_inertia_i18n_shared_props.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_inertia_manifest.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_inertia_shared_providers.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_lifespan_order.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_locale_middleware.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_logging.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_manifest.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_menu_urls_are_canonical.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_precompressed_static.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_proxy_headers.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_redirects.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_response_compression.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_session_cookie_security.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_settings_i18n.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_settings_secrets.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_static_caching.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_strict_discovery_wiring.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_tenant_middleware.py +0 -0
- {simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_translator_dep.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simple_module_hosting
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.27
|
|
4
4
|
Summary: FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -26,8 +26,8 @@ Requires-Dist: fastapi-inertia>=1.0
|
|
|
26
26
|
Requires-Dist: fastapi>=0.115
|
|
27
27
|
Requires-Dist: httpx>=0.27
|
|
28
28
|
Requires-Dist: jinja2>=3.1
|
|
29
|
-
Requires-Dist: simple-module-core==0.0.
|
|
30
|
-
Requires-Dist: simple-module-db==0.0.
|
|
29
|
+
Requires-Dist: simple-module-core==0.0.27
|
|
30
|
+
Requires-Dist: simple-module-db==0.0.27
|
|
31
31
|
Requires-Dist: starlette>=0.44
|
|
32
32
|
Requires-Dist: tomlkit>=0.13
|
|
33
33
|
Requires-Dist: uvicorn[standard]>=0.34
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_hosting"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.27"
|
|
4
4
|
description = "FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -26,8 +26,8 @@ dependencies = [
|
|
|
26
26
|
"fastapi-inertia>=1.0",
|
|
27
27
|
"httpx>=0.27",
|
|
28
28
|
"jinja2>=3.1",
|
|
29
|
-
"simple_module_core==0.0.
|
|
30
|
-
"simple_module_db==0.0.
|
|
29
|
+
"simple_module_core==0.0.27",
|
|
30
|
+
"simple_module_db==0.0.27",
|
|
31
31
|
"starlette>=0.44",
|
|
32
32
|
"tomlkit>=0.13",
|
|
33
33
|
"uvicorn[standard]>=0.34",
|
|
@@ -30,20 +30,25 @@ def branding_head(request: Request) -> dict:
|
|
|
30
30
|
"""Branding metadata for the root template's ``<head>``.
|
|
31
31
|
|
|
32
32
|
Reads the optional branding module's settings off ``app.state`` by name
|
|
33
|
-
(duck-typed, never imported) so the
|
|
34
|
-
are already branded *before* React hydrates
|
|
35
|
-
default
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
(duck-typed, never imported) so the ``<title>``, ``theme-color`` and
|
|
34
|
+
favicon are already branded *before* React hydrates — otherwise the browser
|
|
35
|
+
paints the default favicon and only swaps on hydration, a visible flicker on
|
|
36
|
+
every full page load. Degrades to the framework default when branding isn't
|
|
37
|
+
installed.
|
|
38
|
+
|
|
39
|
+
The favicon URL is read from the module rather than assembled here: branding
|
|
40
|
+
owns its route shape, and framework code must not reach into a plugin
|
|
41
|
+
(SM009). ``BrandingHead`` still applies it client-side too, so a favicon
|
|
42
|
+
changed at runtime updates without a reload.
|
|
39
43
|
"""
|
|
40
44
|
services = getattr(request.app.state, "branding", None)
|
|
41
45
|
settings = getattr(services, "settings", None)
|
|
42
46
|
if settings is None:
|
|
43
|
-
return {"app_name": _DEFAULT_APP_NAME, "theme_color": None}
|
|
47
|
+
return {"app_name": _DEFAULT_APP_NAME, "theme_color": None, "favicon_url": None}
|
|
44
48
|
return {
|
|
45
49
|
"app_name": getattr(settings, "app_name", "") or _DEFAULT_APP_NAME,
|
|
46
50
|
"theme_color": getattr(settings, "primary_color", "") or None,
|
|
51
|
+
"favicon_url": getattr(services, "favicon_url", None),
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Per-module frontend asset discovery and CSS emission.
|
|
2
|
+
|
|
3
|
+
Split out from :mod:`simple_module_hosting.manifest`, which is already at the
|
|
4
|
+
repo's 300-line cap. ``manifest.py`` imports from here.
|
|
5
|
+
|
|
6
|
+
A module may ship two optional stylesheets beside its ``pages/`` directory:
|
|
7
|
+
|
|
8
|
+
* ``theme.css`` — ``@theme`` tokens, ``@custom-variant``, ``@font-face``.
|
|
9
|
+
Imported *unlayered*, because a ``@theme`` block inside a cascade layer is
|
|
10
|
+
inert and registers no design tokens.
|
|
11
|
+
* ``styles.css`` — component rules, keyframes, vendor CSS. Imported into
|
|
12
|
+
``layer(components)``, because unlayered CSS outranks every Tailwind
|
|
13
|
+
utility: a module shipping a bare ``.card { padding: 0 }`` would otherwise
|
|
14
|
+
silently beat ``p-4``.
|
|
15
|
+
|
|
16
|
+
Both are referenced through a per-module Vite alias (``#module/<pkg>``) rather
|
|
17
|
+
than a filesystem path, so the generated CSS never contains something like
|
|
18
|
+
``../../../.venv/lib/python3.12/site-packages/<pkg>/styles.css``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import importlib.resources
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
from collections.abc import Callable, Sequence
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from simple_module_core import ModuleBase, get_module_package_name
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
THEME_CSS = "theme.css"
|
|
35
|
+
STYLES_CSS = "styles.css"
|
|
36
|
+
ALIAS_PREFIX = "#module"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ModuleAssets:
|
|
41
|
+
"""Frontend assets a single module contributes."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
package_name: str
|
|
45
|
+
package_dir: Path
|
|
46
|
+
pages_dir: Path | None
|
|
47
|
+
theme_css: Path | None
|
|
48
|
+
styles_css: Path | None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def compute_module_assets(modules: Sequence[ModuleBase]) -> list[ModuleAssets]:
|
|
52
|
+
"""Return per-module frontend assets, preserving discovery order.
|
|
53
|
+
|
|
54
|
+
Discovery order matters. ``discover_modules()`` topologically sorts by
|
|
55
|
+
``ModuleMeta.depends_on``, which is the order the host invokes
|
|
56
|
+
``register_*`` hooks in; emitting CSS in that same order means a module
|
|
57
|
+
that depends on another can override its dependency's styles.
|
|
58
|
+
|
|
59
|
+
A module is included only if it contributes at least one asset.
|
|
60
|
+
"""
|
|
61
|
+
result: list[ModuleAssets] = []
|
|
62
|
+
for mod in modules:
|
|
63
|
+
pkg_name = get_module_package_name(mod)
|
|
64
|
+
try:
|
|
65
|
+
pkg_root = Path(str(importlib.resources.files(pkg_name)))
|
|
66
|
+
except (ModuleNotFoundError, TypeError):
|
|
67
|
+
logger.debug(
|
|
68
|
+
"Module '%s': package %s not importable — skipping", mod.meta.name, pkg_name
|
|
69
|
+
)
|
|
70
|
+
continue
|
|
71
|
+
pages_dir = pkg_root / "pages"
|
|
72
|
+
theme = pkg_root / THEME_CSS
|
|
73
|
+
styles = pkg_root / STYLES_CSS
|
|
74
|
+
entry = ModuleAssets(
|
|
75
|
+
name=mod.meta.name,
|
|
76
|
+
package_name=pkg_name,
|
|
77
|
+
package_dir=pkg_root.resolve(),
|
|
78
|
+
pages_dir=pages_dir.resolve() if pages_dir.is_dir() else None,
|
|
79
|
+
theme_css=theme.resolve() if theme.is_file() else None,
|
|
80
|
+
styles_css=styles.resolve() if styles.is_file() else None,
|
|
81
|
+
)
|
|
82
|
+
if entry.pages_dir or entry.theme_css or entry.styles_css:
|
|
83
|
+
result.append(entry)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_CSS_HEADER = """\
|
|
88
|
+
/* AUTO-GENERATED by simple_module_hosting.assets — do not edit by hand.
|
|
89
|
+
* Regenerate with: smpy gen-pages
|
|
90
|
+
*
|
|
91
|
+
* Emission order is module discovery order (topological by depends_on), so a
|
|
92
|
+
* dependent module's CSS can override its dependency's.
|
|
93
|
+
*
|
|
94
|
+
* theme.css is imported unlayered so its @theme blocks register as design
|
|
95
|
+
* tokens; styles.css is imported into layer(components) so a module rule can
|
|
96
|
+
* never outrank a Tailwind utility.
|
|
97
|
+
*/"""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def render_modules_css(
|
|
101
|
+
assets: Sequence[ModuleAssets],
|
|
102
|
+
*,
|
|
103
|
+
in_repo: Callable[[Path], bool],
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Render the contents of ``modules.generated.css``.
|
|
106
|
+
|
|
107
|
+
``@source`` is emitted only for wheel-installed modules — in-repo module
|
|
108
|
+
pages are already covered by the static ``@source`` glob in the host's
|
|
109
|
+
``styles.css``, so emitting an absolute one too would just duplicate it.
|
|
110
|
+
``@import`` is emitted for *every* module, in-repo and wheel alike, because
|
|
111
|
+
there is no static-glob equivalent for CSS.
|
|
112
|
+
"""
|
|
113
|
+
source_lines = [
|
|
114
|
+
f'@source "{e.pages_dir.as_posix()}/**/*.{{ts,tsx}}";'
|
|
115
|
+
for e in assets
|
|
116
|
+
if e.pages_dir and not in_repo(e.pages_dir)
|
|
117
|
+
]
|
|
118
|
+
theme_lines = [
|
|
119
|
+
f'@import "{ALIAS_PREFIX}/{e.package_name}/{THEME_CSS}";' for e in assets if e.theme_css
|
|
120
|
+
]
|
|
121
|
+
style_lines = [
|
|
122
|
+
f'@import "{ALIAS_PREFIX}/{e.package_name}/{STYLES_CSS}" layer(components);'
|
|
123
|
+
for e in assets
|
|
124
|
+
if e.styles_css
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
out = [_CSS_HEADER]
|
|
128
|
+
for heading, lines in (
|
|
129
|
+
("/* ── @source: class scanning (wheel-installed modules) ── */", source_lines),
|
|
130
|
+
("/* ── theme: unlayered, registers @theme tokens ── */", theme_lines),
|
|
131
|
+
("/* ── styles: layer(components), always loses to utilities ── */", style_lines),
|
|
132
|
+
):
|
|
133
|
+
if lines:
|
|
134
|
+
out.append("")
|
|
135
|
+
out.append(heading)
|
|
136
|
+
out.extend(lines)
|
|
137
|
+
out.append("")
|
|
138
|
+
return "\n".join(out)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def render_assets_json(assets: Sequence[ModuleAssets]) -> str:
|
|
142
|
+
"""Render ``modules.assets.json`` — the richer companion to the pages manifest.
|
|
143
|
+
|
|
144
|
+
Kept separate from ``modules.manifest.json`` rather than replacing it:
|
|
145
|
+
``vite.config.ts`` is scaffolded *into* each app, so changing the existing
|
|
146
|
+
manifest's value shape would break every downstream app at once. This file
|
|
147
|
+
is purely additive, and also covers CSS-only modules that never appear in
|
|
148
|
+
the pages manifest because they ship no ``pages/``.
|
|
149
|
+
"""
|
|
150
|
+
payload = {
|
|
151
|
+
e.name: {
|
|
152
|
+
"package_name": e.package_name,
|
|
153
|
+
"package": e.package_dir.as_posix(),
|
|
154
|
+
"pages": e.pages_dir.as_posix() if e.pages_dir else None,
|
|
155
|
+
"theme": e.theme_css.as_posix() if e.theme_css else None,
|
|
156
|
+
"styles": e.styles_css.as_posix() if e.styles_css else None,
|
|
157
|
+
}
|
|
158
|
+
for e in assets
|
|
159
|
+
}
|
|
160
|
+
return json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/host_cli.py
RENAMED
|
@@ -37,7 +37,7 @@ def gen_pages(
|
|
|
37
37
|
),
|
|
38
38
|
] = Path("client_app"),
|
|
39
39
|
) -> None:
|
|
40
|
-
"""Regenerate client_app/modules.{manifest.json,generated.ts,generated.css}."""
|
|
40
|
+
"""Regenerate client_app/modules.{manifest.json,generated.ts,generated.css,assets.json}."""
|
|
41
41
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
42
42
|
if not host_dir.is_dir():
|
|
43
43
|
typer.echo(f"ERROR: client_app directory not found at {host_dir}", err=True)
|
|
@@ -47,7 +47,7 @@ def gen_pages(
|
|
|
47
47
|
typer.echo(
|
|
48
48
|
f"Module pages manifest: {len(modules)} module(s) "
|
|
49
49
|
f"→ {written['manifest'].name}, {written['generated'].name}, "
|
|
50
|
-
f"{written['css'].name} in {host_dir}"
|
|
50
|
+
f"{written['css'].name}, {written['assets'].name} in {host_dir}"
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/manifest.py
RENAMED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Frontend pages manifest + per-module JS dependency discovery.
|
|
2
2
|
|
|
3
|
-
* :func:`write_module_pages_manifest` emits the
|
|
3
|
+
* :func:`write_module_pages_manifest` emits the four generated files Vite
|
|
4
4
|
and Tailwind read at build time:
|
|
5
5
|
- ``modules.manifest.json`` — name -> absolute pages dir
|
|
6
6
|
- ``modules.generated.ts`` — ``import.meta.glob`` per module
|
|
7
|
-
- ``modules.generated.css`` — ``@source`` per wheel-installed module
|
|
7
|
+
- ``modules.generated.css`` — ``@source`` per wheel-installed module, plus
|
|
8
|
+
an ``@import`` per module-shipped stylesheet
|
|
9
|
+
- ``modules.assets.json`` — the richer per-module asset record the host's
|
|
10
|
+
``vite.config.ts`` builds its ``#module/<pkg>`` aliases from
|
|
8
11
|
* :func:`read_module_package_json` / :func:`collect_module_js_deps` locate
|
|
9
12
|
the per-module ``package.json`` shipped inside wheels (or alongside the
|
|
10
13
|
source for editable installs) and aggregate its ``dependencies`` block.
|
|
@@ -22,6 +25,12 @@ from pathlib import Path
|
|
|
22
25
|
|
|
23
26
|
from simple_module_core import ModuleBase, get_module_package_name
|
|
24
27
|
|
|
28
|
+
from simple_module_hosting.assets import (
|
|
29
|
+
compute_module_assets,
|
|
30
|
+
render_assets_json,
|
|
31
|
+
render_modules_css,
|
|
32
|
+
)
|
|
33
|
+
|
|
25
34
|
logger = logging.getLogger(__name__)
|
|
26
35
|
|
|
27
36
|
_GENERATED_TS_HEADER = """\
|
|
@@ -33,16 +42,6 @@ _GENERATED_TS_HEADER = """\
|
|
|
33
42
|
// pip-installed module wheels are picked up by Vite's import.meta.glob.
|
|
34
43
|
"""
|
|
35
44
|
|
|
36
|
-
_GENERATED_CSS_HEADER = """\
|
|
37
|
-
/* AUTO-GENERATED by simple_module_hosting.manifest — do not edit by hand.
|
|
38
|
-
* Regenerate with: smpy gen-pages
|
|
39
|
-
*
|
|
40
|
-
* @source entries for module pages shipped inside pip-installed wheels.
|
|
41
|
-
* In-repo modules are covered by the static @source glob in
|
|
42
|
-
* host/client_app/styles.css.
|
|
43
|
-
*/
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
45
|
|
|
47
46
|
def repo_root_from_client_app(client_app_dir: Path) -> Path:
|
|
48
47
|
"""Locate the workspace/repo root that contains ``client_app_dir``.
|
|
@@ -144,11 +143,16 @@ def write_module_pages_manifest(
|
|
|
144
143
|
output_dir: Path,
|
|
145
144
|
repo_root: Path | None = None,
|
|
146
145
|
) -> dict[str, Path]:
|
|
147
|
-
"""Write the
|
|
146
|
+
"""Write the four generated files (manifest JSON, glob TS, CSS, assets JSON).
|
|
148
147
|
|
|
149
148
|
Returns the paths that were written. Content is written only when
|
|
150
149
|
different from what's on disk, so booting the host repeatedly in dev
|
|
151
150
|
does not wake up Vite's file watcher.
|
|
151
|
+
|
|
152
|
+
``modules.manifest.json`` deliberately keeps its ``{name: pages_dir}``
|
|
153
|
+
shape: every downstream app holds its own copy of ``vite.config.ts`` and
|
|
154
|
+
reads it, so changing the value shape would break them all at once. The
|
|
155
|
+
new per-module asset record goes in ``modules.assets.json`` instead.
|
|
152
156
|
"""
|
|
153
157
|
output_dir = Path(output_dir)
|
|
154
158
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -180,32 +184,35 @@ def write_module_pages_manifest(
|
|
|
180
184
|
lines.append("")
|
|
181
185
|
generated_text = "\n".join(lines)
|
|
182
186
|
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
|
|
187
|
+
# Rendered from the richer asset record rather than pages_map, so that a
|
|
188
|
+
# module shipping CSS but no pages/ still contributes its stylesheets.
|
|
189
|
+
assets = compute_module_assets(modules)
|
|
186
190
|
css_path = output_dir / "modules.generated.css"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
continue
|
|
192
|
-
css_lines.append(f'@source "{pages_dir.as_posix()}";')
|
|
193
|
-
css_lines.append("")
|
|
194
|
-
css_text = "\n".join(css_lines)
|
|
191
|
+
css_text = render_modules_css(assets, in_repo=lambda p: _is_in_repo_module(p, repo_root))
|
|
192
|
+
|
|
193
|
+
assets_path = output_dir / "modules.assets.json"
|
|
194
|
+
assets_text = render_assets_json(assets)
|
|
195
195
|
|
|
196
196
|
wrote_manifest = _write_if_changed(manifest_path, manifest_text)
|
|
197
197
|
wrote_generated = _write_if_changed(generated_path, generated_text)
|
|
198
198
|
wrote_css = _write_if_changed(css_path, css_text)
|
|
199
|
+
wrote_assets = _write_if_changed(assets_path, assets_text)
|
|
199
200
|
|
|
200
|
-
if wrote_manifest or wrote_generated or wrote_css:
|
|
201
|
+
if wrote_manifest or wrote_generated or wrote_css or wrote_assets:
|
|
201
202
|
logger.info(
|
|
202
|
-
"Wrote module pages manifest: %d module(s) -> %s, %s, %s",
|
|
203
|
+
"Wrote module pages manifest: %d module(s) -> %s, %s, %s, %s",
|
|
203
204
|
len(pages_map),
|
|
204
205
|
manifest_path.name,
|
|
205
206
|
generated_path.name,
|
|
206
207
|
css_path.name,
|
|
208
|
+
assets_path.name,
|
|
207
209
|
)
|
|
208
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
"manifest": manifest_path,
|
|
212
|
+
"generated": generated_path,
|
|
213
|
+
"css": css_path,
|
|
214
|
+
"assets": assets_path,
|
|
215
|
+
}
|
|
209
216
|
|
|
210
217
|
|
|
211
218
|
def _glob_pattern_for(pages_dir: Path, output_dir: Path) -> str:
|
|
@@ -16,7 +16,7 @@ def _request(branding: object | None) -> SimpleNamespace:
|
|
|
16
16
|
|
|
17
17
|
def test_defaults_when_branding_not_installed() -> None:
|
|
18
18
|
meta = branding_head(_request(None))
|
|
19
|
-
assert meta == {"app_name": "SimpleModule", "theme_color": None}
|
|
19
|
+
assert meta == {"app_name": "SimpleModule", "theme_color": None, "favicon_url": None}
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def test_reads_app_name_and_theme_color() -> None:
|
|
@@ -29,4 +29,21 @@ def test_reads_app_name_and_theme_color() -> None:
|
|
|
29
29
|
def test_blank_values_fall_back() -> None:
|
|
30
30
|
settings = SimpleNamespace(app_name="", primary_color="")
|
|
31
31
|
meta = branding_head(_request(SimpleNamespace(settings=settings)))
|
|
32
|
-
assert meta == {"app_name": "SimpleModule", "theme_color": None}
|
|
32
|
+
assert meta == {"app_name": "SimpleModule", "theme_color": None, "favicon_url": None}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_favicon_url_comes_from_the_module_not_from_here() -> None:
|
|
36
|
+
# The framework must not know branding's route shape (SM009), so it reads
|
|
37
|
+
# whatever the module exposes rather than assembling a URL itself.
|
|
38
|
+
services = SimpleNamespace(
|
|
39
|
+
settings=SimpleNamespace(app_name="Acme", primary_color=""),
|
|
40
|
+
favicon_url="/api/branding/favicon?v=abc",
|
|
41
|
+
)
|
|
42
|
+
assert branding_head(_request(services))["favicon_url"] == "/api/branding/favicon?v=abc"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_favicon_url_is_none_on_a_host_without_that_attribute() -> None:
|
|
46
|
+
# An older branding release exposes no favicon_url; the shell just omits
|
|
47
|
+
# the link rather than erroring, and BrandingHead still sets it client-side.
|
|
48
|
+
services = SimpleNamespace(settings=SimpleNamespace(app_name="Acme", primary_color=""))
|
|
49
|
+
assert branding_head(_request(services))["favicon_url"] is None
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_middleware_order.py
RENAMED
|
@@ -7,7 +7,13 @@ CLAUDE.md spells out the pipeline:
|
|
|
7
7
|
|
|
8
8
|
Tenant/Locale must see ``request.state.user`` set by AuthMiddleware so
|
|
9
9
|
DB queries get filtered correctly; CorrelationId must wrap everything so
|
|
10
|
-
every log line carries its id.
|
|
10
|
+
every log line carries its id. SiteLock must precede AuthMiddleware: it gates
|
|
11
|
+
anonymous visitors itself, and if Auth ran first they would be redirected to
|
|
12
|
+
the login page — revealing that a login form exists on a site that is meant
|
|
13
|
+
to be fully hidden. That inversion breaks the feature without failing any
|
|
14
|
+
site_lock unit test, which is why the order is pinned here.
|
|
15
|
+
|
|
16
|
+
GZip sits inside the observability pair so
|
|
11
17
|
those still see every request, but outside everything that produces a body
|
|
12
18
|
— including the /static mount, which is where compression pays off most.
|
|
13
19
|
Order matters and a swap is the kind of
|
|
@@ -28,6 +34,7 @@ _EXPECTED_MULTI_TENANT = (
|
|
|
28
34
|
"GZipMiddleware",
|
|
29
35
|
"SecurityHeadersMiddleware",
|
|
30
36
|
"SessionMiddleware",
|
|
37
|
+
"SiteLockMiddleware",
|
|
31
38
|
"AuthMiddleware",
|
|
32
39
|
"TenantMiddleware",
|
|
33
40
|
"LocaleMiddleware",
|
|
@@ -40,6 +47,7 @@ _EXPECTED_SINGLE_TENANT = (
|
|
|
40
47
|
"GZipMiddleware",
|
|
41
48
|
"SecurityHeadersMiddleware",
|
|
42
49
|
"SessionMiddleware",
|
|
50
|
+
"SiteLockMiddleware",
|
|
43
51
|
"AuthMiddleware",
|
|
44
52
|
"LocaleMiddleware",
|
|
45
53
|
"InertiaLayoutDataMiddleware",
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/__init__.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/_hydrate_step.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/app_builder.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/health.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/host_settings.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/i18n_deps.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/i18n_manifest.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/inertia_deps.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/inertia_utils.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/logging.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/middleware.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/migrations.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/permissions.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/py.typed
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/redirects.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/settings.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/shared_props.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/simple_module_hosting/static_files.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_check_migrations.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_design_packs_wiring.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_error_page_shared_props.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_hosting_permissions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_inertia_manifest.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_inertia_shared_providers.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_locale_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_menu_urls_are_canonical.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_precompressed_static.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_response_compression.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_session_cookie_security.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_settings_secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_strict_discovery_wiring.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.26 → simple_module_hosting-0.0.27}/tests/test_tenant_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|