litestar-vite 0.11.1__tar.gz → 0.12.1__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 (76) hide show
  1. {litestar_vite-0.11.1 → litestar_vite-0.12.1}/.gitignore +1 -1
  2. {litestar_vite-0.11.1 → litestar_vite-0.12.1}/PKG-INFO +3 -2
  3. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/commands.py +4 -4
  4. litestar_vite-0.12.1/litestar_vite/inertia/__init__.py +37 -0
  5. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/exception_handler.py +2 -1
  6. litestar_vite-0.12.1/litestar_vite/inertia/helpers.py +329 -0
  7. litestar_vite-0.12.1/litestar_vite/inertia/plugin.py +96 -0
  8. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/response.py +33 -86
  9. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/plugin.py +9 -15
  10. {litestar_vite-0.11.1 → litestar_vite-0.12.1}/pyproject.toml +24 -7
  11. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/conftest.py +12 -8
  12. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_cli/conftest.py +3 -3
  13. litestar_vite-0.12.1/tests/test_inertia/__init__.py +0 -0
  14. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_inertia/conftest.py +0 -2
  15. litestar_vite-0.11.1/src/py/tests/test_inertia/test_inertia_request.py → litestar_vite-0.12.1/tests/test_inertia/test_request.py +19 -7
  16. litestar_vite-0.12.1/tests/test_inertia/test_response.py +509 -0
  17. litestar_vite-0.11.1/src/js/LICENSE +0 -45
  18. litestar_vite-0.11.1/src/js/Makefile +0 -66
  19. litestar_vite-0.11.1/src/js/NOTICE +0 -25
  20. litestar_vite-0.11.1/src/js/README.md +0 -15
  21. litestar_vite-0.11.1/src/js/src/dev-server-index.html +0 -185
  22. litestar_vite-0.11.1/src/js/src/index.ts +0 -602
  23. litestar_vite-0.11.1/src/js/src/inertia-helpers/index.ts +0 -170
  24. litestar_vite-0.11.1/src/js/tests/__data__/dummy.ts +0 -1
  25. litestar_vite-0.11.1/src/js/tests/index.test.ts +0 -691
  26. litestar_vite-0.11.1/src/js/tsconfig.inertia-helpers.json +0 -7
  27. litestar_vite-0.11.1/src/js/tsconfig.json +0 -13
  28. litestar_vite-0.11.1/src/js/vitest.config.ts +0 -8
  29. litestar_vite-0.11.1/src/js/vitest.workspace.ts +0 -4
  30. litestar_vite-0.11.1/src/py/litestar_vite/inertia/__init__.py +0 -34
  31. litestar_vite-0.11.1/src/py/litestar_vite/inertia/plugin.py +0 -64
  32. litestar_vite-0.11.1/src/py/tests/test_inertia/test_inertia_response.py +0 -204
  33. {litestar_vite-0.11.1 → litestar_vite-0.12.1}/LICENSE +0 -0
  34. {litestar_vite-0.11.1 → litestar_vite-0.12.1}/README.md +0 -0
  35. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/__init__.py +0 -0
  36. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/__metadata__.py +0 -0
  37. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/cli.py +0 -0
  38. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/config.py +0 -0
  39. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/_utils.py +0 -0
  40. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/config.py +0 -0
  41. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/middleware.py +0 -0
  42. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/request.py +0 -0
  43. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/routes.py +0 -0
  44. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/inertia/types.py +0 -0
  45. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/loader.py +0 -0
  46. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/py.typed +0 -0
  47. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/templates/__init__.py +0 -0
  48. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/templates/index.html.j2 +0 -0
  49. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/templates/main.ts.j2 +0 -0
  50. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/templates/package.json.j2 +0 -0
  51. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/templates/styles.css.j2 +0 -0
  52. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/templates/tsconfig.json.j2 +0 -0
  53. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/litestar_vite/templates/vite.config.ts.j2 +0 -0
  54. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/__init__.py +0 -0
  55. /litestar_vite-0.11.1/src/py/tests/templates/__init__.py → /litestar_vite-0.12.1/tests/py.typed +0 -0
  56. {litestar_vite-0.11.1/src/py/tests/test_app → litestar_vite-0.12.1/tests/templates}/__init__.py +0 -0
  57. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/templates/index.html.j2 +0 -0
  58. {litestar_vite-0.11.1/src/py/tests/test_app/web → litestar_vite-0.12.1/tests/test_app}/__init__.py +0 -0
  59. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/app.py +0 -0
  60. {litestar_vite-0.11.1/src/py/tests/test_inertia → litestar_vite-0.12.1/tests/test_app/web}/__init__.py +0 -0
  61. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/public/.gitkeep +0 -0
  62. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/public/assets/main-l0sNRNKZ.js +0 -0
  63. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/public/assets/styles-l0sNRNKZ.js +0 -0
  64. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/public/manifest.json +0 -0
  65. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/resources/.gitkeep +0 -0
  66. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/resources/main.ts +0 -0
  67. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/resources/styles.css +0 -0
  68. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/templates/.gitkeep +0 -0
  69. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_app/web/templates/index.html +0 -0
  70. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_asset_loader.py +0 -0
  71. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_cli/__init__.py +0 -0
  72. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_cli/test_init.py +0 -0
  73. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_commands.py +0 -0
  74. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_config.py +0 -0
  75. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_inertia/templates/index.html.j2 +0 -0
  76. {litestar_vite-0.11.1/src/py → litestar_vite-0.12.1}/tests/test_inertia/test_routes.py +0 -0
