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/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
|
|
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
|
-
|
|
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
|
-
|
|
970
|
-
if not
|
|
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 {
|
|
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)
|
litestar_vite/handler.py
ADDED
|
@@ -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",)
|
litestar_vite/html_transform.py
CHANGED
|
@@ -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("<", "<")
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
)
|
litestar_vite/inertia/_utils.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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}
|
|
151
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|