Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion framework/cli/tests/test_module_css_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,33 @@ async def test_detects_theme_and_styles(self, tmp_path):
assert entry.styles_css is not None and entry.styles_css.name == "styles.css"
assert entry.pages_dir is not None

async def test_detects_components_dir(self, tmp_path):
"""A module shipping components/ has it detected — its widgets carry
Tailwind classes the build has to scan just like a page's."""
from simple_module_hosting.assets import compute_module_assets

mod, pkg = _make_importable_module(tmp_path, "widget_mod", "Widgets")
(pkg / "components").mkdir()

result = compute_module_assets([mod])

assert len(result) == 1
assert result[0].components_dir is not None
assert result[0].components_dir.name == "components"

async def test_components_only_module_is_included(self, tmp_path):
"""A module with only components/ (no pages/, no css) still appears."""
from simple_module_hosting.assets import compute_module_assets

mod, pkg = _make_importable_module(tmp_path, "conly_mod", "ComponentsOnly")
(pkg / "components").mkdir()

result = compute_module_assets([mod])

assert [e.name for e in result] == ["ComponentsOnly"]
assert result[0].components_dir is not None
assert result[0].pages_dir is None

async def test_css_only_module_is_included(self, tmp_path):
"""A module with CSS but no pages/ still appears — the manifest.json gap."""
from simple_module_hosting.assets import compute_module_assets
Expand Down Expand Up @@ -94,6 +121,7 @@ def _assets(tmp_path: Path, **overrides):
"package_name": "gis",
"package_dir": tmp_path / "gis",
"pages_dir": None,
"components_dir": None,
"theme_css": None,
"styles_css": None,
}
Expand Down Expand Up @@ -142,6 +170,34 @@ async def test_source_emitted_for_wheel_modules(self, tmp_path):

assert f'@source "{pages.as_posix()}/**/*.{{ts,tsx}}";' in css

async def test_components_source_emitted_for_wheel_modules(self, tmp_path):
"""A wheel module's components/ gets its own absolute @source glob.

This is the class scanning a widget's ``lg:flex`` depends on. Emitting
the resolved path via ``as_posix`` (not a hand-written
``.venv/lib/python3.x/...`` line) is what keeps the build working on a
Windows ``.venv/Lib/site-packages`` layout too.
"""
from simple_module_hosting.assets import render_modules_css

components = tmp_path / "gis" / "components"
css = render_modules_css(
[_assets(tmp_path, components_dir=components)], in_repo=lambda _p: False
)

assert f'@source "{components.as_posix()}/**/*.{{ts,tsx}}";' in css

async def test_components_source_skips_in_repo(self, tmp_path):
"""In-repo module components are already covered by the host's static glob."""
from simple_module_hosting.assets import render_modules_css

css = render_modules_css(
[_assets(tmp_path, components_dir=tmp_path / "gis" / "components")],
in_repo=lambda _p: True,
)

assert "@source" not in css

async def test_module_without_css_emits_no_import(self, tmp_path):
"""Pages-only modules contribute @source but no @import."""
from simple_module_hosting.assets import render_modules_css
Expand Down Expand Up @@ -203,7 +259,7 @@ async def test_writes_assets_json(self, tmp_path):
entry = data["Dashboard"]
assert entry["package_name"] == "dashboard"
assert entry["package"].endswith("dashboard")
assert set(entry) == {"package_name", "package", "pages", "theme", "styles"}
assert set(entry) == {"package_name", "package", "pages", "components", "theme", "styles"}

async def test_manifest_json_shape_unchanged(self, tmp_path):
"""modules.manifest.json stays {name: pages_dir} for downstream vite configs.
Expand Down
26 changes: 20 additions & 6 deletions framework/hosting/simple_module_hosting/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class ModuleAssets:
package_name: str
package_dir: Path
pages_dir: Path | None
components_dir: Path | None
theme_css: Path | None
styles_css: Path | None

Expand All @@ -69,17 +70,19 @@ def compute_module_assets(modules: Sequence[ModuleBase]) -> list[ModuleAssets]:
)
continue
pages_dir = pkg_root / "pages"
components_dir = pkg_root / "components"
theme = pkg_root / THEME_CSS
styles = pkg_root / STYLES_CSS
entry = ModuleAssets(
name=mod.meta.name,
package_name=pkg_name,
package_dir=pkg_root.resolve(),
pages_dir=pages_dir.resolve() if pages_dir.is_dir() else None,
components_dir=components_dir.resolve() if components_dir.is_dir() else None,
theme_css=theme.resolve() if theme.is_file() else None,
styles_css=styles.resolve() if styles.is_file() else None,
)
if entry.pages_dir or entry.theme_css or entry.styles_css:
if entry.pages_dir or entry.components_dir or entry.theme_css or entry.styles_css:
result.append(entry)
return result

Expand All @@ -104,16 +107,26 @@ def render_modules_css(
) -> str:
"""Render the contents of ``modules.generated.css``.

``@source`` is emitted only for wheel-installed modules — in-repo module
pages are already covered by the static ``@source`` glob in the host's
``styles.css``, so emitting an absolute one too would just duplicate it.
``@source`` is emitted for a wheel-installed module's ``pages/`` **and**
``components/`` — both hold ``.tsx`` carrying Tailwind classes (a widget's
``lg:flex`` is as real as a page's), and neither is covered by the host's
static glob, which only reaches in-repo ``modules/*``. In-repo modules are
skipped here because that static glob already covers them; emitting an
absolute path too would just duplicate it.

These are absolute ``as_posix()`` paths, so the same generated file works on
POSIX and Windows — unlike a hand-written ``.venv/lib/python3.x/...`` line,
which silently matches nothing on a Windows ``.venv/Lib/site-packages``
layout and drops every widget class from the build.

``@import`` is emitted for *every* module, in-repo and wheel alike, because
there is no static-glob equivalent for CSS.
"""
source_lines = [
f'@source "{e.pages_dir.as_posix()}/**/*.{{ts,tsx}}";'
f'@source "{d.as_posix()}/**/*.{{ts,tsx}}";'
for e in assets
if e.pages_dir and not in_repo(e.pages_dir)
for d in (e.pages_dir, e.components_dir)
if d and not in_repo(d)
]
theme_lines = [
f'@import "{ALIAS_PREFIX}/{e.package_name}/{THEME_CSS}";' for e in assets if e.theme_css
Expand Down Expand Up @@ -152,6 +165,7 @@ def render_assets_json(assets: Sequence[ModuleAssets]) -> str:
"package_name": e.package_name,
"package": e.package_dir.as_posix(),
"pages": e.pages_dir.as_posix() if e.pages_dir else None,
"components": e.components_dir.as_posix() if e.components_dir else None,
"theme": e.theme_css.as_posix() if e.theme_css else None,
"styles": e.styles_css.as_posix() if e.styles_css else None,
}
Expand Down
Loading