@@ -26,7 +26,7 @@ share/python-wheels/
26
26
  *.egg
27
27
  MANIFEST
28
28
  tmp/
29
-
29
+ docs-build/
30
30
  # PyInstaller
31
31
  # Usually these files are written by a python script from a template
32
32
  # before PyInstaller builds the exe, so as to inject date/other infos into it.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: litestar-vite
3
- Version: 0.11.1
3
+ Version: 0.12.1
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
@@ -10,6 +10,7 @@ Project-URL: Issue, https://github.com/cofin/litestar-vite/issues/
10
10
  Project-URL: Source, https://github.com/cofin/litestar-vite
11
11
  Author-email: Cody Fincher <cody.fincher@gmail.com>
12
12
  License: MIT
13
+ License-File: LICENSE
13
14
  Keywords: litestar,vite
14
15
  Classifier: Environment :: Web Environment
15
16
  Classifier: Intended Audience :: Developers
@@ -13,11 +13,11 @@ VITE_INIT_TEMPLATES: set[str] = {"package.json.j2", "tsconfig.json.j2", "vite.co
13
13
  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
- "vite": "^6.0.3",
17
- "litestar-vite-plugin": "^0.11.1",
18
- "@types/node": "^22.10.1",
16
+ "vite": "^6.0.6",
17
+ "litestar-vite-plugin": "^0.12.1",
18
+ "@types/node": "^22.10.2",
19
19
  }
20
- DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.2"}
20
+ DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.9"}
21
21
 
22
22
 
23
23
  def to_json(value: Any) -> str:
@@ -0,0 +1,37 @@
1
+ from litestar_vite.inertia import helpers
2
+ from litestar_vite.inertia.config import InertiaConfig
3
+ from litestar_vite.inertia.exception_handler import create_inertia_exception_response, exception_to_http_response
4
+ from litestar_vite.inertia.helpers import error, get_shared_props, js_routes_script, lazy, share
5
+ from litestar_vite.inertia.middleware import InertiaMiddleware
6
+ from litestar_vite.inertia.plugin import InertiaPlugin
7
+ from litestar_vite.inertia.request import InertiaDetails, InertiaHeaders, InertiaRequest
8
+ from litestar_vite.inertia.response import (
9
+ InertiaBack,
10
+ InertiaExternalRedirect,
11
+ InertiaRedirect,
12
+ InertiaResponse,
13
+ )
14
+
15
+ from .routes import generate_js_routes
16
+
17
+ __all__ = (
18
+ "InertiaBack",
19
+ "InertiaConfig",
20
+ "InertiaDetails",
21
+ "InertiaExternalRedirect",
22
+ "InertiaHeaders",
23
+ "InertiaMiddleware",
24
+ "InertiaPlugin",
25
+ "InertiaRedirect",
26
+ "InertiaRequest",
27
+ "InertiaResponse",
28
+ "create_inertia_exception_response",
29
+ "error",
30
+ "exception_to_http_response",
31
+ "generate_js_routes",
32
+ "get_shared_props",
33
+ "helpers",
34
+ "js_routes_script",
35
+ "lazy",
36
+ "share",
37
+ )
@@ -35,7 +35,8 @@ from litestar.status_codes import (
35
35
  HTTP_500_INTERNAL_SERVER_ERROR,
36
36
  )
