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.
Files changed (55) hide show
  1. litestar_vite/_codegen/__init__.py +26 -0
  2. litestar_vite/_codegen/inertia.py +407 -0
  3. litestar_vite/{codegen/_openapi.py → _codegen/openapi.py} +11 -58
  4. litestar_vite/{codegen/_routes.py → _codegen/routes.py} +43 -110
  5. litestar_vite/{codegen/_ts.py → _codegen/ts.py} +19 -19
  6. litestar_vite/_handler/__init__.py +8 -0
  7. litestar_vite/{handler/_app.py → _handler/app.py} +29 -117
  8. litestar_vite/cli.py +254 -155
  9. litestar_vite/codegen.py +39 -0
  10. litestar_vite/commands.py +6 -0
  11. litestar_vite/{config/__init__.py → config.py} +726 -99
  12. litestar_vite/deploy.py +3 -14
  13. litestar_vite/doctor.py +6 -8
  14. litestar_vite/executor.py +1 -45
  15. litestar_vite/handler.py +9 -0
  16. litestar_vite/html_transform.py +5 -148
  17. litestar_vite/inertia/__init__.py +0 -24
  18. litestar_vite/inertia/_utils.py +0 -5
  19. litestar_vite/inertia/exception_handler.py +16 -22
  20. litestar_vite/inertia/helpers.py +18 -546
  21. litestar_vite/inertia/plugin.py +11 -77
  22. litestar_vite/inertia/request.py +0 -48
  23. litestar_vite/inertia/response.py +17 -113
  24. litestar_vite/inertia/types.py +0 -19
  25. litestar_vite/loader.py +7 -7
  26. litestar_vite/plugin.py +2184 -0
  27. litestar_vite/templates/angular/package.json.j2 +1 -2
  28. litestar_vite/templates/angular-cli/package.json.j2 +1 -2
  29. litestar_vite/templates/base/package.json.j2 +1 -2
  30. litestar_vite/templates/react-inertia/package.json.j2 +1 -2
  31. litestar_vite/templates/vue-inertia/package.json.j2 +1 -2
  32. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/METADATA +5 -5
  33. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/RECORD +36 -49
  34. litestar_vite/codegen/__init__.py +0 -48
  35. litestar_vite/codegen/_export.py +0 -229
  36. litestar_vite/codegen/_inertia.py +0 -619
  37. litestar_vite/codegen/_utils.py +0 -141
  38. litestar_vite/config/_constants.py +0 -97
  39. litestar_vite/config/_deploy.py +0 -70
  40. litestar_vite/config/_inertia.py +0 -241
  41. litestar_vite/config/_paths.py +0 -63
  42. litestar_vite/config/_runtime.py +0 -235
  43. litestar_vite/config/_spa.py +0 -93
  44. litestar_vite/config/_types.py +0 -94
  45. litestar_vite/handler/__init__.py +0 -9
  46. litestar_vite/inertia/precognition.py +0 -274
  47. litestar_vite/plugin/__init__.py +0 -687
  48. litestar_vite/plugin/_process.py +0 -185
  49. litestar_vite/plugin/_proxy.py +0 -689
  50. litestar_vite/plugin/_proxy_headers.py +0 -244
  51. litestar_vite/plugin/_static.py +0 -37
  52. litestar_vite/plugin/_utils.py +0 -489
  53. /litestar_vite/{handler/_routing.py → _handler/routing.py} +0 -0
  54. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +0 -0
  55. {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  from contextlib import asynccontextmanager
2
- from typing import TYPE_CHECKING, Any
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", "_ssr_client", "config")
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
- try:
95
- with start_blocking_portal() as portal:
96
- self._portal = portal
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 HTTPException, ImproperlyConfiguredException, ValidationException
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
- # Register exception handlers
164
- exception_handlers: "dict[type[Exception] | int, Any]" = {
165
- Exception: exception_to_http_response,
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(portal=getattr(self, "_portal", None)),
124
+ DeferredProp: lambda val: val.render(),
191
125
  **(app_config.type_encoders or {}),
192
126
  }
193
127
  app_config.type_decoders = [
@@ -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, client)
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
- response: "httpx.Response"
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
- except httpx.RequestError as exc:
205
- msg = (
206
- f"Inertia SSR is enabled but the SSR server is not reachable at {url!r}. "
207
- "Start the SSR server (Node) or disable InertiaConfig.ssr."
208
- )
209
- raise ImproperlyConfiguredException(msg) from exc
210
- except httpx.HTTPStatusError as exc:
211
- msg = f"Inertia SSR server at {url!r} returned HTTP {exc.response.status_code}. Check the SSR server logs."
212
- raise ImproperlyConfiguredException(msg) from exc
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) or is_or_contains_special_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=_get_relative_url(request),
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)
@@ -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
- return self._config.resolve_manifest_path()
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"{nonce_attr}>
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: