litestar-vite 0.9.0__tar.gz → 0.11.0__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.

Potentially problematic release.


This version of litestar-vite might be problematic. Click here for more details.

Files changed (77) hide show
  1. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/PKG-INFO +6 -4
  2. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/README.md +5 -3
  3. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/pyproject.toml +4 -2
  4. litestar_vite-0.11.0/src/py/litestar_vite/__init__.py +8 -0
  5. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/cli.py +1 -1
  6. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/commands.py +1 -1
  7. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/config.py +1 -67
  8. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/middleware.py +1 -2
  9. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/response.py +8 -9
  10. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/loader.py +66 -3
  11. litestar_vite-0.11.0/src/py/litestar_vite/plugin.py +196 -0
  12. litestar_vite-0.11.0/src/py/tests/conftest.py +39 -0
  13. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/app.py +8 -2
  14. litestar_vite-0.11.0/src/py/tests/test_asset_loader.py +153 -0
  15. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_cli/test_init.py +16 -7
  16. litestar_vite-0.11.0/src/py/tests/test_inertia/conftest.py +33 -0
  17. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_inertia/test_inertia_request.py +27 -7
  18. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_inertia/test_inertia_response.py +33 -9
  19. litestar_vite-0.9.0/src/py/litestar_vite/__init__.py +0 -9
  20. litestar_vite-0.9.0/src/py/litestar_vite/plugin.py +0 -136
  21. litestar_vite-0.9.0/src/py/litestar_vite/template_engine.py +0 -103
  22. litestar_vite-0.9.0/src/py/tests/conftest.py +0 -27
  23. litestar_vite-0.9.0/src/py/tests/test_inertia/conftest.py +0 -30
  24. litestar_vite-0.9.0/src/py/tests/test_template_engine.py +0 -148
  25. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/.gitignore +0 -0
  26. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/LICENSE +0 -0
  27. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/LICENSE +0 -0
  28. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/Makefile +0 -0
  29. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/NOTICE +0 -0
  30. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/README.md +0 -0
  31. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/src/dev-server-index.html +0 -0
  32. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/src/index.ts +0 -0
  33. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/src/inertia-helpers/index.ts +0 -0
  34. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/tests/__data__/dummy.ts +0 -0
  35. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/tests/index.test.ts +0 -0
  36. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/tsconfig.inertia-helpers.json +0 -0
  37. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/tsconfig.json +0 -0
  38. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/vitest.config.ts +0 -0
  39. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/js/vitest.workspace.ts +0 -0
  40. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/__metadata__.py +0 -0
  41. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/__init__.py +0 -0
  42. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/_utils.py +0 -0
  43. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/config.py +0 -0
  44. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/exception_handler.py +0 -0
  45. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/plugin.py +0 -0
  46. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/request.py +0 -0
  47. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/routes.py +0 -0
  48. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/inertia/types.py +0 -0
  49. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/py.typed +0 -0
  50. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/templates/__init__.py +0 -0
  51. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/templates/index.html.j2 +0 -0
  52. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/templates/main.ts.j2 +0 -0
  53. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/templates/package.json.j2 +0 -0
  54. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/templates/styles.css.j2 +0 -0
  55. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/templates/tsconfig.json.j2 +0 -0
  56. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/litestar_vite/templates/vite.config.ts.j2 +0 -0
  57. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/__init__.py +0 -0
  58. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/templates/__init__.py +0 -0
  59. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/templates/index.html.j2 +0 -0
  60. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/__init__.py +0 -0
  61. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/__init__.py +0 -0
  62. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/public/.gitkeep +0 -0
  63. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/public/assets/main-l0sNRNKZ.js +0 -0
  64. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/public/assets/styles-l0sNRNKZ.js +0 -0
  65. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/public/manifest.json +0 -0
  66. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/resources/.gitkeep +0 -0
  67. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/resources/main.ts +0 -0
  68. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/resources/styles.css +0 -0
  69. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/templates/.gitkeep +0 -0
  70. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_app/web/templates/index.html +0 -0
  71. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_cli/__init__.py +0 -0
  72. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_cli/conftest.py +0 -0
  73. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_commands.py +0 -0
  74. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_config.py +0 -0
  75. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_inertia/__init__.py +0 -0
  76. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_inertia/templates/index.html.j2 +0 -0
  77. {litestar_vite-0.9.0 → litestar_vite-0.11.0}/src/py/tests/test_inertia/test_routes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: litestar-vite
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: Vite plugin for Litestar
5
5
  Project-URL: Changelog, https://cofin.github.io/litestar-vite/latest/changelog
