Skip to content
Merged
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
2 changes: 2 additions & 0 deletions example/vite-app/config/vite.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ class ViteConfig:
asset_url: str = ""
static_url: str = "/build"
mount_static: bool = True
template: bool = True
templates_directory: str = "resources/templates"
2 changes: 2 additions & 0 deletions fastapi_startkit/src/fastapi_startkit/vite/config/vite.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ class ViteConfig:
asset_url: str = ""
static_url: str = "/build"
mount_static: bool = True
template: bool = True
templates_directory: str = "resources/templates"
21 changes: 20 additions & 1 deletion fastapi_startkit/src/fastapi_startkit/vite/providers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ def register(self) -> None:

self.app.bind("vite", vite)

self.register_templates(config)

def register_templates(self, config: ViteConfig) -> None:
# Bind a Jinja2 template engine out of the box so fresh apps can render
# views and receive the vite() globals during boot. An existing
# "templates" binding always wins, keeping this backward-compatible.
if not config.template or self.app.has("templates"):
return

try:
from starlette.templating import Jinja2Templates
except ImportError as exc:
raise ImportError(
"Rendering Vite templates requires Jinja2. Install it with: pip install fastapi-startkit[vite]"
) from exc

templates_dir = self.app.base_path / config.templates_directory
self.app.bind("templates", Jinja2Templates(directory=str(templates_dir)))

def boot(self) -> None:
vite: Vite = self.app.make("vite")
config = self.app.make("config").get(self.provider_key)
Expand All @@ -48,7 +67,7 @@ def boot(self) -> None:
os.path.join(stubs_path, "tsconfig.json"): "tsconfig.json",
os.path.join(stubs_path, "resources/js/app.ts"): "resources/js/app.ts",
os.path.join(stubs_path, "resources/css/app.css"): "resources/css/app.css",
os.path.join(stubs_path, "templates/index.html"): "templates/index.html",
os.path.join(stubs_path, "resources/templates/index.html"): "resources/templates/index.html",
}
)

Expand Down
64 changes: 64 additions & 0 deletions fastapi_startkit/tests/vite/test_template_binding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import sys
from unittest import mock

import pytest

from fastapi_startkit.application import Application
from fastapi_startkit.providers import Provider
from fastapi_startkit.vite import ViteProvider
from fastapi_startkit.vite.config.vite import ViteConfig


def make_app(tmp_path, providers=None) -> Application:
if providers is None:
providers = [ViteProvider]
return Application(base_path=tmp_path, env="testing", providers=providers)


class _SentinelTemplatesProvider(Provider):
provider_key = "sentinel_templates"

def register(self) -> None:
self.app.bind("templates", "SENTINEL")


class TestTemplateBinding:
def test_binds_templates_when_enabled_and_none_prebound(self, tmp_path):
from starlette.templating import Jinja2Templates

app = make_app(tmp_path)

assert app.has("templates")
assert isinstance(app.make("templates"), Jinja2Templates)

def test_respects_existing_templates_binding(self, tmp_path):
app = make_app(tmp_path, providers=[_SentinelTemplatesProvider, ViteProvider])

assert app.make("templates") == "SENTINEL"

def test_skips_binding_when_template_disabled(self, tmp_path):
app = make_app(tmp_path, providers=[(ViteProvider, {"template": False})])

assert not app.has("templates")

def test_vite_globals_injected_after_boot(self, tmp_path):
app = make_app(tmp_path)

env_globals = app.make("templates").env.globals
assert "vite" in env_globals
assert "vite_asset" in env_globals
assert "vite_react_refresh" in env_globals


class TestMissingJinja2:
def test_missing_jinja2_raises_clear_install_error(self, tmp_path):
app = make_app(tmp_path, providers=[])
provider = ViteProvider(app)

# Jinja2Templates is only importable from starlette.templating when
# jinja2 is installed; setting the module to None makes the import fail.
with mock.patch.dict(sys.modules, {"starlette.templating": None}):
with pytest.raises(ImportError) as exc_info:
provider.register_templates(ViteConfig())

assert "fastapi-startkit[vite]" in str(exc_info.value)
Loading