37
37
 
38
- from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse, error
38
+ from litestar_vite.inertia.helpers import error
39
+ from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse
39
40
 
40
41
  if TYPE_CHECKING:
41
42
  from litestar.connection import Request
@@ -0,0 +1,329 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections import defaultdict
5
+ from collections.abc import Mapping
6
+ from contextlib import contextmanager
7
+ from functools import lru_cache
8
+ from textwrap import dedent
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Callable,
13
+ Coroutine,
14
+ Dict,
15
+ Generator,
16
+ Generic,
17
+ Iterable,
18
+ List,
19
+ TypeVar,
20
+ cast,
21
+ overload,
22
+ )
23
+
24
+ from anyio.from_thread import BlockingPortal, start_blocking_portal
25
+ from litestar.exceptions import ImproperlyConfiguredException
26
+ from litestar.utils.empty import value_or_default
27
+ from litestar.utils.scope.state import ScopeState
28
+ from markupsafe import Markup
29
+ from typing_extensions import ParamSpec, TypeGuard
30
+
31
+ if TYPE_CHECKING:
32
+ from litestar.connection import ASGIConnection
33
+
34
+ from litestar_vite.inertia.plugin import InertiaPlugin
35
+ from litestar_vite.inertia.routes import Routes
36
+
37
+ T = TypeVar("T")
38
+ T_ParamSpec = ParamSpec("T_ParamSpec")
39
+ PropKeyT = TypeVar("PropKeyT", bound=str)
40
+ StaticT = TypeVar("StaticT", bound=object)
41
+
42
+
43
+ @overload
44
+ def lazy(key: str, value_or_callable: None) -> StaticProp[str, None]: ...
45
+
46
+
47
+ @overload
48
+ def lazy(key: str, value_or_callable: T) -> StaticProp[str, T]: ...
49
+
50
+
51
+ @overload
52
+ def lazy(key: str, value_or_callable: Callable[..., None] = ...) -> DeferredProp[str, None]: ...
53
+
54
+
55
+ @overload
56
+ def lazy(key: str, value_or_callable: Callable[..., Coroutine[Any, Any, None]] = ...) -> DeferredProp[str, None]: ...
57
+
58
+
59
+ @overload
60
+ def lazy(
61
+ key: str,
62
+ value_or_callable: Callable[..., T | Coroutine[Any, Any, T]] = ..., # pyright: ignore[reportInvalidTypeVarUse]
63
+ ) -> DeferredProp[str, T]: ...
64
+
65
+
66
+ def lazy(
67
+ key: str,
68
+ value_or_callable: None
69
+ | Callable[T_ParamSpec, None | Coroutine[Any, Any, None]]
70
+ | T
71
+ | Callable[T_ParamSpec, T | Coroutine[Any, Any, T]] = None,
72
+ ) -> StaticProp[str, None] | StaticProp[str, T] | DeferredProp[str, T] | DeferredProp[str, None]:
73
+ """Wrap an async function to return a DeferredProp."""
74
+ if value_or_callable is None:
75
+ return StaticProp[str, None](key=key, value=None)
76
+
77
+ if not callable(value_or_callable):
78
+ return StaticProp[str, T](key=key, value=value_or_callable)
79
+
80
+ return DeferredProp[str, T](key=key, value=cast("Callable[..., T | Coroutine[Any, Any, T]]", value_or_callable))
81
+
82
+
83
+ class StaticProp(Generic[PropKeyT, StaticT]):
84
+ """A wrapper for static property evaluation."""
85
+
86
+ def __init__(self, key: PropKeyT, value: StaticT) -> None:
87
+ self._key = key
88
+ self._result = value
89
+
90
+ @property
91
+ def key(self) -> PropKeyT:
92
+ return self._key
93
+
94
+ def render(self, portal: BlockingPortal | None = None) -> StaticT:
95
+ return self._result
96
+
97
+
98
+ class DeferredProp(Generic[PropKeyT, T]):
99
+ """A wrapper for deferred property evaluation."""
100
+
101
+ def __init__(
102
+ self, key: PropKeyT, value: Callable[..., None | T | Coroutine[Any, Any, T | None]] | None = None
103
+ ) -> None:
104
+ self._key = key
105
+ self._value = value
106
+ self._evaluated = False
107
+ self._result: T | None = None
108
+
109
+ @property
110
+ def key(self) -> PropKeyT:
111
+ return self._key
112
+
113
+ @contextmanager
114
+ def with_portal(self, portal: BlockingPortal | None = None) -> Generator[BlockingPortal, None, None]:
115
+ if portal is None:
116
+ with start_blocking_portal() as p:
117
+ yield p
118
+ else:
119
+ yield portal
120
+
121
+ @staticmethod
122
+ def _is_awaitable(
123
+ v: Callable[..., T | Coroutine[Any, Any, T]],
124
+ ) -> TypeGuard[Coroutine[Any, Any, T]]:
125
+ return inspect.iscoroutinefunction(v)
126
+
127
+ def render(self, portal: BlockingPortal | None = None) -> T | None:
128
+ if self._evaluated:
129
+ return self._result
130
+ if self._value is None or not callable(self._value):
131
+ self._result = self._value
132
+ self._evaluated = True
133
+ return self._result
134
+ if not self._is_awaitable(cast("Callable[..., T]", self._value)):
135
+ self._result = cast("T", self._value())
136
+ self._evaluated = True
137
+ return self._result
138
+ with self.with_portal(portal) as p:
139
+ self._result = p.call(cast("Callable[..., T]", self._value))
140
+ self._evaluated = True
141
+ return self._result
142
+
143
+
144
+ def is_lazy_prop(value: Any) -> TypeGuard[DeferredProp[Any, Any]]:
145
+ """Check if value is a deferred property.
146
+
147
+ Args:
148
+ value: Any value to check
149
+
150
+ Returns:
151
+ bool: True if value is a deferred property
152
+ """
153
+ return isinstance(value, (DeferredProp, StaticProp))
154
+
155
+
156
+ def should_render(value: Any, partial_data: set[str] | None = None) -> bool:
157
+ """Check if value should be rendered.
158
+
159
+ Args:
160
+ value: Any value to check
161
+ partial_data: Optional set of keys for partial rendering
162
+
163
+ Returns:
164
+ bool: True if value should be rendered
165
+ """
166
+ partial_data = partial_data or set()
167
+ if is_lazy_prop(value):
168
+ return value.key in partial_data
169
+ return True
170
+
171
+
172
+ def is_or_contains_lazy_prop(value: Any) -> bool:
173
+ """Check if value is or contains a deferred property.
174
+
175
+ Args:
176
+ value: Any value to check
177
+
178
+ Returns:
179
+ bool: True if value is or contains a deferred property
180
+ """
181
+ if is_lazy_prop(value):
182
+ return True
183
+ if isinstance(value, str):
184
+ return False
185
+ if isinstance(value, Mapping):
186
+ return any(is_or_contains_lazy_prop(v) for v in cast("Mapping[str, Any]", value).values())
187
+ if isinstance(value, Iterable):
188
+ return any(is_or_contains_lazy_prop(v) for v in cast("Iterable[Any]", value))
189
+ return False
190
+
191
+
192
+ def lazy_render(value: T, partial_data: set[str] | None = None, portal: BlockingPortal | None = None) -> T:
193
+ """Filter deferred properties from the value based on partial data.
194
+
195
+ Args:
196
+ value: The value to filter
197
+ partial_data: Keys for partial rendering
198
+ portal: Optional portal to use for async rendering
199
+ Returns:
200
+ The filtered value
201
+ """
202
+ partial_data = partial_data or set()
203
+ if isinstance(value, str):
204
+ return cast("T", value)
205
+ if isinstance(value, Mapping):
206
+ return cast(
207
+ "T",
208
+ {
209
+ k: lazy_render(v, partial_data, portal)
210
+ for k, v in cast("Mapping[str, Any]", value).items()
211
+ if should_render(v, partial_data)
212
+ },
213
+ )
214
+
215
+ if isinstance(value, (list, tuple)):
216
+ filtered = [
217
+ lazy_render(v, partial_data, portal) for v in cast("Iterable[Any]", value) if should_render(v, partial_data)
218
+ ]
219
+ return cast("T", type(value)(filtered)) # pyright: ignore[reportUnknownArgumentType]
220
+
221
+ if is_lazy_prop(value) and should_render(value, partial_data):
222
+ return cast("T", value.render(portal))
223
+
224
+ return cast("T", value)
225
+
226
+
227
+ def get_shared_props(
228
+ request: ASGIConnection[Any, Any, Any, Any],
229
+ partial_data: set[str] | None = None,
230
+ ) -> dict[str, Any]:
231
+ """Return shared session props for a request.
232
+
233
+ Args:
234
+ request: The ASGI connection.
235
+ partial_data: Optional set of keys for partial rendering.
236
+ portal: Optional portal to use for async rendering
237
+ Returns:
238
+ Dict[str, Any]: The shared props.
239
+
240
+ Note:
241
+ Be sure to call this before `self.create_template_context` if you would like to include the `flash` message details.
242
+ """
243
+ props: dict[str, Any] = {}
244
+ flash: dict[str, list[str]] = defaultdict(list)
245
+ errors: dict[str, Any] = {}
246
+ error_bag = request.headers.get("X-Inertia-Error-Bag", None)
247
+
248
+ try:
249
+ errors = request.session.pop("_errors", {})
250
+ shared_props = cast("Dict[str,Any]", request.session.pop("_shared", {}))
251
+ inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
252
+
253
+ # Handle deferred props
254
+ for key, value in shared_props.items():
255
+ if is_lazy_prop(value) and should_render(value, partial_data):
256
+ props[key] = value.render(inertia_plugin.portal)
257
+ continue
258
+ if should_render(value, partial_data):
259
+ props[key] = value
260
+
261
+ for message in cast("List[Dict[str,Any]]", request.session.pop("_messages", [])):
262
+ flash[message["category"]].append(message["message"])
263
+
264
+ props.update(inertia_plugin.config.extra_static_page_props)
265
+ for session_prop in inertia_plugin.config.extra_session_page_props:
266
+ if session_prop not in props and session_prop in request.session:
267
+ props[session_prop] = request.session.get(session_prop)
268
+
269
+ except (AttributeError, ImproperlyConfiguredException):
270
+ msg = "Unable to generate all shared props. A valid session was not found for this request."
271
+ request.logger.warning(msg)
272
+
273
+ props["flash"] = flash
274
+ props["errors"] = {error_bag: errors} if error_bag is not None else errors
275
+ props["csrf_token"] = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "")
276
+ return props
277
+
278
+
279
+ def share(
280
+ connection: ASGIConnection[Any, Any, Any, Any],
281
+ key: str,
282
+ value: Any,
283
+ ) -> None:
284
+ """Share a value in the session.
285
+
286
+ Args:
287
+ connection: The ASGI connection.
288
+ key: The key to store the value under.
289
+ value: The value to store.
290
+ """
291
+ try:
292
+ connection.session.setdefault("_shared", {}).update({key: value})
293
+ except (AttributeError, ImproperlyConfiguredException):
294
+ msg = "Unable to set `share` session state. A valid session was not found for this request."
295
+ connection.logger.warning(msg)
296
+
297
+
298
+ def error(
299
+ connection: ASGIConnection[Any, Any, Any, Any],
300
+ key: str,
301
+ message: str,
302
+ ) -> None:
303
+ """Set an error message in the session.
304
+
305
+ Args:
306
+ connection: The ASGI connection.
307
+ key: The key to store the error under.
308
+ message: The error message.
309
+ """
310
+ try:
311
+ connection.session.setdefault("_errors", {}).update({key: message})
312
+ except (AttributeError, ImproperlyConfiguredException):
313
+ msg = "Unable to set `error` session state. A valid session was not found for this request."
314
+ connection.logger.warning(msg)
315
+
316
+
317
+ def js_routes_script(js_routes: Routes) -> Markup:
318
+ @lru_cache
319
+ def _markup_safe_json_dumps(js_routes: str) -> Markup:
320
+ js = js_routes.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026").replace("'", "\\u0027")
321
+ return Markup(js)
322
+
323
+ return Markup(
324
+ dedent(f"""
325
+ <script type="module">
326
+ globalThis.routes = JSON.parse('{_markup_safe_json_dumps(js_routes.formatted_routes)}')
327
+ </script>
328
+ """),
329
+ )
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import asynccontextmanager
4
+ from typing import TYPE_CHECKING, AsyncGenerator
5
+
6
+ from anyio.from_thread import start_blocking_portal
7
+ from litestar.plugins import InitPluginProtocol
8
+
9
+ if TYPE_CHECKING:
10
+ from anyio.from_thread import BlockingPortal
11
+ from litestar import Litestar
12
+ from litestar.config.app import AppConfig
13
+
14
+ from litestar_vite.inertia.config import InertiaConfig
15
+
16
+
17
+ def set_js_routes(app: Litestar) -> None:
18
+ """Generate the route structure of the application on startup."""
19
+ from litestar_vite.inertia.routes import generate_js_routes
20
+
21
+ js_routes = generate_js_routes(app)
22
+ app.state.js_routes = js_routes
23
+
24
+
25
+ class InertiaPlugin(InitPluginProtocol):
26
+ """Inertia plugin."""
27
+
28
+ __slots__ = ("_portal", "config")
29
+
30
+ def __init__(self, config: InertiaConfig) -> None:
31
+ """Initialize ``Inertia``.
32
+
33
+ Args:
34
+ config: Inertia configuration.
35
+ """
36
+ self.config = config
37
+
38
+ @asynccontextmanager
39
+ async def lifespan(self, app: Litestar) -> AsyncGenerator[None, None]:
40
+ """Lifespan to ensure the event loop is available."""
41
+
42
+ with start_blocking_portal() as portal:
43
+ self._portal = portal
44
+ yield
45
+
46
+ @property
47
+ def portal(self) -> BlockingPortal:
48
+ """Get the portal."""
49
+ return self._portal
50
+
51
+ def on_app_init(self, app_config: AppConfig) -> AppConfig:
52
+ """Configure application for use with Vite.
53
+
54
+ Args:
55
+ app_config: The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
56
+ """
57
+
58
+ from litestar.exceptions import ImproperlyConfiguredException
59
+ from litestar.middleware import DefineMiddleware
60
+ from litestar.middleware.session import SessionMiddleware
61
+ from litestar.security.session_auth.middleware import MiddlewareWrapper
62
+ from litestar.utils.predicates import is_class_and_subclass
63
+
64
+ from litestar_vite.inertia.exception_handler import exception_to_http_response
65
+ from litestar_vite.inertia.helpers import DeferredProp, StaticProp
66
+ from litestar_vite.inertia.middleware import InertiaMiddleware
67
+ from litestar_vite.inertia.request import InertiaRequest
68
+ from litestar_vite.inertia.response import InertiaBack, InertiaResponse
69
+
70
+ for mw in app_config.middleware:
71
+ if isinstance(mw, DefineMiddleware) and is_class_and_subclass(
72
+ mw.middleware,
73
+ (MiddlewareWrapper, SessionMiddleware),
74
+ ):
75
+ break
76
+ else:
77
+ msg = "The Inertia plugin require a session middleware."
78
+ raise ImproperlyConfiguredException(msg)
79
+ app_config.exception_handlers.update({Exception: exception_to_http_response}) # pyright: ignore[reportUnknownMemberType]
80
+ app_config.request_class = InertiaRequest
81
+ app_config.response_class = InertiaResponse
82
+ app_config.middleware.append(InertiaMiddleware)
83
+ app_config.on_startup.append(set_js_routes)
84
+ app_config.signature_types.extend([InertiaRequest, InertiaResponse, InertiaBack, StaticProp, DeferredProp])
85
+ app_config.type_encoders = {
86
+ StaticProp: lambda val: val.render(),
87
+ DeferredProp: lambda val: val.render(),
88
+ **(app_config.type_encoders or {}),
89
+ }
90
+ app_config.type_decoders = [
91
+ (lambda x: x is StaticProp, lambda t, v: t(v)),
92
+ (lambda x: x is DeferredProp, lambda t, v: t(v)),
93
+ *(app_config.type_decoders or []),
94
+ ]
95
+ app_config.lifespan.append(self.lifespan) # pyright: ignore[reportUnknownMemberType]
96
+ return app_config
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import itertools
4
- from collections import defaultdict
5
- from functools import lru_cache
4
+ from collections.abc import Mapping
6
5
  from mimetypes import guess_type