6
6
  Project-URL: Discord, https://discord.gg/X3FJqy8d2j
@@ -53,6 +53,8 @@ from pathlib import Path
53
53
  from litestar import Controller, get, Litestar
54
54
  from litestar.response import Template
55
55
  from litestar.status_codes import HTTP_200_OK
56
+ from litestar.template.config import TemplateConfig
57
+ from litestar.contrib.jinja import JinjaTemplateEngine
56
58
  from litestar_vite import ViteConfig, VitePlugin
57
59
 
58
60
  class WebController(Controller):
@@ -64,9 +66,9 @@ class WebController(Controller):
64
66
  async def index(self) -> Template:
65
67
  return Template(template_name="index.html.j2")
66
68
 
67
-
68
- vite = VitePlugin(config=ViteConfig(template_dir='templates/'))
69
- app = Litestar(plugins=[vite], route_handlers=[WebController])
69
+ template_config = TemplateConfig(engine=JinjaTemplateEngine(directory='templates/'))
70
+ vite = VitePlugin(config=ViteConfig())
71
+ app = Litestar(plugins=[vite], template_config=template_config, route_handlers=[WebController])
70
72
 
71
73
  ```
72
74
 
@@ -18,6 +18,8 @@ from pathlib import Path
18
18
  from litestar import Controller, get, Litestar
19
19
  from litestar.response import Template
20
20
  from litestar.status_codes import HTTP_200_OK
21
+ from litestar.template.config import TemplateConfig
22
+ from litestar.contrib.jinja import JinjaTemplateEngine
21
23
  from litestar_vite import ViteConfig, VitePlugin
22
24
 
23
25
  class WebController(Controller):
@@ -29,9 +31,9 @@ class WebController(Controller):
29
31
  async def index(self) -> Template:
30
32
  return Template(template_name="index.html.j2")
31
33
 
32
-
33
- vite = VitePlugin(config=ViteConfig(template_dir='templates/'))
34
- app = Litestar(plugins=[vite], route_handlers=[WebController])
34
+ template_config = TemplateConfig(engine=JinjaTemplateEngine(directory='templates/'))
35
+ vite = VitePlugin(config=ViteConfig())
36
+ app = Litestar(plugins=[vite], template_config=template_config, route_handlers=[WebController])
35
37
 
36
38
  ```
37
39
 
@@ -25,7 +25,7 @@ license = { text = "MIT" }
25
25
  name = "litestar-vite"
26
26
  readme = "README.md"
27
27
  requires-python = ">=3.8"
28
- version = "0.9.0"
28
+ version = "0.11.0"
29
29
 
30
30
  [project.urls]
31
31
  Changelog = "https://cofin.github.io/litestar-vite/latest/changelog"
