litestar-vite 0.15.0__py3-none-any.whl → 0.15.0rc2__py3-none-any.whl
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.
- litestar_vite/_codegen/__init__.py +26 -0
- litestar_vite/_codegen/inertia.py +407 -0
- litestar_vite/{codegen/_openapi.py → _codegen/openapi.py} +11 -58
- litestar_vite/{codegen/_routes.py → _codegen/routes.py} +43 -110
- litestar_vite/{codegen/_ts.py → _codegen/ts.py} +19 -19
- litestar_vite/_handler/__init__.py +8 -0
- litestar_vite/{handler/_app.py → _handler/app.py} +29 -117
- litestar_vite/cli.py +254 -155
- litestar_vite/codegen.py +39 -0
- litestar_vite/commands.py +6 -0
- litestar_vite/{config/__init__.py → config.py} +726 -99
- litestar_vite/deploy.py +3 -14
- litestar_vite/doctor.py +6 -8
- litestar_vite/executor.py +1 -45
- litestar_vite/handler.py +9 -0
- litestar_vite/html_transform.py +5 -148
- litestar_vite/inertia/__init__.py +0 -24
- litestar_vite/inertia/_utils.py +0 -5
- litestar_vite/inertia/exception_handler.py +16 -22
- litestar_vite/inertia/helpers.py +18 -546
- litestar_vite/inertia/plugin.py +11 -77
- litestar_vite/inertia/request.py +0 -48
- litestar_vite/inertia/response.py +17 -113
- litestar_vite/inertia/types.py +0 -19
- litestar_vite/loader.py +7 -7
- litestar_vite/plugin.py +2184 -0
- litestar_vite/templates/angular/package.json.j2 +1 -2
- litestar_vite/templates/angular-cli/package.json.j2 +1 -2
- litestar_vite/templates/base/package.json.j2 +1 -2
- litestar_vite/templates/react-inertia/package.json.j2 +1 -2
- litestar_vite/templates/vue-inertia/package.json.j2 +1 -2
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/METADATA +5 -5
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/RECORD +36 -49
- litestar_vite/codegen/__init__.py +0 -48
- litestar_vite/codegen/_export.py +0 -229
- litestar_vite/codegen/_inertia.py +0 -619
- litestar_vite/codegen/_utils.py +0 -141
- litestar_vite/config/_constants.py +0 -97
- litestar_vite/config/_deploy.py +0 -70
- litestar_vite/config/_inertia.py +0 -241
- litestar_vite/config/_paths.py +0 -63
- litestar_vite/config/_runtime.py +0 -235
- litestar_vite/config/_spa.py +0 -93
- litestar_vite/config/_types.py +0 -94
- litestar_vite/handler/__init__.py +0 -9
- litestar_vite/inertia/precognition.py +0 -274
- litestar_vite/plugin/__init__.py +0 -687
- litestar_vite/plugin/_process.py +0 -185
- litestar_vite/plugin/_proxy.py +0 -689
- litestar_vite/plugin/_proxy_headers.py +0 -244
- litestar_vite/plugin/_static.py +0 -37
- litestar_vite/plugin/_utils.py +0 -489
- /litestar_vite/{handler/_routing.py → _handler/routing.py} +0 -0
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +0 -0
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
litestar_vite/inertia/plugin.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
|
-
import httpx
|
|
5
4
|
from anyio.from_thread import start_blocking_portal
|
|
6
5
|
from litestar.plugins import InitPluginProtocol
|
|
7
6
|
|
|
@@ -38,16 +37,6 @@ class InertiaPlugin(InitPluginProtocol):
|
|
|
38
37
|
custom serialization), ensure the app lifespan is active and the
|
|
39
38
|
portal is available via ``inertia_plugin.portal``.
|
|
40
39
|
|
|
41
|
-
SSR Client Pooling:
|
|
42
|
-
When SSR is enabled, the plugin maintains a shared ``httpx.AsyncClient``
|
|
43
|
-
for all SSR requests. This provides significant performance benefits:
|
|
44
|
-
- Connection pooling with keep-alive
|
|
45
|
-
- TLS session reuse
|
|
46
|
-
- HTTP/2 multiplexing (when available)
|
|
47
|
-
|
|
48
|
-
The client is initialized during app lifespan and properly closed on shutdown.
|
|
49
|
-
Access via ``inertia_plugin.ssr_client`` if needed.
|
|
50
|
-
|
|
51
40
|
Example::
|
|
52
41
|
|
|
53
42
|
from litestar_vite.inertia import InertiaPlugin, InertiaConfig
|
|
@@ -58,46 +47,26 @@ class InertiaPlugin(InitPluginProtocol):
|
|
|
58
47
|
)
|
|
59
48
|
"""
|
|
60
49
|
|
|
61
|
-
__slots__ = ("_portal", "
|
|
50
|
+
__slots__ = ("_portal", "config")
|
|
62
51
|
|
|
63
52
|
def __init__(self, config: "InertiaConfig") -> "None":
|
|
64
53
|
"""Initialize the plugin with Inertia configuration."""
|
|
65
54
|
self.config = config
|
|
66
|
-
self._ssr_client: "httpx.AsyncClient | None" = None
|
|
67
|
-
self._portal: "BlockingPortal | None" = None # pyright: ignore[reportInvalidTypeForm]
|
|
68
55
|
|
|
69
56
|
@asynccontextmanager
|
|
70
57
|
async def lifespan(self, app: "Litestar") -> "AsyncGenerator[None, None]":
|
|
71
58
|
"""Lifespan to ensure the event loop is available.
|
|
72
59
|
|
|
73
|
-
Initializes:
|
|
74
|
-
- BlockingPortal for sync-to-async DeferredProp resolution
|
|
75
|
-
- Shared httpx.AsyncClient for SSR requests (connection pooling)
|
|
76
|
-
|
|
77
60
|
Args:
|
|
78
61
|
app: The :class:`Litestar <litestar.app.Litestar>` instance.
|
|
79
62
|
|
|
80
63
|
Yields:
|
|
81
64
|
An asynchronous context manager.
|
|
82
65
|
"""
|
|
83
|
-
# Initialize shared SSR client with connection pooling
|
|
84
|
-
# These limits are tuned for typical SSR workloads:
|
|
85
|
-
# - max_keepalive_connections: 10 per-host keep-alive connections
|
|
86
|
-
# - max_connections: 20 total concurrent connections
|
|
87
|
-
# - keepalive_expiry: 30s idle timeout before closing
|
|
88
|
-
limits = httpx.Limits(max_keepalive_connections=10, max_connections=20, keepalive_expiry=30.0)
|
|
89
|
-
self._ssr_client = httpx.AsyncClient(
|
|
90
|
-
limits=limits,
|
|
91
|
-
timeout=httpx.Timeout(10.0), # Default timeout, can be overridden per-request
|
|
92
|
-
)
|
|
93
66
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
yield
|
|
98
|
-
finally:
|
|
99
|
-
await self._ssr_client.aclose()
|
|
100
|
-
self._ssr_client = None # Reset to signal client is closed
|
|
67
|
+
with start_blocking_portal() as portal:
|
|
68
|
+
self._portal = portal
|
|
69
|
+
yield
|
|
101
70
|
|
|
102
71
|
@property
|
|
103
72
|
def portal(self) -> "BlockingPortal":
|
|
@@ -105,27 +74,9 @@ class InertiaPlugin(InitPluginProtocol):
|
|
|
105
74
|
|
|
106
75
|
Returns:
|
|
107
76
|
The BlockingPortal instance.
|
|
108
|
-
|
|
109
|
-
Raises:
|
|
110
|
-
RuntimeError: If accessed before app lifespan is active.
|
|
111
77
|
"""
|
|
112
|
-
if self._portal is None:
|
|
113
|
-
msg = "BlockingPortal not available. Ensure app lifespan is active."
|
|
114
|
-
raise RuntimeError(msg)
|
|
115
78
|
return self._portal
|
|
116
79
|
|
|
117
|
-
@property
|
|
118
|
-
def ssr_client(self) -> "httpx.AsyncClient | None":
|
|
119
|
-
"""Return the shared httpx.AsyncClient for SSR requests.
|
|
120
|
-
|
|
121
|
-
The client is initialized during app lifespan and provides connection
|
|
122
|
-
pooling, TLS session reuse, and HTTP/2 multiplexing benefits.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
The shared AsyncClient instance, or None if not initialized.
|
|
126
|
-
"""
|
|
127
|
-
return self._ssr_client
|
|
128
|
-
|
|
129
80
|
def on_app_init(self, app_config: "AppConfig") -> "AppConfig":
|
|
130
81
|
"""Configure application for use with Vite.
|
|
131
82
|
|
|
@@ -139,7 +90,7 @@ class InertiaPlugin(InitPluginProtocol):
|
|
|
139
90
|
The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
|
|
140
91
|
"""
|
|
141
92
|
|
|
142
|
-
from litestar.exceptions import
|
|
93
|
+
from litestar.exceptions import ImproperlyConfiguredException
|
|
143
94
|
from litestar.middleware import DefineMiddleware
|
|
144
95
|
from litestar.middleware.session import SessionMiddleware
|
|
145
96
|
from litestar.security.session_auth.middleware import MiddlewareWrapper
|
|
@@ -159,35 +110,18 @@ class InertiaPlugin(InitPluginProtocol):
|
|
|
159
110
|
else:
|
|
160
111
|
msg = "The Inertia plugin require a session middleware."
|
|
161
112
|
raise ImproperlyConfiguredException(msg)
|
|
113
|
+
from litestar.exceptions import HTTPException
|
|
162
114
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
HTTPException: exception_to_http_response,
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# Add Precognition exception handler when enabled
|
|
170
|
-
# Note: The exception handler formats validation errors in Laravel's format.
|
|
171
|
-
# For successful validation to return 204 (without executing the handler),
|
|
172
|
-
# use the @precognition decorator on your route handlers.
|
|
173
|
-
if self.config.precognition:
|
|
174
|
-
from litestar_vite.inertia.precognition import create_precognition_exception_handler
|
|
175
|
-
|
|
176
|
-
exception_handlers[ValidationException] = create_precognition_exception_handler(
|
|
177
|
-
fallback_handler=exception_to_http_response
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
app_config.exception_handlers.update(exception_handlers) # pyright: ignore[reportUnknownMemberType]
|
|
115
|
+
app_config.exception_handlers.update( # pyright: ignore[reportUnknownMemberType]
|
|
116
|
+
{Exception: exception_to_http_response, HTTPException: exception_to_http_response}
|
|
117
|
+
)
|
|
181
118
|
app_config.request_class = InertiaRequest
|
|
182
119
|
app_config.response_class = InertiaResponse
|
|
183
120
|
app_config.middleware.append(InertiaMiddleware)
|
|
184
121
|
app_config.signature_types.extend([InertiaRequest, InertiaResponse, InertiaBack, StaticProp, DeferredProp])
|
|
185
|
-
# Type encoders for prop resolution
|
|
186
|
-
# DeferredProp encoder passes the plugin's portal for efficient async resolution
|
|
187
|
-
# This avoids creating a new BlockingPortal per DeferredProp (~5-10ms savings)
|
|
188
122
|
app_config.type_encoders = {
|
|
189
123
|
StaticProp: lambda val: val.render(),
|
|
190
|
-
DeferredProp: lambda val: val.render(
|
|
124
|
+
DeferredProp: lambda val: val.render(),
|
|
191
125
|
**(app_config.type_encoders or {}),
|
|
192
126
|
}
|
|
193
127
|
app_config.type_decoders = [
|
litestar_vite/inertia/request.py
CHANGED
|
@@ -186,30 +186,6 @@ class InertiaDetails:
|
|
|
186
186
|
"""
|
|
187
187
|
return self.reset_props.split(",") if self.reset_props is not None else []
|
|
188
188
|
|
|
189
|
-
@cached_property
|
|
190
|
-
def is_precognition(self) -> bool:
|
|
191
|
-
"""Return True when the request is a Precognition validation request.
|
|
192
|
-
|
|
193
|
-
Precognition requests allow real-time form validation without
|
|
194
|
-
executing handler side effects.
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
True if Precognition header is present and "true".
|
|
198
|
-
"""
|
|
199
|
-
return self._get_header_value(InertiaHeaders.PRECOGNITION) == "true"
|
|
200
|
-
|
|
201
|
-
@cached_property
|
|
202
|
-
def precognition_validate_only(self) -> list[str]:
|
|
203
|
-
"""Return the fields to validate for partial Precognition validation.
|
|
204
|
-
|
|
205
|
-
When present, only errors for these fields should be returned.
|
|
206
|
-
|
|
207
|
-
Returns:
|
|
208
|
-
List of field names to validate, or empty list if validating all fields.
|
|
209
|
-
"""
|
|
210
|
-
value = self._get_header_value(InertiaHeaders.PRECOGNITION_VALIDATE_ONLY)
|
|
211
|
-
return value.split(",") if value else []
|
|
212
|
-
|
|
213
189
|
|
|
214
190
|
class InertiaRequest(Request[UserT, AuthT, StateT]):
|
|
215
191
|
"""Inertia Request class to work with Inertia client."""
|
|
@@ -308,27 +284,3 @@ class InertiaRequest(Request[UserT, AuthT, StateT]):
|
|
|
308
284
|
The version string sent by the client, or None if not present.
|
|
309
285
|
"""
|
|
310
286
|
return self.inertia.version
|
|
311
|
-
|
|
312
|
-
@property
|
|
313
|
-
def is_precognition(self) -> bool:
|
|
314
|
-
"""Check if this is a Precognition validation request.
|
|
315
|
-
|
|
316
|
-
Precognition requests run validation without executing the handler body,
|
|
317
|
-
enabling real-time form validation.
|
|
318
|
-
|
|
319
|
-
Returns:
|
|
320
|
-
True if this is a Precognition request.
|
|
321
|
-
"""
|
|
322
|
-
return self.inertia.is_precognition
|
|
323
|
-
|
|
324
|
-
@property
|
|
325
|
-
def precognition_validate_only(self) -> "set[str]":
|
|
326
|
-
"""Get fields to validate for partial Precognition validation.
|
|
327
|
-
|
|
328
|
-
When present, only validation errors for these fields should be returned.
|
|
329
|
-
Used for validating individual fields as the user types.
|
|
330
|
-
|
|
331
|
-
Returns:
|
|
332
|
-
A set of field names to validate, or empty set if validating all.
|
|
333
|
-
"""
|
|
334
|
-
return set(self.inertia.precognition_validate_only)
|
|
@@ -24,12 +24,10 @@ from litestar_vite.inertia._utils import get_headers
|
|
|
24
24
|
from litestar_vite.inertia.helpers import (
|
|
25
25
|
extract_deferred_props,
|
|
26
26
|
extract_merge_props,
|
|
27
|
-
extract_once_props,
|
|
28
27
|
extract_pagination_scroll_props,
|
|
29
28
|
get_shared_props,
|
|
30
29
|
is_merge_prop,
|
|
31
30
|
is_or_contains_lazy_prop,
|
|
32
|
-
is_or_contains_special_prop,
|
|
33
31
|
is_pagination_container,
|
|
34
32
|
lazy_render,
|
|
35
33
|
pagination_to_dict,
|
|
@@ -129,12 +127,7 @@ def _parse_inertia_ssr_payload(payload: Any, url: str) -> _InertiaSSRResult:
|
|
|
129
127
|
|
|
130
128
|
|
|
131
129
|
def _render_inertia_ssr_sync(
|
|
132
|
-
page: dict[str, Any],
|
|
133
|
-
url: str,
|
|
134
|
-
*,
|
|
135
|
-
timeout_seconds: float,
|
|
136
|
-
portal: "BlockingPortal",
|
|
137
|
-
client: "httpx.AsyncClient | None" = None,
|
|
130
|
+
page: dict[str, Any], url: str, *, timeout_seconds: float, portal: "BlockingPortal"
|
|
138
131
|
) -> _InertiaSSRResult:
|
|
139
132
|
"""Call the Inertia SSR server and return head/body HTML.
|
|
140
133
|
|
|
@@ -145,48 +138,15 @@ def _render_inertia_ssr_sync(
|
|
|
145
138
|
This function uses the application's :class:`~anyio.from_thread.BlockingPortal`
|
|
146
139
|
to call the async HTTP client without blocking the event loop thread.
|
|
147
140
|
|
|
148
|
-
Args:
|
|
149
|
-
page: The page object to send to the SSR server.
|
|
150
|
-
url: The SSR server URL.
|
|
151
|
-
timeout_seconds: Request timeout in seconds.
|
|
152
|
-
portal: BlockingPortal for sync-to-async bridging.
|
|
153
|
-
client: Optional shared httpx.AsyncClient for connection pooling.
|
|
154
|
-
|
|
155
141
|
Returns:
|
|
156
142
|
An _InertiaSSRResult with head and body HTML.
|
|
157
143
|
"""
|
|
158
|
-
return portal.call(_render_inertia_ssr, page, url, timeout_seconds
|
|
144
|
+
return portal.call(_render_inertia_ssr, page, url, timeout_seconds)
|
|
159
145
|
|
|
160
146
|
|
|
161
|
-
async def _render_inertia_ssr(
|
|
162
|
-
page: dict[str, Any], url: str, timeout_seconds: float, client: "httpx.AsyncClient | None" = None
|
|
163
|
-
) -> _InertiaSSRResult:
|
|
147
|
+
async def _render_inertia_ssr(page: dict[str, Any], url: str, timeout_seconds: float) -> _InertiaSSRResult:
|
|
164
148
|
"""Call the Inertia SSR server asynchronously and return head/body HTML.
|
|
165
149
|
|
|
166
|
-
Args:
|
|
167
|
-
page: The page object to send to the SSR server.
|
|
168
|
-
url: The SSR server URL (typically http://localhost:13714/render).
|
|
169
|
-
timeout_seconds: Request timeout in seconds.
|
|
170
|
-
client: Optional shared httpx.AsyncClient for connection pooling.
|
|
171
|
-
If None, creates a new client per request (slower).
|
|
172
|
-
|
|
173
|
-
Returns:
|
|
174
|
-
An _InertiaSSRResult with head and body HTML.
|
|
175
|
-
"""
|
|
176
|
-
return await _do_ssr_request(page, url, timeout_seconds, client)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
async def _do_ssr_request(
|
|
180
|
-
page: dict[str, Any], url: str, timeout_seconds: float, client: "httpx.AsyncClient | None"
|
|
181
|
-
) -> _InertiaSSRResult:
|
|
182
|
-
"""Execute the SSR request with optional client reuse.
|
|
183
|
-
|
|
184
|
-
Args:
|
|
185
|
-
page: The page object to send to the SSR server.
|
|
186
|
-
url: The SSR server URL.
|
|
187
|
-
timeout_seconds: Request timeout in seconds.
|
|
188
|
-
client: Optional shared httpx.AsyncClient.
|
|
189
|
-
|
|
190
150
|
Raises:
|
|
191
151
|
ImproperlyConfiguredException: If the SSR server is unreachable,
|
|
192
152
|
returns an error status, or returns invalid payload.
|
|
@@ -194,37 +154,19 @@ async def _do_ssr_request(
|
|
|
194
154
|
Returns:
|
|
195
155
|
An _InertiaSSRResult with head and body HTML.
|
|
196
156
|
"""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if client is not None:
|
|
200
|
-
# Use shared client for connection pooling benefits
|
|
201
|
-
try:
|
|
157
|
+
try:
|
|
158
|
+
async with httpx.AsyncClient() as client:
|
|
202
159
|
response = await client.post(url, json=page, timeout=timeout_seconds)
|
|
203
160
|
response.raise_for_status()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
else:
|
|
214
|
-
# Fallback: create a new client per request (graceful degradation)
|
|
215
|
-
try:
|
|
216
|
-
async with httpx.AsyncClient() as fallback_client:
|
|
217
|
-
response = await fallback_client.post(url, json=page, timeout=timeout_seconds)
|
|
218
|
-
response.raise_for_status()
|
|
219
|
-
except httpx.RequestError as exc:
|
|
220
|
-
msg = (
|
|
221
|
-
f"Inertia SSR is enabled but the SSR server is not reachable at {url!r}. "
|
|
222
|
-
"Start the SSR server (Node) or disable InertiaConfig.ssr."
|
|
223
|
-
)
|
|
224
|
-
raise ImproperlyConfiguredException(msg) from exc
|
|
225
|
-
except httpx.HTTPStatusError as exc:
|
|
226
|
-
msg = f"Inertia SSR server at {url!r} returned HTTP {exc.response.status_code}. Check the SSR server logs."
|
|
227
|
-
raise ImproperlyConfiguredException(msg) from exc
|
|
161
|
+
except httpx.RequestError as exc:
|
|
162
|
+
msg = (
|
|
163
|
+
f"Inertia SSR is enabled but the SSR server is not reachable at {url!r}. "
|
|
164
|
+
"Start the SSR server (Node) or disable InertiaConfig.ssr."
|
|
165
|
+
)
|
|
166
|
+
raise ImproperlyConfiguredException(msg) from exc
|
|
167
|
+
except httpx.HTTPStatusError as exc:
|
|
168
|
+
msg = f"Inertia SSR server at {url!r} returned HTTP {exc.response.status_code}. Check the SSR server logs."
|
|
169
|
+
raise ImproperlyConfiguredException(msg) from exc
|
|
228
170
|
|
|
229
171
|
try:
|
|
230
172
|
payload = response.json()
|
|
@@ -265,28 +207,6 @@ def _get_redirect_url(request: "Request[Any, Any, Any]", url: str | None) -> str
|
|
|
265
207
|
return url
|
|
266
208
|
|
|
267
209
|
|
|
268
|
-
def _get_relative_url(request: "Request[Any, Any, Any]") -> str:
|
|
269
|
-
"""Return the relative URL including query string for Inertia page props.
|
|
270
|
-
|
|
271
|
-
The Inertia.js protocol requires the ``url`` property to include query parameters
|
|
272
|
-
so that page state (e.g., filters, pagination) is preserved on refresh.
|
|
273
|
-
|
|
274
|
-
This matches the behavior of other Inertia adapters:
|
|
275
|
-
- Laravel: Uses ``fullUrl()`` minus scheme/host
|
|
276
|
-
- Rails: Uses ``request.fullpath``
|
|
277
|
-
- Django: Uses ``request.get_full_path()``
|
|
278
|
-
|
|
279
|
-
Args:
|
|
280
|
-
request: The request object.
|
|
281
|
-
|
|
282
|
-
Returns:
|
|
283
|
-
The path with query string if present, e.g., ``/reports?page=1&status=active``.
|
|
284
|
-
"""
|
|
285
|
-
path = request.url.path
|
|
286
|
-
query = request.url.query
|
|
287
|
-
return f"{path}?{query}" if query else path
|
|
288
|
-
|
|
289
|
-
|
|
290
210
|
class InertiaResponse(Response[T]):
|
|
291
211
|
"""Inertia Response"""
|
|
292
212
|
|
|
@@ -418,7 +338,7 @@ class InertiaResponse(Response[T]):
|
|
|
418
338
|
shared_props.pop(key, None)
|
|
419
339
|
|
|
420
340
|
route_content: Any | None = None
|
|
421
|
-
if is_or_contains_lazy_prop(self.content)
|
|
341
|
+
if is_or_contains_lazy_prop(self.content):
|
|
422
342
|
filtered_content = lazy_render(self.content, partial_data, inertia_plugin.portal, partial_except)
|
|
423
343
|
if filtered_content is not None:
|
|
424
344
|
route_content = filtered_content
|
|
@@ -438,11 +358,6 @@ class InertiaResponse(Response[T]):
|
|
|
438
358
|
shared_props["content"] = route_content
|
|
439
359
|
|
|
440
360
|
deferred_props = extract_deferred_props(shared_props) or None
|
|
441
|
-
# Extract once props tracked during get_shared_props (already rendered)
|
|
442
|
-
once_props_from_shared = shared_props.pop("_once_props", [])
|
|
443
|
-
# Also check route content for once props
|
|
444
|
-
once_props_from_content = extract_once_props(shared_props) or []
|
|
445
|
-
once_props = (once_props_from_shared + once_props_from_content) or None
|
|
446
361
|
|
|
447
362
|
merge_props_list, prepend_props_list, deep_merge_props_list, match_props_on = extract_merge_props(shared_props)
|
|
448
363
|
|
|
@@ -474,26 +389,19 @@ class InertiaResponse(Response[T]):
|
|
|
474
389
|
with contextlib.suppress(AttributeError, ImproperlyConfiguredException):
|
|
475
390
|
clear_history_flag = request.session.pop("_inertia_clear_history", False) # pyright: ignore[reportUnknownMemberType,reportAttributeAccessIssue]
|
|
476
391
|
|
|
477
|
-
# v2.3+ protocol: Extract flash to top level (not in props)
|
|
478
|
-
# This prevents flash from persisting in browser history state
|
|
479
|
-
# Always send {} for empty flash to support router.flash((current) => ({ ...current }))
|
|
480
|
-
flash_data: "dict[str, list[str]]" = shared_props.pop("flash", None) or {}
|
|
481
|
-
|
|
482
392
|
return PageProps[T](
|
|
483
393
|
component=request.inertia.route_component, # type: ignore[attr-defined] # pyright: ignore[reportUnknownArgumentType,reportUnknownMemberType,reportAttributeAccessIssue]
|
|
484
394
|
props=shared_props, # pyright: ignore[reportArgumentType]
|
|
485
395
|
version=vite_plugin.asset_loader.version_id,
|
|
486
|
-
url=
|
|
396
|
+
url=request.url.path,
|
|
487
397
|
encrypt_history=encrypt_history,
|
|
488
398
|
clear_history=clear_history_flag,
|
|
489
399
|
deferred_props=deferred_props,
|
|
490
|
-
once_props=once_props,
|
|
491
400
|
merge_props=merge_props_list or None,
|
|
492
401
|
prepend_props=prepend_props_list or None,
|
|
493
402
|
deep_merge_props=deep_merge_props_list or None,
|
|
494
403
|
match_props_on=match_props_on or None,
|
|
495
404
|
scroll_props=extracted_scroll_props,
|
|
496
|
-
flash=flash_data,
|
|
497
405
|
)
|
|
498
406
|
|
|
499
407
|
def _render_template(
|
|
@@ -583,11 +491,7 @@ class InertiaResponse(Response[T]):
|
|
|
583
491
|
ssr_config = inertia_plugin.config.ssr_config
|
|
584
492
|
if ssr_config is not None:
|
|
585
493
|
ssr_payload = _render_inertia_ssr_sync(
|
|
586
|
-
page_dict,
|
|
587
|
-
ssr_config.url,
|
|
588
|
-
timeout_seconds=ssr_config.timeout,
|
|
589
|
-
portal=inertia_plugin.portal,
|
|
590
|
-
client=inertia_plugin.ssr_client,
|
|
494
|
+
page_dict, ssr_config.url, timeout_seconds=ssr_config.timeout, portal=inertia_plugin.portal
|
|
591
495
|
)
|
|
592
496
|
|
|
593
497
|
csrf_token = self._get_csrf_token(request)
|
litestar_vite/inertia/types.py
CHANGED
|
@@ -27,15 +27,6 @@ MergeStrategy = Literal["append", "prepend", "deep"]
|
|
|
27
27
|
_SNAKE_CASE_PATTERN = re.compile(r"_([a-z])")
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def _empty_flash_factory() -> "dict[str, list[str]]":
|
|
31
|
-
"""Return an empty flash dict with proper type annotation.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
Empty dict[str, list[str]] for flash messages.
|
|
35
|
-
"""
|
|
36
|
-
return {}
|
|
37
|
-
|
|
38
|
-
|
|
39
30
|
def to_camel_case(snake_str: str) -> str:
|
|
40
31
|
"""Convert snake_case string to camelCase.
|
|
41
32
|
|
|
@@ -277,8 +268,6 @@ class PageProps(Generic[T]):
|
|
|
277
268
|
match_props_on: Keys for matching items during merge (v2).
|
|
278
269
|
deferred_props: Configuration for lazy-loaded props (v2).
|
|
279
270
|
scroll_props: Configuration for infinite scroll (v2).
|
|
280
|
-
flash: Flash messages as top-level property (v2.3+). Unlike props, flash
|
|
281
|
-
messages are NOT persisted in browser history state.
|
|
282
271
|
"""
|
|
283
272
|
|
|
284
273
|
component: str
|
|
@@ -296,16 +285,8 @@ class PageProps(Generic[T]):
|
|
|
296
285
|
|
|
297
286
|
deferred_props: "dict[str, list[str]] | None" = None
|
|
298
287
|
|
|
299
|
-
# v2.2.20+ protocol: Props that should only be resolved once and cached client-side
|
|
300
|
-
once_props: "list[str] | None" = None
|
|
301
|
-
|
|
302
288
|
scroll_props: "ScrollPropsConfig | None" = None
|
|
303
289
|
|
|
304
|
-
# v2.3+ protocol: Flash messages at top level (not in props)
|
|
305
|
-
# This prevents flash from persisting in browser history state
|
|
306
|
-
# Always send {} for empty flash to support router.flash((current) => ({ ...current }))
|
|
307
|
-
flash: "dict[str, list[str]]" = field(default_factory=_empty_flash_factory)
|
|
308
|
-
|
|
309
290
|
def to_dict(self) -> dict[str, Any]:
|
|
310
291
|
"""Convert to Inertia.js protocol format with camelCase keys.
|
|
311
292
|
|
litestar_vite/loader.py
CHANGED
|
@@ -12,7 +12,6 @@ Key features:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import hashlib
|
|
15
|
-
import html
|
|
16
15
|
from functools import cached_property
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from textwrap import dedent
|
|
@@ -270,7 +269,10 @@ class ViteAssetLoader:
|
|
|
270
269
|
Returns:
|
|
271
270
|
Absolute path to the Vite manifest file.
|
|
272
271
|
"""
|
|
273
|
-
|
|
272
|
+
bundle_dir = self._config.bundle_dir
|
|
273
|
+
if not bundle_dir.is_absolute():
|
|
274
|
+
bundle_dir = self._config.root_dir / bundle_dir
|
|
275
|
+
return bundle_dir / self._config.manifest_name
|
|
274
276
|
|
|
275
277
|
def _get_hot_file_path(self) -> Path:
|
|
276
278
|
"""Get the path to the hot file.
|
|
@@ -400,7 +402,7 @@ class ViteAssetLoader:
|
|
|
400
402
|
if path not in self._manifest:
|
|
401
403
|
raise AssetNotFoundError(path, str(self._get_manifest_path()))
|
|
402
404
|
|
|
403
|
-
return urljoin(self._config.asset_url, self._manifest[path]["file"])
|
|
405
|
+
return urljoin(self._config.base_url or self._config.asset_url, self._manifest[path]["file"])
|
|
404
406
|
|
|
405
407
|
def generate_ws_client_tags(self) -> str:
|
|
406
408
|
"""Generate the Vite HMR client script tag.
|
|
@@ -423,10 +425,8 @@ class ViteAssetLoader:
|
|
|
423
425
|
React refresh script HTML or empty string.
|
|
424
426
|
"""
|
|
425
427
|
if self._config.is_react and self._is_hot_dev:
|
|
426
|
-
nonce = self._config.csp_nonce
|
|
427
|
-
nonce_attr = f' nonce="{html.escape(nonce, quote=True)}"' if nonce else ""
|
|
428
428
|
return dedent(f"""
|
|
429
|
-
<script type="module"
|
|
429
|
+
<script type="module">
|
|
430
430
|
import RefreshRuntime from '{self._vite_server_url()}@react-refresh'
|
|
431
431
|
RefreshRuntime.injectIntoGlobalHook(window)
|
|
432
432
|
window.$RefreshReg$ = () => {{}}
|
|
@@ -472,7 +472,7 @@ class ViteAssetLoader:
|
|
|
472
472
|
if not scripts_attrs:
|
|
473
473
|
scripts_attrs = {"type": "module", "async": "", "defer": ""}
|
|
474
474
|
|
|
475
|
-
asset_url_base = self._config.asset_url
|
|
475
|
+
asset_url_base = self._config.base_url or self._config.asset_url
|
|
476
476
|
|
|
477
477
|
for manifest in manifest_entries.values():
|
|
478
478
|
if "css" in manifest:
|