7
6
  from pathlib import PurePath
8
- from textwrap import dedent
9
- from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Mapping, TypeVar, cast
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Iterable,
11
+ TypeVar,
12
+ cast,
13
+ )
10
14
  from urllib.parse import quote, urlparse, urlunparse
11
15
 
12
16
  from litestar import Litestar, MediaType, Request, Response
@@ -20,99 +24,29 @@ from litestar.utils.deprecation import warn_deprecation
20
24
  from litestar.utils.empty import value_or_default
21
25
  from litestar.utils.helpers import get_enum_string_value
22
26
  from litestar.utils.scope.state import ScopeState
23
- from markupsafe import Markup
24
27
 
25
28
  from litestar_vite.inertia._utils import get_headers
29
+ from litestar_vite.inertia.helpers import (
30
+ get_shared_props,
31
+ is_or_contains_lazy_prop,
32
+ js_routes_script,
33
+ lazy_render,
34
+ should_render,
35
+ )
36
+ from litestar_vite.inertia.plugin import InertiaPlugin
26
37
  from litestar_vite.inertia.types import InertiaHeaderType, PageProps
27
38
  from litestar_vite.plugin import VitePlugin
28
39
 
29
40
  if TYPE_CHECKING:
30
41
  from litestar.app import Litestar