@@ -85,7 +85,7 @@ test = [
85
85
  allow_dirty = true
86
86
  commit = true
87
87
  commit_args = "--no-verify"
88
- current_version = "0.9.0"
88
+ current_version = "0.11.0"
89
89
  ignore_missing_files = false
90
90
  ignore_missing_version = false
91
91
  message = "chore(release): bump to v{new_version}"
@@ -267,6 +267,8 @@ lint.select = ["ALL"]
267
267
  lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
268
268
  src = ["src/py/litestar_vite", "src/py/tests"]
269
269
  target-version = "py38"
270
+ unsafe-fixes = true
271
+
270
272
 
271
273
  [tool.ruff.lint.pydocstyle]
272
274
  convention = "google"
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from litestar_vite import inertia
4
+ from litestar_vite.config import ViteConfig
5
+ from litestar_vite.loader import ViteAssetLoader
6
+ from litestar_vite.plugin import VitePlugin
7
+
8
+ __all__ = ("ViteAssetLoader", "ViteConfig", "VitePlugin", "inertia")
@@ -113,8 +113,8 @@ def vite_init(
113
113
  )
114
114
  from rich.prompt import Confirm
115
115
 
116
+ from litestar_vite import VitePlugin
116
117
  from litestar_vite.commands import execute_command, init_vite
117
- from litestar_vite.plugin import VitePlugin
118
118
 
119
119
  if callable(ctx.obj):
120
120
  ctx.obj = ctx.obj()
@@ -14,7 +14,7 @@ DEFAULT_RESOURCES: set[str] = {"styles.css.j2", "main.ts.j2"}
14
14
  DEFAULT_DEV_DEPENDENCIES: dict[str, str] = {
15
15
  "typescript": "^5.7.2",
16
16
  "vite": "^6.0.3",
17
- "litestar-vite-plugin": "^0.9.0",
17
+ "litestar-vite-plugin": "^0.11.0",
18
18
  "@types/node": "^22.10.1",
19
19
  }
20
20
  DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.2"}
@@ -2,21 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  from dataclasses import dataclass, field
5
- from functools import cached_property
6
- from inspect import isclass
7
5
  from pathlib import Path
8
- from typing import TYPE_CHECKING, cast
9
6
 
10
- from litestar.exceptions import ImproperlyConfiguredException
11
- from litestar.template import TemplateConfig
12
- from litestar.template.config import EngineType
13
-
14
- if TYPE_CHECKING:
15
- from collections.abc import Callable
16
-
17
- from litestar.types import PathType
18
-
19
- __all__ = ("ViteConfig", "ViteTemplateConfig")
7
+ __all__ = ("ViteConfig",)
20
8
  TRUE_VALUES = {"True", "true", "1", "yes", "Y", "T"}
21
9
 
22
10
 
@@ -39,8 +27,6 @@ class ViteConfig:
39
27
 
40
28
  In a standalone Vue or React application, this would be equivalent to the ``./src`` directory.
41
29
  """
42
- template_dir: Path | str | None = field(default="templates")
43
- """Location of the Jinja2 template file."""
44
30
  public_dir: Path | str = field(default="public")
45
31
  """The optional public directory Vite serves assets from.
46
32
 
@@ -113,8 +99,6 @@ class ViteConfig:
113
99
  self.root_dir = Path(self.root_dir)
114
100
  elif self.root_dir is None:
115
101
  self.root_dir = Path()
116
- if self.template_dir is not None and isinstance(self.template_dir, str):
117
- self.template_dir = Path(self.template_dir)
118
102
  if self.public_dir and isinstance(self.public_dir, str):
119
103
  self.public_dir = Path(self.public_dir)
120
104
  if isinstance(self.resource_dir, str):
@@ -123,53 +107,3 @@ class ViteConfig:
123
107
  self.bundle_dir = Path(self.bundle_dir)
124
108
  if isinstance(self.ssr_output_dir, str):
125
109
  self.ssr_output_dir = Path(self.ssr_output_dir)
126
-
127
-
128
- @dataclass
129
- class ViteTemplateConfig(TemplateConfig[EngineType]):
130
- """Configuration for Templating.
131
-
132
- To enable templating, pass an instance of this class to the
133
- :class:`Litestar <litestar.app.Litestar>` constructor using the
134
- 'template_config' key.
135
- """
136
-
137
- config: ViteConfig = field(default_factory=lambda: ViteConfig())
138
- """A a config for the vite engine`."""
139
- engine: type[EngineType] | EngineType | None = field(default=None)
140
- """A template engine adhering to the :class:`TemplateEngineProtocol <litestar.template.TemplateEngineProtocol>`."""
141
- directory: PathType | list[PathType] | None = field(default=None)
142
- """A directory or list of directories from which to serve templates."""
143
- engine_callback: Callable[[EngineType], None] | None = field(default=None)
144
- """A callback function that allows modifying the instantiated templating
145
- protocol."""
146
-
147
- instance: EngineType | None = field(default=None)
148
- """An instance of the templating protocol."""
149
-
150
- def __post_init__(self) -> None:
151
- """Ensure that directory is set if engine is a class."""
152
- if isclass(self.engine) and not self.directory: # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
153
- msg = "directory is a required kwarg when passing a template engine class"
154
- raise ImproperlyConfiguredException(msg)
155
- """Ensure that directory is not set if instance is."""
156
- if self.instance is not None and self.directory is not None: # pyright: ignore[reportUnknownMemberType]
157
- msg = "directory cannot be set if instance is"
158
- raise ImproperlyConfiguredException(msg)
159
-
160
- def to_engine(self) -> EngineType:
161
- """Instantiate the template engine."""
162
- template_engine = cast(
163
- "EngineType",
164
- self.engine(directory=self.directory, config=self.config, engine_instance=None) # pyright: ignore[reportUnknownMemberType,reportCallIssue]
165
- if isclass(self.engine) # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
166
- else self.engine, # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
167
- )
168
- if callable(self.engine_callback):
169
- self.engine_callback(template_engine) # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
170
- return template_engine
171
-
172
- @cached_property
173
- def engine_instance(self) -> EngineType:
174
- """Return the template engine instance."""
175
- return self.to_engine() if self.instance is None else self.instance
@@ -26,8 +26,7 @@ async def redirect_on_asset_version_mismatch(request: Request[UserT, AuthT, Stat
26
26
  return None
27
27
 
28
28
  vite_plugin = request.app.plugins.get(VitePlugin)
29
- template_engine = vite_plugin.template_config.to_engine()
30
- if inertia_version == template_engine.asset_loader.version_id:
29
+ if inertia_version == vite_plugin.asset_loader.version_id:
31
30
  return None
32
31
  return InertiaRedirect(request, redirect_to=str(request.url))
33
32
 
@@ -245,7 +245,7 @@ class InertiaResponse(Response[T]):
245
245
  is_partial_render = cast("bool", getattr(request, "is_partial_render", False))
246
246
  partial_keys = cast("set[str]", getattr(request, "partial_keys", {}))
247
247
  vite_plugin = request.app.plugins.get(VitePlugin)
248
- template_engine = vite_plugin.template_config.to_engine()
248
+ template_engine = request.app.template_engine # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
249
249
  headers.update(
250
250
  {"Vary": "Accept", **get_headers(InertiaHeaderType(enabled=True))},
251
251
  )
@@ -254,13 +254,13 @@ class InertiaResponse(Response[T]):
254
254
  page_props = PageProps[T](
255
255
  component=request.inertia.route_component, # type: ignore[attr-defined] # pyright: ignore[reportUnknownArgumentType,reportUnknownMemberType,reportAttributeAccessIssue]
256
256
  props=shared_props, # pyright: ignore[reportArgumentType]
257
- version=template_engine.asset_loader.version_id,
257
+ version=vite_plugin.asset_loader.version_id,
258
258
  url=request.url.path,
259
259
  )
260
260
  if is_inertia:
261
261
  media_type = get_enum_string_value(self.media_type or media_type or MediaType.JSON)
262
262
  body = self.render(page_props, media_type, get_serializer(type_encoders))
263
- return ASGIResponse(
263
+ return ASGIResponse( # pyright: ignore[reportUnknownMemberType]
264
264
  background=self.background or background,
265
265
  body=body,
266
266
  cookies=cookies,
@@ -290,17 +290,16 @@ class InertiaResponse(Response[T]):
290
290
  media_type = MediaType.HTML
291
291
  context = self.create_template_context(request, page_props, type_encoders) # pyright: ignore[reportUnknownMemberType]
292
292
  if self.template_str is not None:
293
- body = template_engine.render_string(self.template_str, context).encode(self.encoding)
293
+ body = template_engine.render_string(self.template_str, context).encode(self.encoding) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
294
294
  else:
295
295
  inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
296
296
  template_name = self.template_name or inertia_plugin.config.root_template
297
- # cast to str b/c we know that either template_name cannot be None if template_str is None
298
- template = template_engine.get_template(template_name)
299
- body = template.render(**context).encode(self.encoding)
297
+ template = template_engine.get_template(template_name) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
298
+ body = template.render(**context).encode(self.encoding) # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
300
299
 
301
- return ASGIResponse(
300
+ return ASGIResponse( # pyright: ignore[reportUnknownMemberType]
302
301
  background=self.background or background,
303
- body=body,
302
+ body=body, # pyright: ignore[reportUnknownArgumentType]
304
303
  cookies=cookies,
305
304
  encoded_headers=encoded_headers,
306
305
  encoding=self.encoding,
@@ -4,11 +4,61 @@ import json
4
4
  from functools import cached_property
5
5
  from pathlib import Path
6
6
  from textwrap import dedent
7
- from typing import TYPE_CHECKING, Any, ClassVar
7
+ from typing import TYPE_CHECKING, Any, ClassVar, Mapping, cast
8
8
  from urllib.parse import urljoin
9
9
 
10
+ import markupsafe
11
+ from litestar.exceptions import ImproperlyConfiguredException
12
+
10
13
  if TYPE_CHECKING:
14
+ from litestar.connection import Request
15
+
11
16
  from litestar_vite.config import ViteConfig
17
+ from litestar_vite.plugin import VitePlugin
18
+
19
+
20
+ def _get_request_from_context(context: Mapping[str, Any]) -> Request[Any, Any, Any]:
21
+ """Get the request from the template context.
22
+
23
+ Args:
24
+ context: The template context.
25
+
26
+ Returns:
27
+ The request object.
28
+ """
29
+ return cast("Request[Any, Any, Any]", context["request"])
30
+
31
+
32
+ def render_hmr_client(context: Mapping[str, Any], /) -> markupsafe.Markup:
33
+ """Render the HMR client.
34
+
35
+ Args:
36
+ context: The template context.
37
+
38
+ Returns:
39
+ The HMR client.
40
+ """
41
+ return cast(
42
+ "VitePlugin", _get_request_from_context(context).app.plugins.get("VitePlugin")
43
+ ).asset_loader.render_hmr_client()
44
+
45
+
46
+ def render_asset_tag(
47
+ context: Mapping[str, Any], /, path: str | list[str], scripts_attrs: dict[str, str] | None = None
48
+ ) -> markupsafe.Markup:
49
+ """Render an asset tag.
50
+
51
+ Args:
52
+ context: The template context.
53
+ path: The path to the asset.
54
+ scripts_attrs: The attributes for the script tag.
55
+
56
+ Returns:
57
+ The asset tag.
58
+ """
59
+ return cast(
60
+ "VitePlugin", _get_request_from_context(context).app.plugins.get("VitePlugin")
61
+ ).asset_loader.render_asset_tag(path, scripts_attrs)
12
62
 
13
63
 
14
64
  class ViteAssetLoader:
@@ -39,6 +89,19 @@ class ViteAssetLoader:
39
89
  return str(hash(self.manifest_content))
40
90
  return "1.0"
41
91
 
92
+ def render_hmr_client(self) -> markupsafe.Markup:
93
+ """Generate the script tag for the Vite WS client for HMR."""
94
+ return markupsafe.Markup(
95
+ f"{self.generate_react_hmr_tags()}{self.generate_ws_client_tags()}",
96
+ )
97
+
98
+ def render_asset_tag(self, path: str | list[str], scripts_attrs: dict[str, str] | None = None) -> markupsafe.Markup:
99
+ """Generate all assets include tags for the file in argument."""
100
+ path = [str(p) for p in path] if isinstance(path, list) else [str(path)]
101
+ return markupsafe.Markup(
102
+ "".join([self.generate_asset_tags(p, scripts_attrs=scripts_attrs) for p in path]),
103
+ )
104
+
42
105
  def parse_manifest(self) -> None:
43
106
  """Parse the Vite manifest file.
44
107
 
@@ -151,7 +214,7 @@ class ViteAssetLoader:
151
214
 
152
215
  if any(p for p in path if p not in self._manifest):
153
216
  msg = "Cannot find %s in Vite manifest at %s. Did you forget to build your assets after an update?"
154
- raise RuntimeError(
217
+ raise ImproperlyConfiguredException(
155
218
  msg,
156
219
  path,
157
220
  Path(f"{self._config.bundle_dir}/{self._config.manifest_name}"),
@@ -159,7 +222,7 @@ class ViteAssetLoader:
159
222
 
160
223
  tags: list[str] = []
161
224
  manifest_entry: dict[str, Any] = {}
162
- manifest_entry.update({p: self._manifest[p] for p in path})
225
+ manifest_entry.update({p: self._manifest[p] for p in path if p})
163
226
  if not scripts_attrs:
164
227
  scripts_attrs = {"type": "module", "async": "", "defer": ""}
165
228
  for manifest in manifest_entry.values():
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import signal
5
+ import threading
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Iterator, cast
9
+
10
+ from litestar.contrib.jinja import JinjaTemplateEngine
11
+ from litestar.exceptions import ImproperlyConfiguredException
12
+ from litestar.plugins import CLIPlugin, InitPluginProtocol
13
+ from litestar.static_files import create_static_files_router # pyright: ignore[reportUnknownVariableType]
14
+
15
+ if TYPE_CHECKING:
16
+ from click import Group
17
+ from litestar import Litestar
18
+ from litestar.config.app import AppConfig
19
+
20
+ from litestar_vite.config import ViteConfig
21
+ from litestar_vite.loader import ViteAssetLoader
22
+
23
+
24
+ def set_environment(config: ViteConfig) -> None:
25
+ """Configure environment for easier integration"""
26
+ os.environ.setdefault("ASSET_URL", config.asset_url)
27
+ os.environ.setdefault("VITE_ALLOW_REMOTE", str(True))
28
+ os.environ.setdefault("VITE_PORT", str(config.port))
29
+ os.environ.setdefault("VITE_HOST", config.host)
30
+ os.environ.setdefault("VITE_PROTOCOL", config.protocol)
31
+ os.environ.setdefault("APP_URL", f"http://localhost:{os.environ.get('LITESTAR_PORT', 8000)}")
32
+ if config.dev_mode:
33
+ os.environ.setdefault("VITE_DEV_MODE", str(config.dev_mode))
34
+
35
+
36
+ class ViteProcess:
37
+ """Manages the Vite process."""
38
+
39
+ def __init__(self) -> None:
40
+ self.process: threading.Thread | None = None
41
+ self._lock = threading.Lock()
42
+
43
+ def start(self, command: list[str], cwd: Path | str | None) -> None:
44
+ """Start the Vite process."""
45
+ from litestar.cli._utils import console
46
+
47
+ from litestar_vite.commands import execute_command
48
+
49
+ try:
50
+ with self._lock:
51
+ if self.process and self.process.is_alive():
52
+ return
53
+
54
+ self.process = threading.Thread(
55
+ name="vite",
56
+ target=execute_command,
57
+ args=[],
58
+ kwargs={"command_to_run": command, "cwd": cwd},
59
+ daemon=True, # Make thread daemon so it exits when main thread exits
60
+ )
61
+ console.print(f"Starting Vite process with command: {command}")
62
+ self.process.start()
63
+ except Exception as e:
64
+ console.print(f"[red]Failed to start Vite process: {e!s}[/]")
65
+ raise
66
+
67
+ def stop(self, timeout: float = 5.0) -> None:
68
+ """Stop the Vite process."""
69
+ from litestar.cli._utils import console
70
+
71
+ try:
72
+ with self._lock:
73
+ if self.process and self.process.is_alive():
74
+ # Send SIGTERM to child process
75
+ if hasattr(signal, "SIGTERM") and self.process.ident is not None:
76
+ os.kill(self.process.ident, signal.SIGTERM)
77
+ self.process.join(timeout=timeout)
78
+
79
+ # Force kill if still alive
80
+ if self.process.is_alive():
81
+ if hasattr(signal, "SIGKILL") and self.process.ident is not None:
82
+ os.kill(self.process.ident, signal.SIGKILL)
83
+ self.process.join(timeout=1.0)
84
+ console.print("Stopping Vite process")
85
+ except Exception as e:
86
+ console.print(f"[red]Failed to stop Vite process: {e!s}[/]")
87
+ raise
88
+
89
+
90
+ class VitePlugin(InitPluginProtocol, CLIPlugin):
91
+ """Vite plugin."""
92
+
93
+ __slots__ = ("_asset_loader", "_config", "_vite_process")
94
+
95
+ def __init__(self, config: ViteConfig | None = None, asset_loader: ViteAssetLoader | None = None) -> None:
96
+ """Initialize ``Vite``.
97
+
98
+ Args:
99
+ config: configuration to use for starting Vite. The default configuration will be used if it is not provided.
100
+ asset_loader: an initialized asset loader to use for rendering asset tags.
101
+ """
102
+ from litestar_vite.config import ViteConfig
103
+
104
+ if config is None:
105
+ config = ViteConfig()
106
+ self._config = config
107
+ self._asset_loader = asset_loader
108
+ self._vite_process = ViteProcess()
109
+
110
+ @property
111
+ def config(self) -> ViteConfig:
112
+ return self._config
113
+
114
+ @property
115
+ def asset_loader(self) -> ViteAssetLoader:
116
+ from litestar_vite.loader import ViteAssetLoader
117
+
118
+ if self._asset_loader is None:
119
+ self._asset_loader = ViteAssetLoader.initialize_loader(config=self._config)
120
+ return self._asset_loader
121
+
122
+ def on_cli_init(self, cli: Group) -> None:
123
+ from litestar_vite.cli import vite_group
124
+
125
+ cli.add_command(vite_group)
126
+
127
+ def on_app_init(self, app_config: AppConfig) -> AppConfig:
128
+ """Configure application for use with Vite.
129
+
130
+ Args:
131
+ app_config: The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
132
+ """
133
+ from litestar_vite.loader import render_asset_tag, render_hmr_client
134
+
135
+ if app_config.template_config is None: # pyright: ignore[reportUnknownMemberType]
136
+ msg = "A template configuration is required for Vite."
137
+ raise ImproperlyConfiguredException(msg)
138
+ if not isinstance(app_config.template_config.engine_instance, JinjaTemplateEngine): # pyright: ignore[reportUnknownMemberType]
139
+ msg = "Jinja2 template engine is required for Vite."
140
+ raise ImproperlyConfiguredException(msg)
141
+ app_config.template_config.engine_instance.register_template_callable( # pyright: ignore[reportUnknownMemberType]
142
+ key="vite_hmr",
143
+ template_callable=render_hmr_client,
144
+ )
145
+ app_config.template_config.engine_instance.register_template_callable( # pyright: ignore[reportUnknownMemberType]
146
+ key="vite",
147
+ template_callable=render_asset_tag,
148
+ )
149
+ if self._config.set_static_folders:
150
+ static_dirs = [Path(self._config.bundle_dir), Path(self._config.resource_dir)]
151
+ if Path(self._config.public_dir).exists() and self._config.public_dir != self._config.bundle_dir:
152
+ static_dirs.append(Path(self._config.public_dir))
153
+ app_config.route_handlers.append(
154
+ create_static_files_router(
155
+ directories=cast( # type: ignore[arg-type]
156
+ "list[Path]",
157
+ static_dirs if self._config.dev_mode else [Path(self._config.bundle_dir)],
158
+ ),
159
+ path=self._config.asset_url,
160
+ name="vite",
161
+ html_mode=False,
162
+ include_in_schema=False,
163
+ opt={"exclude_from_auth": True},
164
+ ),
165
+ )
166
+ return app_config
167
+
168
+ @contextmanager
169
+ def server_lifespan(self, app: Litestar) -> Iterator[None]:
170
+ """Manage Vite server process lifecycle."""
171
+ from litestar.cli._utils import console
172
+
173
+ if self._config.use_server_lifespan and self._config.dev_mode:
174
+ command_to_run = self._config.run_command if self._config.hot_reload else self._config.build_watch_command
175
+
176
+ if self.config.hot_reload:
177
+ console.rule("[yellow]Starting Vite process with HMR Enabled[/]", align="left")
178
+ else:
179
+ console.rule("[yellow]Starting Vite watch and build process[/]", align="left")
180
+
181
+ if self._config.set_environment:
182
+ set_environment(config=self._config)
183
+
184
+ try:
185
+ self._vite_process.start(command_to_run, self._config.root_dir)
186
+ yield
187
+ finally:
188
+ self._vite_process.stop()
189
+ console.print("[yellow]Vite process stopped.[/]")
190
+ else:
191
+ manifest_path = Path(f"{self._config.bundle_dir}/{self._config.manifest_name}")
192
+ if manifest_path.exists():
193
+ console.rule(f"[yellow]Serving assets using manifest at `{manifest_path!s}`.[/]", align="left")
194
+ else:
195
+ console.rule(f"[yellow]Serving assets without manifest at `{manifest_path!s}`.[/]", align="left")
196
+ yield
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Generator
5
+
6
+ import pytest
7
+ from litestar.contrib.jinja import JinjaTemplateEngine
8
+ from litestar.template.config import TemplateConfig
9
+
10
+ from litestar_vite.config import ViteConfig
11
+
12
+ pytestmark = pytest.mark.anyio
13
+ here = Path(__file__).parent
14
+
15
+
16
+ @pytest.fixture
17
+ def anyio_backend() -> str:
18
+ return "asyncio"
19
+
20
+
21
+ @pytest.fixture
22
+ def test_app_path() -> Generator[Path, None, None]:
23
+ yield Path(here / "test_app" / "web")
24
+
25
+
26
+ @pytest.fixture
27
+ def template_config(test_app_path: Path) -> TemplateConfig[JinjaTemplateEngine]:
28
+ return TemplateConfig(engine=JinjaTemplateEngine(directory=test_app_path / "templates"))
29
+
30
+
31
+ # Define a fixture for ViteConfig
32
+ @pytest.fixture
33
+ def vite_config(test_app_path: Path) -> ViteConfig:
34
+ # Mock the ViteConfig with necessary attributes for testing
35
+ return ViteConfig(
36
+ bundle_dir=test_app_path / "public",
37
+ resource_dir=test_app_path / "resources",
38
+ hot_reload=True,
39
+ )
@@ -3,17 +3,23 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
 
5
5
  from litestar import Litestar
6
+ from litestar.contrib.jinja import JinjaTemplateEngine
7
+ from litestar.template.config import TemplateConfig
6
8
 
7
9
  from litestar_vite import ViteConfig, VitePlugin
8
10
 
9
11
  here = Path(__file__).parent
10
12
 
13
+ template_config = TemplateConfig(
14
+ engine=JinjaTemplateEngine(
15
+ directory=Path(here / "web" / "templates"),
16
+ )
17
+ )
11
18
  vite = VitePlugin(
12
19
  config=ViteConfig(
13
20
  bundle_dir=Path(here / "web" / "public"),
14
21
  resource_dir=Path(here / "web" / "resources"),
15
- template_dir=Path(here / "web" / "templates"),
16
22
  hot_reload=True,
17
23
  ),
18
24
  )
19
- app = Litestar(plugins=[vite])
25
+ app = Litestar(plugins=[vite], template_config=template_config)