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
litestar_vite/deploy.py CHANGED
@@ -8,13 +8,13 @@ DeployConfig is defined in litestar_vite.config and passed into ViteDeployer.
8
8
 
9
9
  from collections.abc import Callable, Iterable
10
10
  from dataclasses import dataclass
11
+ from importlib.util import find_spec
11
12
  from pathlib import Path
12
13
  from typing import Any, cast
13
14
 
14
15
  from litestar.exceptions import SerializationException
15
16
  from litestar.serialization import decode_json
16
17
 
17
- from litestar_vite.config import FSSPEC_INSTALLED
18
18
  from litestar_vite.config import DeployConfig as _DeployConfig
19
19
  from litestar_vite.exceptions import MissingDependencyError
20
20
 
@@ -51,7 +51,7 @@ def _import_fsspec(storage_backend: "str | None") -> tuple[Any, Callable[..., tu
51
51
  Raises:
52
52
  MissingDependencyError: If fsspec is not installed.
53
53
  """
54
- if not FSSPEC_INSTALLED:
54
+ if find_spec("fsspec") is None:
55
55
  msg = "fsspec"
56
56
  raise MissingDependencyError(msg, install_package=_suggest_install_extra(storage_backend))
57
57
 
@@ -110,18 +110,7 @@ class ViteDeployer:
110
110
  raise ValueError(msg)
111
111
 
112
112
  self.bundle_dir = bundle_dir
113
- manifest_rel = Path(manifest_name)
114
- manifest_path = bundle_dir / manifest_rel
115
- if (
116
- not manifest_path.exists()
117
- and not manifest_rel.is_absolute()
118
- and (not manifest_rel.parts or manifest_rel.parts[0] != ".vite")
119
- ):
120
- vite_manifest = bundle_dir / ".vite" / manifest_rel
121
- if vite_manifest.exists():
122
- manifest_path = vite_manifest
123
-
124
- self.manifest_path = manifest_path
113
+ self.manifest_path = bundle_dir / manifest_name
125
114
  self.config = deploy_config
126
115
  self._fs, self.remote_path = self._init_filesystem(fs, remote_path)
127
116
 
litestar_vite/doctor.py CHANGED
@@ -421,10 +421,8 @@ class ViteDoctor:
421
421
  A dictionary representing the expected bridge configuration.
422
422
  """
423
423
  types = self.config.types if isinstance(self.config.types, TypeGenConfig) else None
424
- deploy = self.config.deploy_config
425
424
  return {
426
425
  "assetUrl": self.config.asset_url,
427
- "deployAssetUrl": deploy.asset_url if deploy is not None and deploy.asset_url else None,
428
426
  "bundleDir": str(self.config.bundle_dir),
429
427
  "hotFile": self.config.hot_file,
430
428
  "resourceDir": str(self.config.resource_dir),
@@ -434,6 +432,7 @@ class ViteDoctor:
434
432
  "proxyMode": self.config.proxy_mode,
435
433
  "port": self.config.port,
436
434
  "host": self.config.host,
435
+ "ssrEnabled": self.config.ssr_enabled,
437
436
  "ssrOutDir": str(self.config.ssr_output_dir) if self.config.ssr_output_dir else None,
438
437
  "types": (
439
438
  {
@@ -966,14 +965,13 @@ class ViteDoctor:
966
965
  if self.config.is_dev_mode:
967
966
  return
968
967
 
969
- candidates = self.config.candidate_manifest_paths()
970
- if not any(path.exists() for path in candidates):
971
- manifest_locations = " or ".join(str(path) for path in candidates)
968
+ manifest_path = self._resolve_to_root(self.config.bundle_dir) / self.config.manifest_name
969
+ if not manifest_path.exists():
972
970
  self.issues.append(
973
971
  DoctorIssue(
974
972
  check="Manifest Missing",
975
973
  severity="warning",
976
- message=f"Manifest not found at {manifest_locations} (expected in production; ok during dev)",
974
+ message=f"Manifest not found at {manifest_path} (expected in production; ok during dev)",
977
975
  fix_hint="Run `litestar assets build` before starting in production",
978
976
  auto_fixable=False,
979
977
  )
@@ -1021,9 +1019,8 @@ class ViteDoctor:
1021
1019
  "VITE_HOST": self.config.host,
1022
1020
  "VITE_PROXY_MODE": self.config.proxy_mode,
1023
1021
  "VITE_PROTOCOL": self.config.protocol,
1022
+ "VITE_BASE_URL": self.config.base_url or self.config.asset_url,
1024
1023
  }
1025
- if self.config.base_url:
1026
- comparisons["VITE_BASE_URL"] = self.config.base_url
1027
1024
 
1028
1025
  for key, expected in comparisons.items():
1029
1026
  actual = os.getenv(key)
@@ -1063,6 +1060,7 @@ class ViteDoctor:
1063
1060
  "dev_mode": self.config.is_dev_mode,
1064
1061
  "host": self.config.host,
1065
1062
  "port": self.config.port,
1063
+ "ssr_enabled": self.config.ssr_enabled,
1066
1064
  "executor": self.config.runtime.executor,
1067
1065
  "set_environment": self.config.set_environment,
1068
1066
  "types_enabled": isinstance(self.config.types, TypeGenConfig),
litestar_vite/executor.py CHANGED
@@ -72,22 +72,9 @@ class JSExecutor(ABC):
72
72
  def install(self, cwd: Path) -> None:
73
73
  """Install dependencies."""
74
74
 
75
- @abstractmethod
76
- def update(self, cwd: Path, *, latest: bool = False) -> None:
77
- """Update dependencies.
78
-
79
- Args:
80
- cwd: The working directory.
81
- latest: If True, update to latest versions (ignoring semver constraints where supported).
82
- """
83
-
84
75
  @abstractmethod
85
76
  def run(self, args: list[str], cwd: Path) -> "subprocess.Popen[Any]":
86
- """Run a command.
87
-
88
- Returns:
89
- The result.
90
- """
77
+ """Run a command."""
91
78
 
92
79
  @abstractmethod
93
80
  def execute(self, args: list[str], cwd: Path) -> None:
@@ -155,10 +142,6 @@ class JSExecutor(ABC):
155
142
  class CommandExecutor(JSExecutor):
156
143
  """Generic command executor."""
157
144
 
158
- # Subclasses override to customize update behavior
159
- update_command: ClassVar[str] = "update"
160
- update_latest_flag: ClassVar[str] = "--latest"
161
-
162
145
  def install(self, cwd: Path) -> None:
163
146
  executable = self._resolve_executable()
164
147
  command = [executable, "install"]
@@ -166,15 +149,6 @@ class CommandExecutor(JSExecutor):
166
149
  if process.returncode != 0:
167
150
  raise ViteExecutionError(command, process.returncode, "package install failed")
168
151
 
169
- def update(self, cwd: Path, *, latest: bool = False) -> None:
170
- executable = self._resolve_executable()
171
- command = [executable, self.update_command]
172
- if latest and self.update_latest_flag:
173
- command.append(self.update_latest_flag)
174
- process = subprocess.run(command, cwd=cwd, shell=platform.system() == "Windows", check=False)
175
- if process.returncode != 0:
176
- raise ViteExecutionError(command, process.returncode, "package update failed")
177
-
178
152
  def run(self, args: list[str], cwd: Path) -> "subprocess.Popen[Any]":
179
153
  executable = self._resolve_executable()
180
154
  args = self._apply_silent_flag(args)
@@ -203,8 +177,6 @@ class NodeExecutor(CommandExecutor):
203
177
  """Node.js executor."""
204
178
 
205
179
  bin_name = "npm"
206
- # npm doesn't have --latest; use --save to update package.json
207
- update_latest_flag: ClassVar[str] = "--save"
208
180
 
209
181
 
210
182
  class BunExecutor(CommandExecutor):
@@ -218,22 +190,15 @@ class DenoExecutor(CommandExecutor):
218
190
 
219
191
  bin_name = "deno"
220
192
  silent_flag: ClassVar[str] = ""
221
- update_latest_flag: ClassVar[str] = ""
222
193
 
223
194
  def install(self, cwd: Path) -> None:
224
195
  pass
225
196
 
226
- def update(self, cwd: Path, *, latest: bool = False) -> None:
227
- """Deno doesn't have traditional package management."""
228
- del cwd, latest # unused
229
-
230
197
 
231
198
  class YarnExecutor(CommandExecutor):
232
199
  """Yarn executor."""
233
200
 
234
201
  bin_name = "yarn"
235
- # yarn uses "upgrade" command (not "update")
236
- update_command: ClassVar[str] = "upgrade"
237
202
 
238
203
 
239
204
  class PnpmExecutor(CommandExecutor):
@@ -299,15 +264,6 @@ class NodeenvExecutor(JSExecutor):
299
264
  command = [npm_path, "install"]
300
265
  subprocess.run(command, cwd=cwd, check=True)
301
266
 
302
- def update(self, cwd: Path, *, latest: bool = False) -> None:
303
- npm_path = self._find_npm_in_venv()
304
- command = [npm_path, "update"]
305
- if latest:
306
- command.append("--save")
307
- process = subprocess.run(command, cwd=cwd, shell=platform.system() == "Windows", check=False)
308
- if process.returncode != 0:
309
- raise ViteExecutionError(command, process.returncode, "package update failed")
310
-
311
267
  def run(self, args: list[str], cwd: Path) -> "subprocess.Popen[Any]":
312
268
  npm_path = self._find_npm_in_venv()
313
269
  args = self._apply_silent_flag(args)
@@ -0,0 +1,9 @@
1
+ """SPA mode handler public API.
2
+
3
+ The SPA handler implementation lives in ``litestar_vite._handler.app``. This module exists as a stable import
4
+ location for users and tests.
5
+ """
6
+
7
+ from litestar_vite._handler.app import AppHandler
8
+
9
+ __all__ = ("AppHandler",)
@@ -91,8 +91,7 @@ def _escape_attr(value: str) -> str:
91
91
  The escaped value safe for use in HTML attribute values.
92
92
  """
93
93
  return (
94
- value
95
- .replace("&", "&")
94
+ value.replace("&", "&")
96
95
  .replace('"', """)
97
96
  .replace("'", "'")
98
97
  .replace("<", "&lt;")
@@ -136,14 +135,13 @@ def _set_inner_html_replacer(match: re.Match[str], *, content: str) -> str:
136
135
  return match.group(1) + content + match.group(5)
137
136
 
138
137
 
139
- def inject_head_script(html: str, script: str, *, escape: bool = True, nonce: str | None = None) -> str:
138
+ def inject_head_script(html: str, script: str, *, escape: bool = True) -> str:
140
139
  """Inject a script tag before the closing </head> tag.
141
140
 
142
141
  Args:
143
142
  html: The HTML document.
144
143
  script: The JavaScript code to inject (without <script> tags).
145
144
  escape: Whether to escape the script content. Default True.
146
- nonce: Optional CSP nonce to add to the injected ``<script>`` tag.
147
145
 
148
146
  Returns:
149
147
  The HTML with the injected script. If ``</head>`` is not found,
@@ -160,8 +158,7 @@ def inject_head_script(html: str, script: str, *, escape: bool = True, nonce: st
160
158
  if escape:
161
159
  script = _escape_script(script)
162
160
 
163
- nonce_attr = f' nonce="{_escape_attr(nonce)}"' if nonce else ""
164
- script_tag = f"<script{nonce_attr}>{script}</script>\n"
161
+ script_tag = f"<script>{script}</script>\n"
165
162
 
166
163
  head_end_match = _HEAD_END_PATTERN.search(html)
167
164
  if head_end_match:
@@ -303,55 +300,7 @@ def set_element_inner_html(html: str, selector: str, content: str) -> str:
303
300
  return pattern.sub(replacer, html, count=1)
304
301
 
305
302
 
306
- def inject_page_script(html: str, json_data: str, *, nonce: str | None = None, script_id: str = "app_page") -> str:
307
- r"""Inject page data as a JSON script element before ``</body>``.
308
-
309
- This is an Inertia.js v2.3+ optimization that embeds page data in a
310
- ``<script type="application/json">`` element instead of a ``data-page`` attribute.
311
- This provides ~37% payload reduction for large pages by avoiding HTML entity escaping.
312
-
313
- The script element is inserted before ``</body>`` with:
314
- - ``type="application/json"`` (non-executable, just data)
315
- - ``id="app_page"`` (Inertia's expected ID for useScriptElementForInitialPage)
316
- - Optional ``nonce`` for CSP compliance
317
-
318
- Args:
319
- html: The HTML document.
320
- json_data: Pre-serialized JSON string (page props).
321
- nonce: Optional CSP nonce to add to the script element.
322
- script_id: The script element ID (default "app_page" per Inertia protocol).
323
-
324
- Returns:
325
- The HTML with the script element injected before ``</body>``.
326
- Falls back to appending at the end if no ``</body>`` tag is found.
327
-
328
- Note:
329
- The JSON content is escaped to prevent XSS via ``</script>`` injection.
330
- Sequences like ``</`` are replaced with ``<\\/`` (escaped forward slash)
331
- which is valid JSON and prevents HTML parser issues.
332
-
333
- Example:
334
- html = inject_page_script(html, '{"component":"Home","props":{}}')
335
- """
336
- if not json_data:
337
- return html
338
-
339
- # Escape sequences that could break out of script element
340
- # Replace </ with <\/ to prevent premature tag closure (XSS prevention)
341
- escaped_json = json_data.replace("</", r"<\/")
342
-
343
- nonce_attr = f' nonce="{_escape_attr(nonce)}"' if nonce else ""
344
- script_tag = f'<script type="application/json" id="{script_id}"{nonce_attr}>{escaped_json}</script>\n'
345
-
346
- body_end_match = _BODY_END_PATTERN.search(html)
347
- if body_end_match:
348
- pos = body_end_match.start()
349
- return html[:pos] + script_tag + html[pos:]
350
-
351
- return html + "\n" + script_tag
352
-
353
-
354
- def inject_json_script(html: str, var_name: str, data: dict[str, Any], *, nonce: str | None = None) -> str:
303
+ def inject_json_script(html: str, var_name: str, data: dict[str, Any]) -> str:
355
304
  """Inject a script that sets a global JavaScript variable to JSON data.
356
305
 
357
306
  This is a convenience function for injecting structured data into the page.
@@ -362,7 +311,6 @@ def inject_json_script(html: str, var_name: str, data: dict[str, Any], *, nonce:
362
311
  html: The HTML document.
363
312
  var_name: The global variable name (e.g., "__LITESTAR_ROUTES__").
364
313
  data: The data to serialize as JSON.
365
- nonce: Optional CSP nonce to add to the injected ``<script>`` tag.
366
314
 
367
315
  Returns:
368
316
  The HTML with the injected script in the ``<head>`` section. Falls back
@@ -378,98 +326,7 @@ def inject_json_script(html: str, var_name: str, data: dict[str, Any], *, nonce:
378
326
  """
379
327
  json_data = encode_json(data).decode("utf-8")
380
328
  script = f"window.{var_name} = {json_data};"
381
- return inject_head_script(html, script, escape=False, nonce=nonce)
382
-
383
-
384
- def inject_vite_dev_scripts(
385
- html: str,
386
- vite_url: str,
387
- *,
388
- asset_url: str = "/static/",
389
- is_react: bool = False,
390
- csp_nonce: str | None = None,
391
- resource_dir: str | None = None,
392
- ) -> str:
393
- """Inject Vite dev server scripts for HMR support.
394
-
395
- This function injects the necessary scripts for Vite's Hot Module Replacement
396
- (HMR) to work when serving HTML from the backend (e.g., in hybrid/Inertia mode).
397
- The scripts are injected into the ``<head>`` section.
398
-
399
- For React apps, a preamble script is injected before the Vite client to
400
- enable React Fast Refresh.
401
-
402
- Scripts are injected as relative URLs using the ``asset_url`` prefix. This
403
- routes them through Litestar's proxy middleware, which forwards to Vite
404
- with the correct base path handling.
405
-
406
- When ``resource_dir`` is provided, entry point script URLs are also transformed
407
- to include the asset URL prefix (e.g., ``/resources/main.tsx`` becomes
408
- ``/static/resources/main.tsx``).
409
-
410
- Args:
411
- html: The HTML document.
412
- vite_url: The Vite dev server URL (kept for backward compatibility, unused).
413
- asset_url: The asset URL prefix (e.g., "/static/"). Scripts are served
414
- at ``{asset_url}@vite/client`` etc.
415
- is_react: Whether to inject the React Fast Refresh preamble.
416
- csp_nonce: Optional CSP nonce to add to injected ``<script>`` tags.
417
- resource_dir: Optional resource directory name (e.g., "resources", "src").
418
- When provided, script sources starting with ``/{resource_dir}/`` are
419
- prefixed with ``asset_url``.
420
-
421
- Returns:
422
- The HTML with Vite dev scripts injected. Scripts are inserted before
423
- ``</head>`` when present, otherwise before ``</html>`` or at the end.
424
-
425
- Example:
426
- html = inject_vite_dev_scripts(html, "", asset_url="/static/", is_react=True)
427
- """
428
- # Use relative URLs with asset_url prefix so requests go through Litestar's proxy
429
- # This ensures proper base path handling (Vite expects /static/@vite/client, not /@vite/client)
430
- base = asset_url.rstrip("/")
431
- nonce_attr = f' nonce="{_escape_attr(csp_nonce)}"' if csp_nonce else ""
432
-
433
- # Transform entry point script URLs to include the asset URL prefix
434
- # This ensures /resources/main.tsx becomes /static/resources/main.tsx
435
- if resource_dir:
436
- resource_prefix = f"/{resource_dir.strip('/')}/"
437
-
438
- def transform_entry_script(match: re.Match[str]) -> str:
439
- prefix = match.group(1)
440
- src = match.group(2)
441
- suffix = match.group(3)
442
- if src.startswith(resource_prefix) and not src.startswith(base):
443
- return prefix + base + src + suffix
444
- return match.group(0)
445
-
446
- html = _SCRIPT_SRC_PATTERN.sub(transform_entry_script, html)
447
-
448
- scripts: list[str] = []
449
-
450
- if is_react:
451
- react_preamble = f"""import RefreshRuntime from '{base}/@react-refresh'
452
- RefreshRuntime.injectIntoGlobalHook(window)
453
- window.$RefreshReg$ = () => {{}}
454
- window.$RefreshSig$ = () => (type) => type
455
- window.__vite_plugin_react_preamble_installed__ = true"""
456
- scripts.append(f'<script type="module"{nonce_attr}>{react_preamble}</script>')
457
-
458
- scripts.append(f'<script type="module" src="{base}/@vite/client"{nonce_attr}></script>')
459
-
460
- script_content = "\n".join(scripts) + "\n"
461
-
462
- head_end_match = _HEAD_END_PATTERN.search(html)
463
- if head_end_match:
464
- pos = head_end_match.start()
465
- return html[:pos] + script_content + html[pos:]
466
-
467
- html_end_match = _HTML_END_PATTERN.search(html)
468
- if html_end_match:
469
- pos = html_end_match.start()
470
- return html[:pos] + script_content + html[pos:]
471
-
472
- return html + "\n" + script_content
329
+ return inject_head_script(html, script, escape=False)
473
330
 
474
331
 
475
332
  def transform_asset_urls(
@@ -2,41 +2,27 @@ from litestar_vite.config import InertiaConfig
2
2
  from litestar_vite.inertia import helpers
3
3
  from litestar_vite.inertia.exception_handler import create_inertia_exception_response, exception_to_http_response
4
4
  from litestar_vite.inertia.helpers import (
5
- AlwaysProp,
6
- OnceProp,
7
- OptionalProp,
8
5
  PropFilter,
9
- always,
10
6
  clear_history,
11
7
  defer,
12
8
  error,
13
9
  except_,
14
10
  extract_deferred_props,
15
11
  extract_merge_props,
16
- extract_once_props,
17
12
  flash,
18
13
  get_shared_props,
19
14
  lazy,
20
15
  merge,
21
- once,
22
16
  only,
23
- optional,
24
17
  scroll_props,
25
18
  share,
26
19
  )
27
20
  from litestar_vite.inertia.middleware import InertiaMiddleware
28
21
  from litestar_vite.inertia.plugin import InertiaPlugin
29
- from litestar_vite.inertia.precognition import (
30
- PrecognitionResponse,
31
- create_precognition_exception_handler,
32
- normalize_validation_errors,
33
- precognition,
34
- )
35
22
  from litestar_vite.inertia.request import InertiaDetails, InertiaHeaders, InertiaRequest
36
23
  from litestar_vite.inertia.response import InertiaBack, InertiaExternalRedirect, InertiaRedirect, InertiaResponse
37
24
 
38
25
  __all__ = (
39
- "AlwaysProp",
40
26
  "InertiaBack",
41
27
  "InertiaConfig",
42
28
  "InertiaDetails",
@@ -47,31 +33,21 @@ __all__ = (
47
33
  "InertiaRedirect",
48
34
  "InertiaRequest",
49
35
  "InertiaResponse",
50
- "OnceProp",
51
- "OptionalProp",
52
- "PrecognitionResponse",
53
36
  "PropFilter",
54
- "always",
55
37
  "clear_history",
56
38
  "create_inertia_exception_response",
57
- "create_precognition_exception_handler",
58
39
  "defer",
59
40
  "error",
60
41
  "except_",
61
42
  "exception_to_http_response",
62
43
  "extract_deferred_props",
63
44
  "extract_merge_props",
64
- "extract_once_props",
65
45
  "flash",
66
46
  "get_shared_props",
67
47
  "helpers",
68
48
  "lazy",
69
49
  "merge",
70
- "normalize_validation_errors",
71
- "once",
72
50
  "only",
73
- "optional",
74
- "precognition",
75
51
  "scroll_props",
76
52
  "share",
77
53
  )
@@ -30,11 +30,6 @@ class InertiaHeaders(str, Enum):
30
30
 
31
31
  INFINITE_SCROLL_MERGE_INTENT = "X-Inertia-Infinite-Scroll-Merge-Intent"
32
32
 
33
- # Precognition headers (Laravel Precognition protocol)
34
- PRECOGNITION = "Precognition"
35
- PRECOGNITION_SUCCESS = "Precognition-Success"
36
- PRECOGNITION_VALIDATE_ONLY = "Precognition-Validate-Only"
37
-
38
33
 
39
34
  def get_enabled_header(enabled: bool = True) -> "dict[str, Any]":
40
35
  """True if inertia is enabled.
@@ -1,12 +1,12 @@
1
1
  import re
2
2
  from typing import TYPE_CHECKING, Any, cast
3
- from urllib.parse import quote, urlparse, urlunparse
4
3
 
5
4
  from litestar import MediaType
6
5
  from litestar.connection import Request
7
6
  from litestar.connection.base import AuthT, StateT, UserT
8
7
  from litestar.exceptions import (
9
8
  HTTPException,
9
+ ImproperlyConfiguredException,
10
10
  InternalServerException,
11
11
  NotAuthorizedException,
12
12
  NotFoundException,
@@ -16,6 +16,7 @@ from litestar.exceptions.responses import (
16
16
  create_debug_response, # pyright: ignore[reportUnknownVariableType]
17
17
  create_exception_response, # pyright: ignore[reportUnknownVariableType]
18
18
  )
19
+ from litestar.plugins.flash import flash
19
20
  from litestar.repository.exceptions import (
20
21
  ConflictError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
21
22
  NotFoundError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
@@ -32,7 +33,7 @@ from litestar.status_codes import (
32
33
  HTTP_500_INTERNAL_SERVER_ERROR,
33
34
  )
34
35
 
35
- from litestar_vite.inertia.helpers import error, flash
36
+ from litestar_vite.inertia.helpers import error
36
37
  from litestar_vite.inertia.request import InertiaRequest
37
38
  from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse
38
39
 
@@ -132,9 +133,11 @@ def create_inertia_exception_response(request: "Request[UserT, AuthT, StateT]",
132
133
  if extras:
133
134
  content.update({"extra": extras})
134
135
 
135
- flash_succeeded = False
136
- if detail:
137
- flash_succeeded = flash(request, detail, category="error")
136
+ try:
137
+ if detail:
138
+ flash(request, detail, category="error")
139
+ except (AttributeError, KeyError, RuntimeError, ImproperlyConfiguredException):
140
+ request.logger.warning("Unable to set flash message", exc_info=True)
138
141
 
139
142
  if extras and isinstance(extras, (list, tuple)) and len(extras) >= 1: # pyright: ignore[reportUnknownArgumentType]
140
143
  first_extra = extras[0] # pyright: ignore[reportUnknownVariableType]
@@ -147,28 +150,19 @@ def create_inertia_exception_response(request: "Request[UserT, AuthT, StateT]",
147
150
  field = match.group(1) if match else default_field
148
151
  error(request, field, error_detail or detail)
149
152
 
150
- if status_code in {HTTP_422_UNPROCESSABLE_ENTITY, HTTP_400_BAD_REQUEST} or isinstance(
151
- exc, PermissionDeniedException
152
- ):
153
+ if status_code in {HTTP_422_UNPROCESSABLE_ENTITY, HTTP_400_BAD_REQUEST}:
154
+ return InertiaBack(request)
155
+ if isinstance(exc, PermissionDeniedException):
153
156
  return InertiaBack(request)
154
157
 
155
158
  if inertia_plugin is None:
156
159
  return InertiaResponse[Any](media_type=preferred_type, content=content, status_code=status_code)
157
160
 
158
- is_unauthorized = status_code == HTTP_401_UNAUTHORIZED or isinstance(exc, NotAuthorizedException)
159
- redirect_to_login = inertia_plugin.config.redirect_unauthorized_to
160
- if is_unauthorized and redirect_to_login is not None:
161
- if request.url.path != redirect_to_login:
162
- # If flash failed (no session), pass error message via query param
163
- if not flash_succeeded and detail:
164
- parsed = urlparse(redirect_to_login)
165
- error_param = f"error={quote(detail, safe='')}"
166
- query = f"{parsed.query}&{error_param}" if parsed.query else error_param
167
- redirect_to_login = urlunparse(parsed._replace(query=query))
168
- return InertiaRedirect(request, redirect_to=redirect_to_login)
169
- # Already on login page - redirect back so Inertia processes flash messages
170
- # (Inertia.js shows 4xx responses in a modal instead of updating page state)
171
- return InertiaBack(request)
161
+ if (status_code == HTTP_401_UNAUTHORIZED or isinstance(exc, NotAuthorizedException)) and (
162
+ inertia_plugin.config.redirect_unauthorized_to is not None
163
+ and request.url.path != inertia_plugin.config.redirect_unauthorized_to
164
+ ):
165
+ return InertiaRedirect(request, redirect_to=inertia_plugin.config.redirect_unauthorized_to)
172
166
 
173
167
  if status_code in {HTTP_404_NOT_FOUND, HTTP_405_METHOD_NOT_ALLOWED} and (
174
168
  inertia_plugin.config.redirect_404 is not None and request.url.path != inertia_plugin.config.redirect_404