31
42
  from litestar.background_tasks import BackgroundTask, BackgroundTasks
32
- from litestar.connection import ASGIConnection
33
43
  from litestar.connection.base import AuthT, StateT, UserT
34
44
  from litestar.types import ResponseCookies, ResponseHeaders, TypeEncodersMap
35
45
 
36
- from litestar_vite.inertia.routes import Routes
37
-
38
- from .plugin import InertiaPlugin
39
46
 
40
47
  T = TypeVar("T")
41
48
 
42
49
 
43
- def share(
44
- connection: ASGIConnection[Any, Any, Any, Any],
45
- key: str,
46
- value: Any,
47
- ) -> None:
48
- try:
49
- connection.session.setdefault("_shared", {}).update({key: value})
50
- except (AttributeError, ImproperlyConfiguredException):
51
- msg = "Unable to set `share` session state. A valid session was not found for this request."
52
- connection.logger.warning(msg)
53
-
54
-
55
- def error(
56
- connection: ASGIConnection[Any, Any, Any, Any],
57
- key: str,
58
- message: str,
59
- ) -> None:
60
- try:
61
- connection.session.setdefault("_errors", {}).update({key: message})
62
- except (AttributeError, ImproperlyConfiguredException):
63
- msg = "Unable to set `error` session state. A valid session was not found for this request."
64
- connection.logger.warning(msg)
65
-
66
-
67
- def get_shared_props(
68
- request: ASGIConnection[Any, Any, Any, Any],
69
- partial_data: set[str] | None = None,
70
- ) -> Dict[str, Any]: # noqa: UP006
71
- """Return shared session props for a request
72
-
73
-
74
- Be sure to call this before `self.create_template_context` if you would like to include the `flash` message details.
75
- """
76
- props: dict[str, Any] = {}
77
- flash: dict[str, list[str]] = defaultdict(list)
78
- errors: dict[str, Any] = {}
79
- error_bag = request.headers.get("X-Inertia-Error-Bag", None)
80
- try:
81
- errors = request.session.pop("_errors", {})
82
- props.update(cast("Dict[str,Any]", request.session.pop("_shared", {})))
83
- for message in cast("List[Dict[str,Any]]", request.session.pop("_messages", [])):
84
- flash[message["category"]].append(message["message"])
85
-
86
- inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
87
- props.update(inertia_plugin.config.extra_static_page_props)
88
- for session_prop in inertia_plugin.config.extra_session_page_props:
89
- if session_prop not in props and session_prop in request.session:
90
- props[session_prop] = request.session.get(session_prop)
91
-
92
- except (AttributeError, ImproperlyConfiguredException):
93
- msg = "Unable to generate all shared props. A valid session was not found for this request."
94
- request.logger.warning(msg)
95
- props["flash"] = flash
96
- props["errors"] = {error_bag: errors} if error_bag is not None else errors
97
- props["csrf_token"] = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "")
98
- return props
99
-
100
-
101
- def js_routes_script(js_routes: Routes) -> Markup:
102
- @lru_cache
103
- def _markup_safe_json_dumps(js_routes: str) -> Markup:
104
- js = js_routes.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026").replace("'", "\\u0027")
105
- return Markup(js)
106
-
107
- return Markup(
108
- dedent(f"""
109
- <script type="module">
110
- globalThis.routes = JSON.parse('{_markup_safe_json_dumps(js_routes.formatted_routes)}')
111
- </script>
112
- """),
113
- )
114
-
115
-
116
50
  class InertiaResponse(Response[T]):
117
51
  """Inertia Response"""
118
52
 
@@ -197,7 +131,7 @@ class InertiaResponse(Response[T]):
197
131
  "csrf_input": f'<input type="hidden" name="_csrf_token" value="{csrf_token}" />',
198
132
  }
199
133
 
200
- def to_asgi_response(
134
+ def to_asgi_response( # noqa: C901, PLR0912
201
135
  self,
202
136
  app: Litestar | None,
203
137
  request: Request[UserT, AuthT, StateT],
@@ -245,12 +179,26 @@ class InertiaResponse(Response[T]):
245
179
  is_partial_render = cast("bool", getattr(request, "is_partial_render", False))
246
180
  partial_keys = cast("set[str]", getattr(request, "partial_keys", {}))
247
181
  vite_plugin = request.app.plugins.get(VitePlugin)
182
+ inertia_plugin = request.app.plugins.get(InertiaPlugin)
248
183
  template_engine = request.app.template_engine # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
249
184
  headers.update(
250
185
  {"Vary": "Accept", **get_headers(InertiaHeaderType(enabled=True))},
251
186
  )
252
- shared_props = get_shared_props(request, partial_data=partial_keys if is_partial_render else None)
253
- shared_props["content"] = self.content
187
+ shared_props = get_shared_props(
188
+ request,
189
+ partial_data=partial_keys if is_partial_render else None,
190
+ )
191
+ if is_or_contains_lazy_prop(self.content):
192
+ filtered_content = lazy_render(
193
+ self.content,
194
+ partial_keys if is_partial_render else None,
195
+ inertia_plugin.portal,
196
+ )
197
+ if filtered_content is not None:
198
+ shared_props["content"] = filtered_content
199
+ elif should_render(self.content, partial_keys):
200
+ shared_props["content"] = self.content
201
+
254
202
  page_props = PageProps[T](
255
203
  component=request.inertia.route_component, # type: ignore[attr-defined] # pyright: ignore[reportUnknownArgumentType,reportUnknownMemberType,reportAttributeAccessIssue]
256
204
  props=shared_props, # pyright: ignore[reportArgumentType]
@@ -292,7 +240,6 @@ class InertiaResponse(Response[T]):
292
240
  if self.template_str is not None:
293
241
  body = template_engine.render_string(self.template_str, context).encode(self.encoding) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
294
242
  else:
295
- inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
296
243
  template_name = self.template_name or inertia_plugin.config.root_template
297
244
  template = template_engine.get_template(template_name) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
298
245
  body = template.render(**context).encode(self.encoding) # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]